场景
公司的业务越来越复杂,我们需要抽出一个用户系统,向各个业务系统提供用户的基本信息。
用户系统作为非常基础的应用,公司内部会有很多个系统去调用,因此一定要注意性能问题。因此在用户系统中,可以增加一个内存缓存,当然具体的信息是存放在数据库里的。每当查找一个用户时会先在哈希表中进行查询,以此来提高访问的性能。
不过把缓存数据放在内存也会导致一个问题,就是时间长了之后,内存可能会由于数据太多而溢出,那么这时候就需要有缓存清除机制来保证系统正常运行。
LRU算法
LRU全称Least Recently Used,也就是最近最少使用的意思,是一种内存管理算法,该算法最早应用于Linux操作系统。
这个算法基于一种假设:长期不被使用的数据,在未来被用到的几率也不大。因此,当数据所占内存达到一定阈值时,我们要移除掉最近最少被使用的数据。
在LRU算法中,使用了一种有趣的数据结构,这种数据结构叫作哈希链表。
哈希链表
哈希表是由若干个Key-Value组成的。在“逻辑”上,这些Key-Value是无所谓排列顺序的,谁先谁后都一样。
在哈希链表中,这些Key-Value不再是彼此无关的存在,而是被一个链条串了起来。每一个Key-Value都具有它的前驱Key-Value、后继Key-Value,就像双向链表中的节点一样。
这样一来,原本无序的哈希表就拥有了固定的排列顺序。
LRU算法演示
1. 假设使用哈希链表来缓存用户信息,目前缓存了4个用户,这4个用户是按照被访问的时间顺序依次从链表右端插入的。
2. 如果这时业务方访问用户5,由于哈希链表中没有用户5的数据,需要从数据库中读取出来,插入到缓存中。此时,链表最右端是最新被访问的用户5,最左端是最近最少被访问的用户1。
3. 接下来,如果业务方访问用户2,哈希链表中已经存在用户2的数据,这时我们把用户2从它的前驱节点和后继节点之间移除,重新插入链表的最右端。此时,链表的最右端变成了最新被访问的用户2,最左端仍然是最近最少被访问的用户1。
4. 接下来,如果业务方请求修改用户4的信息。同样的道理,我们会把用户4从原来的位置移动到链表的最右侧,并把用户信息的值更新。这时,链表的最右端是最新被访问的用户4,最左端仍然是最近最少被访问的用户1。
5. 后来业务方又要访问用户6,用户6在缓存里没有,需要插入哈希链表中。假设这时缓存容量已经达到上限,必须先删除最近最少被访问的数据,那么位于哈希链表最左端的用户1就会被删除,然后再把用户6插入最右端的位置。
代码实现
public class LRUCache { private Node head; private Node end; // 缓存存储上限 private int limit; private HashMap<String, Node> hashMap; public LRUCache(int limit) { this.limit = limit; hashMap = new HashMap<String, Node>(); } public String get(String key) { Node node = hashMap.get(key); if(node == null) { return null; } refreshNode(node); return node.value; } public void put(String key, String value) { Node node = hashMap.get(key); if(node == null) { // 如果key不存在,插入key-value if(hashMap.size() >= limit) { String oldKey = removeNode(head); hashMap.remove(oldKey); } node = new Node(key, value); addNode(node); hashMap.put(key, node); } else { // 如果key存在,刷新key-value node.value = value; refreshNode(node); } } public void remove(String key) { Node node = hashMap.get(key); removeNode(node); hashMap.remove(key); } /** * * 刷新被访问的节点位置 * * @param node 被访问的节点 * */ private void refreshNode(Node node) { // 如果访问的是尾节点,无需移动节点 if(node == end) { return; } // 移除节点 removeNode(node); // 重新插入节点 addNode(node); } /** * * 删除节点 * * @param node 要删除的节点 * */ private String removeNode(Node node) { if(node == end) { // 移除尾节点 end = end.pre; }else if(node == head) { // 移除头节点 head = head.next; }else { // 移除中间节点 node.pre.next = node.next; node.next.pre = node.pre; } return node.key; } /** * * 尾部插入节点 * * @param node 要插入的节点 * */ private void addNode(Node node) { if (end != null) { end.next = node; node.pre = end; node.next = null; } end = node; if (head == null) { head = node; } } class Node { Node(String key, String value) { this.key = key; this.value = value; } public Node pre; public Node next; public String key; public String value; } public static void main(String[] args) { LRUCache lruCache = new LRUCache(5); lruCache.put("001", "用户1信息"); lruCache.put("002", "用户1信息"); lruCache.put("003", "用户1信息"); lruCache.put("004", "用户1信息"); lruCache.put("005", "用户1信息"); lruCache.get("002"); lruCache.put("004", "用户2信息更新"); lruCache.put("006", "用户6信息"); System.out.println(lruCache.get("001")); System.out.println(lruCache.get("006")); } }