lrucache学习总结(参考leveldb)

lrucache学习总结(参考leveldb)

  1. 背景

    • 工程中需要经常访问数据库,在并发数比较大的情况下,对数据库的压力比较大。
    • 一种解决方案,可以使用缓存。 即在内存里保留最近使用的数据。
    • 缓存对数据敏感性要求高的话,每次可以和数据库的版本对比一下。如果要求性不高,则直接使用缓存的数据即可。
    • cache的淘汰机制,lru=least recently used, 最近最少使用的数据,优先被淘汰。
    • 前人已经仿照leveldb中的lrucache, 封装成适合业务使用的模式,下面学习一下这里具体的实现。
  2. 实现思路

    • 主要的思路, 使用双向链表 + 哈希表即可完成。哈希表记录了key->链表位置。 最新的数据放在链表的头部,最旧的数据放在链表的尾部。 发现内存超过阈值时,将链表尾部的数据淘汰即可。
    • 读请求:先读哈希表, 找到链表节点, 将节点数据返回。 将节点数据调整到链表头部。
    • 写请求:先读哈希表, 找到链表节点。 如果旧数据存在,清理旧数据。插入新数据,节点数据放到链表头部,并且更新哈希表数据。
  3. 巧妙的实现

    • 写入的数据是指针。出于通用的考虑,Insert进来数据是指针变量(有些场景业务侧拿到的是指针),当数据被淘汰时。 会调用delete来释放相应的内存。
    • 引用计数。
      • 并发场景下对同一个key的访问,返回的数据是一个指针。 何时释放这个指针是一个需要思考的点。
      • 这里使用引用计数,计算了当前多少个并发持有了这个指针, 只有引用计数为0时,才会真正地delete内存。
      • 引用计数增加的场景: Lookup成功、Insert成功。
      • 引用计数减少的场景: lookup成功之后的业务析构、Insert成功之后的业务析构、Insert成功的时候如果存在旧数据需要清理 、lrucache将当节节点淘汰
    • 拆分两个链表。
      • 如果只有一个链表,如果链表的尾部数据引用计数>1的话,这个节点是不能被淘汰的。 只能每次从尾部往前查找,直到第一个引用计算=1的数据才能被淘汰。效率较低。
      • 于是这里拆成两个链表, used链表+lru链表。 used链表代表正在使用的链表,这里的数据引用计数>1, 这里的数据不可能被淘汰。 当引用计数减少到1的时候, 再放到lru链表,这里的数据可以被淘汰。 随着引用计数的变更, 在两个链表里来回切换。 从lru链表淘汰的时候,再delete清理内存。
    • 使用SharedLruCache分桶,减少竞争。
      • 单个cache内,对哈希表、两个链表的操作需要加锁,这里对所有的key分桶处理,减少竞争。
  4. 其它可优化的点。

    • 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;
};

}

猜你喜欢

转载自blog.csdn.net/a576323437/article/details/88609962
今日推荐