万书网 > 文学作品 > 漫画算法:小灰的算法之旅 > 6.3LRU算法的应用

6.3LRU算法的应用





6.3.1 一个关于用户信息的需求

现在公司的业务越来越复杂,我们需要抽出一个用户系统,向各个业务系统提供用户的基本信息。

业务方对用户信息的查询频率很高,一定要注意性能问题哦。

放心吧,交给我,妥妥的!

用户信息当然是存放在数据库里。但是由于我们对用户系统的性能要求比较高,显然不能在每一次请求时都去查询数据库。

所以,小灰在内存中创建了一个哈希表作为缓存,每当查找一个用户时会先在哈希表中进行查询,以此来提高访问的性能。

很快,用户系统上线了,小灰美美地休息了几天。

一个多月之后……

小灰,小灰,大事不好了!

哦,出了什么事?

线上服务器宕机了!

让我看看……糟了,是内存溢出了,用户数量越来越多,当初设计的哈希表把内存给撑爆了,赶紧重启吧!

可是以后该怎么办呢?我们能不能给服务器的硬件升级,或者加几台服务器呀?

可是咱们公司没钱呀?!

那我能不能在内存快耗尽的时候,随机删掉一半用户缓存呢?

唉,这样也不妥,如果删掉的用户信息,正好是被高频查询的用户,会影响系统性能的。



6.3.2 用算法解决问题

小灰,你怎么日渐消瘦了啊?

唉,还不是被一个需求折腾的!

事情是这样子的……(小灰把工作中的难题告诉了大黄)

小灰,你听说过LRU算法吗?

只听说过URL,没听说过LRU,那是什么鬼?

LRU全称Least  Recently  Used,也就是最近最少使用的意思,是一种内存管理算法,该算法最早应用于Linux操作系统。

这个算法基于一种假设:长期不被使用的数据,在未来被用到的几率也不大。因此,当数据所占内存达到一定阈值时,我们要移除掉最近最少被使用的数据。

原来如此,这个算法正好对我的用户系统有帮助!可以在内存不够时,从哈希表中移除一部分很少被访问的用户。

可是,我怎么知道哈希表中哪些Key-Value最近被访问过,哪些没被访问过?总不能给每一个Value加上时间戳,然后遍历整个哈希表吧?

这就涉及LRU算法的精妙所在了。在LRU算法中,使用了一种有趣的数据结构,这种数据结构叫作哈希链表。

什么是哈希链表呢?

我们都知道,哈希表是由若干个Key-Value组成的。在“逻辑”上,这些Key-Value是无所谓排列顺序的,谁先谁后都一样。

在哈希链表中,这些Key-Value不再是彼此无关的存在,而是被一个链条串了起来。每一个Key-Value都具有它的前驱Key-Value、后继Key-Value,就像双向链表中的节点一样。

这样一来,原本无序的哈希表就拥有了固定的排列顺序。

可是,这哈希链表和LRU算法有什么关系呢?

依靠哈希链表的有序性,我们可以把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插入最右端的位置。

以上,就是LRU算法的基本思路。

明白了,这真是个巧妙的算法!那么LRU算法怎么用代码来实现呢?

虽然Java中的LinkedHashMap已经对哈希链表做了很好的实现,但为了加深印象,我们还是自己写代码来简单实现一下吧。

1.  private  Node  head;

2.  private  Node  end;

3.  //  缓存存储上限

4.  private  int  limit;

5.

6.  private  HashMap  hashMap;

7.

8.  public  LRUCache(int  limit)  {

9.  this.limit  =  limit;

10.  hashMap  =  new  HashMap();

11.  }

12.

13.  public  String  get(String  key)  {

14.  Node  node  =  hashMap.get(key);

15.  if  (node  ==  null){

16.  return  null;

17.  }

18.  refreshNode(node);

19.  return  node.value;

20.  }

21.

22.  public  void  put(String  key,  String  value)  {

23.  Node  node  =  hashMap.get(key);

24.  if  (node  ==  null)  {

25.  //如果Key  不存在,则插入Key-Value

26.  if  (hashMap.size()  >=  limit)  {

27.  String  oldKey  =  removeNode(head);

28.  hashMap.remove(oldKey);

29.  }

30.  node  =  new  Node(key,  value);

31.  addNode(node);

32.  hashMap.put(key,  node);

33.  }else  {

34.  //如果Key  存在,则刷新Key-Value

35.  node.value  =  value;

36.  refreshNode(node);

37.  }

38.  }

39.

40.  public  void  remove(String  key)  {

41.  Node  node  =  hashMap.get(key);

42.  removeNode(node);

43.  hashMap.remove(key);

44.  }

45.

46.  /**

47.  *  刷新被访问的节点位置

48.  *  @param  node  被访问的节点

49.  */

50.  private  void  refreshNode(Node  node)  {

51.  //如果访问的是尾节点,则无须移动节点

52.  if  (node  ==  end)  {

53.  return;

54.  }

55.  //移除节点

56.  removeNode(node);

57.  //重新插入节点

58.  addNode(node);

59.  }

60.

61.  /**

62.  *  删除节点

63.  *  @param  node  要删除的节点

64.  */

65.  private  String  removeNode(Node  node)  {

66.  if(node  ==  head  &&  node  ==  end){

67.  //移除唯一的节点

68.  head  =  null;

69.  end  =  null;

70.  }else  if(node  ==  end){

71.  //移除尾节点

72.  end  =  end.pre;

73.  end.next  =  null;

74.  }else  if(node  ==  head){

75.  //移除头节点

76.  head  =  head.next;

77.  head.pre  =  null;

78.  }else  {

79.  //移除中间节点

80.  node.pre.next  =  node.next;

81.  node.next.pre  =  node.pre;

82.  }

83.  return  node.key;

84.  }

85.

86.  /**


87.  *  尾部插入节点

88.  *  @param  node  要插入的节点

89.  */

90.  private  void  addNode(Node  node)  {

91.  if(end  !=  null)  {

92.  end.next  =  node;

93.  node.pre  =  end;

94.  node.next  =  null;

95.  }

96.  end  =  node;

97.  if(head  ==  null){

98.  head  =  node;

99.  }

100.}

101.

102.class  Node  {

103.  Node(String  key,  String  value){

104.  this.key  =  key;

105.  this.value  =  value;

106.  }

107.  public  Node  pre;

108.  public  Node  next;

109.  public  String  key;

110.  public  String  value;

111.}

112.

113.public  static  void  main(String[]  args)  {

114.  LRUCache  lruCache  =  new  LRUCache(5);

115.  lruCache.put("001",  "  用户1信息");

116.  lruCache.put("002",  "  用户1信息");

117.  lruCache.put("003",  "  用户1信息");

118.  lruCache.put("004",  "  用户1信息");

119.  lruCache.put("005",  "  用户1信息");

120.  lruCache.get("002");

121.  lruCache.put("004",  "  用户2信息更新");

122.  lruCache.put("006",  "  用户6信息");

123.  System.out.println(lruCache.get("001"));;

124.  System.out.println(lruCache.get("006"));;

125.}

需要注意的是,这段代码不是线程安全的代码,要想做到线程安全,需要加上synchronized修饰符。

小灰,对于用户系统的需求,你也可以使用缓存数据库Redis来实现,Redis底层也实现了类似LRU的回收算法。

啊,你怎么不早说?我直接用Redis就好了,省得费这么大劲去研究LRU算法。

千万不能这么想,底层原理和算法还是需要学习的,这样才能让我们更好地去选择技术方案,排查疑难问题。

好了,关于LRU算法就介绍到这里,咱们下一节再会!