lrucache学习总结(参考leveldb)
-
背景
- 工程中需要经常访问数据库,在并发数比较大的情况下,对数据库的压力比较大。
- 一种解决方案,可以使用缓存。 即在内存里保留最近使用的数据。
- 缓存对数据敏感性要求高的话,每次可以和数据库的版本对比一下。如果要求性不高,则直接使用缓存的数据即可。
- cache的淘汰机制,lru=least recently used, 最近最少使用的数据,优先被淘汰。
- 前人已经仿照leveldb中的lrucache, 封装成适合业务使用的模式,下面学习一下这里具体的实现。
-
实现思路
- 主要的思路, 使用双向链表 + 哈希表即可完成。哈希表记录了key->链表位置。 最新的数据放在链表的头部,最旧的数据放在链表的尾部。 发现内存超过阈值时,将链表尾部的数据淘汰即可。
- 读请求:先读哈希表, 找到链表节点, 将节点数据返回。 将节点数据调整到链表头部。
- 写请求:先读哈希表, 找到链表节点。 如果旧数据存在,清理旧数据。插入新数据,节点数据放到链表头部,并且更新哈希表数据。
-
巧妙的实现
- 写入的数据是指针。出于通用的考虑,Insert进来数据是指针变量(有些场景业务侧拿到的是指针),当数据被淘汰时。 会调用delete来释放相应的内存。
- 引用计数。
- 并发场景下对同一个key的访问,返回的数据是一个指针。 何时释放这个指针是一个需要思考的点。
- 这里使用引用计数,计算了当前多少个并发持有了这个指针, 只有引用计数为0时,才会真正地delete内存。
- 引用计数增加的场景: Lookup成功、Insert成功。
- 引用计数减少的场景: lookup成功之后的业务析构、Insert成功之后的业务析构、Insert成功的时候如果存在旧数据需要清理 、lrucache将当节节点淘汰
- 拆分两个链表。
- 如果只有一个链表,如果链表的尾部数据引用计数>1的话,这个节点是不能被淘汰的。 只能每次从尾部往前查找,直到第一个引用计算=1的数据才能被淘汰。效率较低。
- 于是这里拆成两个链表, used链表+lru链表。 used链表代表正在使用的链表,这里的数据引用计数>1, 这里的数据不可能被淘汰。 当引用计数减少到1的时候, 再放到lru链表,这里的数据可以被淘汰。 随着引用计数的变更, 在两个链表里来回切换。 从lru链表淘汰的时候,再delete清理内存。
- 使用SharedLruCache分桶,减少竞争。
- 单个cache内,对哈希表、两个链表的操作需要加锁,这里对所有的key分桶处理,减少竞争。
-
其它可优化的点。
- CacheItem这个wrapper类, 需要限制一下复制构造函数。 考虑以下的场景:
- Lookup获取cache的数据。
- 对比了db的数据
- 发现数据太旧,使用Insert写入新数据。
- Lookup的wrapper和Insert的wrapper如果使用了相同一个实例,则可能引起内存泄露。
- 容量问题。
- 容量的计算,是由外部带入。 没有计算哈希表、链表占用的内存。会导致运行时,占用的内存比实际高。特别是key比较大,而value比较小的场景。 极端情况如果value的数据为空, 可能会导致key无限多!!!
- 这里可以限制一下参数cap不允许为0, 并且容量在内部上加上key + list的数值。
- 写入参数使用的指针,业务侧需要new出来。 这里如果数据比较小,频繁申请小块内存,使用tcmalloc替代ptmalloc会性能更优些。 (后续学习)
#pragma once
#include <pthread.h>
#include <string.h>
#include <assert.h>
#include <functional>
#include <mutex>
namespace cachespace
{
template<class KEY, class T>
class Node
{
public:
Node()
:pNext(NULL),
pPrev(NULL),
pNex