缓存策略之LRU实现及分析

Posted 程序猿思维

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了缓存策略之LRU实现及分析相关的知识,希望对你有一定的参考价值。

  

  1. LRU定义

   

    Cache的容量有限,因此当Cache的容量用完后,而又有新的内容需要添加进来时,

    就需要挑选并舍弃原有的部分内容,从而腾出空间来放新内容。

    LRU Cache 的替换原则就是将最近最少使用的内容替换掉

     Least recently used. 可以理解为, 淘汰不常用的数据


2. 数据更新测量


  基于双链表的LRU算法的实现,

    它的原理: 将Cache的所有位置都用双连表连接起来,

a 访问操作: 

    当一个位置被命中之后,就将通过调整链表的指向,将该位置调整到链表头的位置,

 b 插入操作

  1 如果cache 未满 新加入的Cache直接加到链表头中。

 

  2 当需要替换内容时候,链表的最后位置就是最少被命中的位置,

   我们只需要淘汰链表最后的部分即可。

 



 

结果:

经常被访问数据在前面

不经常被访问数据在后面



  3 代码实现


    我们用一个对象来表示Cache,并实现双链表,

     

Java代码  

  1. public class LRUCache {  

  2.     /** 

  3.      * 链表节点 

  4.      * @author Administrator 

  5.      * 

  6.      */  

  7.     class CacheNode {  

  8.         ……  

  9.     }  

  10.   

  11.     private int cacheSize;//缓存大小  

  12.     private Hashtable nodes;//缓存容器  

  13.     private int currentSize;//当前缓存对象数量  

  14.     private CacheNode first;//(实现双链表)链表头  

  15.     private CacheNode last;//(实现双链表)链表尾  

  16. }  

 

 下面给出完整的实现,这个类也被Tomcat所使用( org.apache.tomcat.util.collections.LRUCache),但是在tomcat6.x版本中,已经被弃用,使用另外其他的缓存类来替代它。

 

Java代码  

  1. public class LRUCache {  

  2.     /** 

  3.      * 链表节点 

  4.      * @author Administrator 

  5.      * 

  6.      */  

  7.     class CacheNode {  

  8.         CacheNode prev;//前一节点  

  9.         CacheNode next;//后一节点  

  10.         Object value;//值  

  11.         Object key;//键  

  12.         CacheNode() {  

  13.         }  

  14.     }  

  15.   

  16.     public LRUCache(int i) {  

  17.         currentSize = 0;  

  18.         cacheSize = i;  

  19.         nodes = new Hashtable(i);//缓存容器  

  20.     }  

  21.       

  22.     /** 

  23.      * 获取缓存中对象 

  24.      * @param key 

  25.      * @return 

  26.      */  

  27.     public Object get(Object key) {  

  28.         CacheNode node = (CacheNode) nodes.get(key);  

  29.         if (node != null) {  

  30.             moveToHead(node);  

  31.             return node.value;  

  32.         } else {  

  33.             return null;  

  34.         }  

  35.     }  

  36.       

  37.     /** 

  38.      * 添加缓存 

  39.      * @param key 

  40.      * @param value 

  41.      */  

  42.     public void put(Object key, Object value) {  

  43.         CacheNode node = (CacheNode) nodes.get(key);  

  44.           

  45.         if (node == null) {  

  46.             //缓存容器是否已经超过大小.  

  47.             if (currentSize >= cacheSize) {  

  48.                 if (last != null)//将最少使用的删除  

  49.                     nodes.remove(last.key);  

  50.                 removeLast();  

  51.             } else {  

  52.                 currentSize++;  

  53.             }  

  54.               

  55.             node = new CacheNode();  

  56.         }  

  57.         node.value = value;  

  58.         node.key = key;  

  59.         //将最新使用的节点放到链表头,表示最新使用的.  

  60.         moveToHead(node);  

  61.         nodes.put(key, node);  

  62.     }  

  63.   

  64.     /** 

  65.      * 将缓存删除 

  66.      * @param key 

  67.      * @return 

  68.      */  

  69.     public Object remove(Object key) {  

  70.         CacheNode node = (CacheNode) nodes.get(key);  

  71.         if (node != null) {  

  72.             if (node.prev != null) {  

  73.                 node.prev.next = node.next;  

  74.             }  

  75.             if (node.next != null) {  

  76.                 node.next.prev = node.prev;  

  77.             }  

  78.             if (last == node)  

  79.                 last = node.prev;  

  80.             if (first == node)  

  81.                 first = node.next;  

  82.         }  

  83.         return node;  

  84.     }  

  85.   

  86.     public void clear() {  

  87.         first = null;  

  88.         last = null;  

  89.     }  

  90.   

  91.     /** 

  92.      * 删除链表尾部节点 

  93.      *  表示 删除最少使用的缓存对象 

  94.      */  

  95.     private void removeLast() {  

  96.         //链表尾不为空,则将链表尾指向null. 删除连表尾(删除最少使用的缓存对象)  

  97.         if (last != null) {  

  98.             if (last.prev != null)  

  99.                 last.prev.next = null;  

  100.             else  

  101.                 first = null;  

  102.             last = last.prev;  

  103.         }  

  104.     }  

  105.       

  106.     /** 

  107.      * 移动到链表头,表示这个节点是最新使用过的 

  108.      * @param node 

  109.      */  

  110.     private void moveToHead(CacheNode node) {  

  111.         if (node == first)  

  112.             return;  

  113.         if (node.prev != null)  

  114.             node.prev.next = node.next;  

  115.         if (node.next != null)  

  116.             node.next.prev = node.prev;  

  117.         if (last == node)  

  118.             last = node.prev;  

  119.         if (first != null) {  

  120.             node.next = first;  

  121.             first.prev = node;  

  122.         }  

  123.         first = node;  

  124.         node.prev = null;  

  125.         if (last == null)  

  126.             last = first;  

  127.     }  

  128.     private int cacheSize;  

  129.     private Hashtable nodes;//缓存容器  

  130.     private int currentSize;  

  131.     private CacheNode first;//链表头  

  132.     private CacheNode last;//链表尾  

  133. }  




4  性能分析:


    lur 算法 时间复杂度是 o(1)

    

    为啥不是o(n)

    hashtable作为索引 --快速访问

    list链表  ---存储有序数据

    

5 扩展   hashtable+list方式

Q1 用redis数据结构 zset结构如何实现的 请问是用红黑树吗 ?

A:不是



1 hash--快速查找 key-value

2  链表方式 --保证数据顺序


KIPLIST 编码的有序集

当使用 REDIS_ENCODING_SKIPLIST 编码时, 有序集元素由 redis.h/zset 结构来保存:

/* * 有序集 */typedef struct zset {

    // 字典
    dict *dict;

    // 跳跃表
    zskiplist *zsl;} zset;

采用复合结构,

字典维护了name=>score的映射表,

而跳跃表则维护了按score排序的列表. 按name和按score的范围查询都天然支持.




Q2  为什么用能map 代替(hash+list方式) 两个结构表示多麻烦呀


STL的map底层是用红黑树实现的,查找时间复杂度是log(n);
STL的hash_map底层是用hash表存储的,查询时间复杂度是O(1)重复记录 最坏情况o(n)
 

A2:

map在数量大时候缺点

一般应用情况下,我们保存的数据不超过100万份,查找的频繁程度不高情况下使用map性能比较好;

而保存的数据较多时(超过100万),查找频繁时使用hash_map的性能就高于map了


参考

1 http://redisbook.readthedocs.io/en/latest/datatype/sorted_set.html






以上是关于缓存策略之LRU实现及分析的主要内容,如果未能解决你的问题,请参考以下文章

分布式技术专题「分布式缓存专题」针对于缓存淘汰算法之LRU和LFU及FIFO原理分析

LRU缓存替换策略及C#实现

面试题LRU算法及编码实现LRU策略缓存

从源码出发,分析LRU缓存淘汰策略的实现!

双链表实现LRU缓存淘汰策略

myBatis组件之缓存实现及使用