在阅读shadowsocks的代码过程中,发现ss的lrucache
实现不是很友好。想到以前看过openresty的lru-cache模块https://github.com/openresty/lua-resty-lrucache
,春哥代码写的很好,所以此文进行一番分析。
“LRU cache的目的”
做一个东西一定是有它的目的的,不能为了炫技术而去做一点东西。首先,为什么做缓存,答案是节省时间。为了实现一个缓存,你可能会这样做:
|
|
但是久而久之,如果key足够多,占用的内存会越来越大。你会想着删除一点key,但是这里遇到问题了,删哪些部分呢?
于是一般人都会想,很久不用的key就可以删掉了,最近使用过的key一定是要保留的。这就是lrucache的想法了:key个数可以定义最多数目,超过数目了,删除最老的数据,保留最新的。
“实现原理”
为了实现LRUCache,一定要有一个cache[key]=value
这种kv的存储结构,但是怎么维护顺序呢?resty采用的是双向链表维护顺序。
队列和节点
lrucache有两个节点队列,一个是free_queue
,另一个是cache_queue
。set操作为从free_queue
中取出节点放到cache队列中;get操作为从cache队列中取node。
因为lua和c语言可以借助ffi很方便地互相嵌入,节点的定义是使用c代码定义的:
|
|
一开始初始化queue。
|
|
set操作
如果free_queue
是有node的,set一个key的步骤如下:
从free队列中取一个node
把node和key加入到node2key和key2node中
|
|
- 把node从free队列中删除,
queue_remove(node)
,删除队列node代码如下:
|
|
- 把node加入到cache队列中,
queue_insert_head(self.cache_queue, node)
|
|
如果free_queue
没有空闲的node了,set一个key的步骤如下:
与上面第一步的”从free队列中取一个node”不同,这里取node的方法为从cache队列的最后摘一个node下来。
|
|
这里说下queue_last
函数O(1)的实现:
|
|
除了去node不一致之外,其他步骤与free queue有空闲node是一样的。
get操作
和set相比,get的操作比较简单。如果有key存在,则把node移到cache队列的最前面。
|
|
delete操作
如果删除某一个key后,需要回收cache_queue
中的node到free_queue
中。
|
|
队列操作
lru-cache中使用链表操作,remove, insert_head
等,耗时都是O(1)。
|
|