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),
pNextHash(NULL),
iCharge(0),
bInCache(false),
iRef(0),
iHash(0),
value(NULL),
bPermanent(false)
{
}
~Node()
{
if (value)
delete value;
}
Node* pNext;
Node* pPrev;
Node* pNextHash;
size_t iCharge;
bool bInCache;
uint32_t iRef;
uint32_t iHash;
KEY key;
T* value;
bool bPermanent; //node will never be elimited iff bPermanent == true, can erase permanent node manually
};
template<class KEY, class T>
class HashTable
{
public:
HashTable(uint32_t iInitLen)
: m_iInitLen(iInitLen),
m_iLength(0),
m_iElemCnt(0),
m_pList(NULL)
{
Resize();
}
~HashTable()
{
delete[] m_pList;
}
Node<KEY, T>* Lookup(const KEY& key, uint32_t iHash)
{
return *FindPointer(key, iHash);
}
Node<KEY, T>* Insert(Node<KEY, T>* n)
{
Node<KEY, T>** ptr = FindPointer(n->key, n->iHash);
Node<KEY, T>* oldNode = *ptr;
n->pNextHash = (oldNode == NULL ? NULL : oldNode->pNextHash);
*ptr = n;
if (oldNode == NULL)
{
++m_iElemCnt;
if (m_iElemCnt > m_iLength)
{
Resize();
}
}
return oldNode;
}
Node<KEY, T>* Remove(const KEY& key, uint32_t iHash)
{
Node<KEY, T>** ptr = FindPointer(key, iHash);
Node<KEY, T>* removeNode = *ptr;
if (removeNode != NULL)
{
*ptr = removeNode->pNextHash;
--m_iElemCnt;
}
return removeNode;
}
private:
void Resize()
{
uint32_t iNewLen = m_iInitLen;
while (iNewLen < m_iElemCnt)
iNewLen *= 2;
Node<KEY, T>** pNewList = new Node<KEY, T>*[iNewLen];
memset(pNewList, 0, sizeof(Node<KEY, T>*) * iNewLen);
uint32_t iCnt = 0;
for (uint32_t i = 0; i < m_iLength; i++)
{
Node<KEY, T>* n = m_pList[i];
while (n != NULL)
{
Node<KEY, T>* pNext = n->pNextHash;
uint32_t iHash = n->iHash;
Node<KEY, T>** ptr = &pNewList[ iHash % (iNewLen - 1) ];
n->pNextHash = *ptr;
*ptr = n;
n = pNext;
iCnt++;
}
}
assert(m_iElemCnt == iCnt);
delete[] m_pList;
m_pList = pNewList;
m_iLength = iNewLen;
}
Node<KEY, T>** FindPointer(const KEY& key, uint32_t iHash)
{
Node<KEY, T>** ptr = &m_pList[ iHash % (m_iLength - 1) ];
while (*ptr != NULL && ( (*ptr)->iHash != iHash || !(key == (*ptr)->key )) )
ptr = &(*ptr)->pNextHash;
return ptr;
}
uint32_t m_iInitLen;
uint32_t m_iLength;
uint32_t m_iElemCnt;
Node<KEY, T>** m_pList;
};
template<class KEY, class T>
class LRUCache
{
public:
LRUCache(uint32_t iInitLen)
:m_iCapacity(0),
m_iUsage(0),
m_oTable(iInitLen)
{
//empty circular linked list
m_oLru.pNext = &m_oLru;
m_oLru.pPrev = &m_oLru;
m_oUsed.pNext = &m_oUsed;
m_oUsed.pPrev = &m_oUsed;
m_iNo = 0;
}
~LRUCache()
{
assert(m_oUsed.pNext == &m_oUsed);
for (Node<KEY, T>* e = m_oLru.pNext; e != &m_oLru;)
{
Node<KEY, T>* next = e->pNext;
assert(e->bInCache);
e->bInCache = false;
assert(e->iRef == 1);
Unref(e);
e = next;
}
}
void SetAttr(int iNo, size_t iCapacity)
{
m_iNo = iNo;
m_iCapacity = iCapacity;
}
void ModifyCharge(int iModCharge)
{
std::lock_guard<std::mutex> guard(m_oMutex);
m_iUsage += iModCharge;
//HILogInfo("LRUCache:%d usage:%zu capacity:%zu", m_iNo, m_iUsage, m_iCapacity);
CheckAndElimite();
}
Node<KEY, T>* Insert(const KEY& key, uint32_t iHash, T* value, size_t iCharge, bool bPermanent)
{
std::lock_guard<std::mutex> guard(m_oMutex);
Node<KEY, T>* e = new Node<KEY, T>;
e->key = key;
e->value = value;
e->iCharge = iCharge;
e->iHash = iHash;
e->bInCache = false;
e->bPermanent = bPermanent;
e->iRef = 1; //for the return Node
//if capacity is zero, then cache will NOT effect
if (m_iCapacity > 0)
{
e->iRef++; //for the cache reference
e->bInCache = true;
LRU_Append(&m_oUsed, e);
m_iUsage += iCharge;
//HILogInfo("LRUCache:%d usage:%zu capacity:%zu", m_iNo, m_iUsage, m_iCapacity);
FinishErase(m_oTable.Insert(e));
CheckAndElimite();
}
return e;
}
Node<KEY, T>* Lookup(const KEY& key, uint32_t iHash)
{
std::lock_guard<std::mutex> guard(m_oMutex);
Node<KEY, T>* e = m_oTable.Lookup(key, iHash);
if (e != NULL)
{
Ref(e);
}
return e;
}
void Release(Node<KEY, T>* e)
{
std::lock_guard<std::mutex> guard(m_oMutex);
Unref(e);
CheckAndElimite();
}
void Erase(const KEY& key, uint32_t iHash)
{
std::lock_guard<std::mutex> guard(m_oMutex);
FinishErase(m_oTable.Remove(key, iHash));
}
void Prune()
{
std::lock_guard<std::mutex> guard(m_oMutex);
while (m_oLru.pNext != &m_oLru)
{
Node<KEY, T>* e = m_oLru.pNext;
assert(e->iRef == 1);
FinishErase(m_oTable.Remove(e->key, e->iHash));
}
}
size_t TotalCharge()
{
std::lock_guard<std::mutex> guard(m_oMutex);
return m_iUsage;
}
private:
void CheckAndElimite()
{
while (m_iUsage > m_iCapacity && m_oLru.pNext != &m_oLru)
{
//HILogInfo("LRUCache:%d usage:%zu >capacity:%zu, need elimite", m_iNo, m_iUsage, m_iCapacity);
Node<KEY, T>* old = m_oLru.pNext;
assert(old->iRef == 1);
if (old->bPermanent)
{
LRU_Remove(old);
LRU_Append(&m_oLru, old);
}
else
{
bool erased = FinishErase(m_oTable.Remove(old->key, old->iHash));
assert(erased);
(void)erased; //avoid unused variable when compiled NDEBUG
}
}
}
bool FinishErase(Node<KEY, T>* e)
{
if (e != NULL)
{
assert(e->bInCache);
LRU_Remove(e);
e->bInCache = false;
m_iUsage -= e->iCharge;
//HILogInfo("LRUCache:%d usage:%zu capacity:%zu", m_iNo, m_iUsage, m_iCapacity);
Unref(e);
}
return e != NULL;
}
void Ref(Node<KEY, T>* e)
{
if (e->iRef == 1 && e->bInCache)
{
LRU_Remove(e);
LRU_Append(&m_oUsed, e);
}
e->iRef++;
}
void Unref(Node<KEY, T>* e)
{
assert(e->iRef > 0);
e->iRef--;
if (e->iRef == 0)
{
assert(!e->bInCache);
delete e;
}
else if (e->bInCache && e->iRef == 1)
{
LRU_Remove(e);
LRU_Append(&m_oLru, e);
}
}
void LRU_Remove(Node<KEY, T>* e)
{
e->pNext->pPrev = e->pPrev;
e->pPrev->pNext = e->pNext;
}
void LRU_Append(Node<KEY, T>* pList, Node<KEY, T>* e)
{
e->pNext = pList;
e->pPrev = pList->pPrev;
e->pPrev->pNext = e;
e->pNext->pPrev = e;
}
int m_iNo;
size_t m_iCapacity;
std::mutex m_oMutex;
size_t m_iUsage;
Node<KEY, T> m_oLru;
Node<KEY, T> m_oUsed;
HashTable<KEY, T> m_oTable;
};
template<class KEY, class T, class Hasher = std::hash<KEY> > class CacheItem;
template<class KEY, class T, class Hasher = std::hash<KEY> >
class ShardedLRUCache
{
public:
explicit ShardedLRUCache(int iNumShard, size_t iCapacity, size_t iHashTableInitLen = 4)
:m_iNumShard(iNumShard),
m_iCapacity(iCapacity)
{
m_pCache = reinterpret_cast<LRUCache<KEY, T>* >(new char[m_iNumShard * sizeof(LRUCache<KEY, T>)]);
const size_t iPerShard = m_iCapacity == 0 ? 0 : m_iCapacity / m_iNumShard;
for (int i = 0; i < m_iNumShard; i++)
{
new(&m_pCache[i]) LRUCache<KEY, T>(iHashTableInitLen);
m_pCache[i].SetAttr(i, iPerShard);
}
}
~ShardedLRUCache()
{
delete[] m_pCache;
}
void ModifyCharge(const KEY& key, int iModCharge)
{
const uint32_t iHash = hashFunc(key);
m_pCache[Shard(iHash)].ModifyCharge(iModCharge);
}
void Insert(const KEY& key, T* value, size_t iCharge = 1, bool bPermanent = false)
{
const uint32_t iHash = hashFunc(key);
Node<KEY, T>* pNode = m_pCache[Shard(iHash)].Insert(key, iHash, value, iCharge, bPermanent);
Release(pNode);
}
void Insert(const KEY& key, T* value, CacheItem<KEY, T, Hasher>& wrapper, size_t iCharge = 1, bool bPermanent = false)
{
const uint32_t iHash = hashFunc(key);
Node<KEY, T>* pNode = m_pCache[Shard(iHash)].Insert(key, iHash, value, iCharge, bPermanent);
wrapper.m_pCache = this;
wrapper.m_pNode = pNode;
}
void Lookup(const KEY& key, CacheItem<KEY, T, Hasher>& wrapper)
{
assert(wrapper.m_pNode == NULL);
assert(wrapper.m_pCache == NULL);
const uint32_t iHash = hashFunc(key);
Node<KEY, T>* pNode = m_pCache[Shard(iHash)].Lookup(key, iHash);
wrapper.m_pCache = this;
wrapper.m_pNode = pNode;
//HILogInfo("ShardedLRUCache %s cache", pNode == NULL ? "miss" : "hit");
}
void Release(Node<KEY, T>* pNode)
{
m_pCache[Shard(pNode->iHash)].Release(pNode);
}
void Erase(const KEY& key)
{
const uint32_t iHash = hashFunc(key);
m_pCache[Shard(iHash)].Erase(key, iHash);
}
void Prune()
{
for (int i = 0; i < m_iNumShard; i++)
m_pCache[i].Prune();
}
size_t TotalCharge()
{
size_t iTotal = 0;
for (int i = 0; i < m_iNumShard; i++)
iTotal += m_pCache[i].TotalCharge();
return iTotal;
}
private:
uint32_t hashFunc(const KEY& key)
{
return Hasher{}(key);
}
uint32_t Shard(uint32_t iHash)
{
return iHash % m_iNumShard;
}
int m_iNumShard;
size_t m_iCapacity;
LRUCache<KEY, T>* m_pCache;
};
//ensure to release the record
template<class KEY, class T, class Hasher>
class CacheItem
{
public:
friend class ShardedLRUCache<KEY, T, Hasher>;
CacheItem(): m_pCache(NULL), m_pNode(NULL) {}
void MoveFrom(CacheItem& other)
{
m_pCache = other.m_pCache;
m_pNode = other.m_pNode;
other.m_pCache = NULL;
other.m_pNode = NULL;
}
T* Get()
{
if (m_pNode)
return m_pNode->value;
else
return NULL;
}
T* operator->()
{
return Get();
}
void SetCharge(uint32_t iCharge)
{
if (m_pNode)
{
int iUsageChange = iCharge - m_pNode->iCharge;
m_pCache->ModifyCharge(m_pNode->key, iUsageChange);
m_pNode->iCharge = iCharge;
}
}
~CacheItem()
{
if (m_pNode)
m_pCache->Release(m_pNode);
}
private:
ShardedLRUCache<KEY, T, Hasher>* m_pCache;
Node<KEY, T> *m_pNode;
};
}