什么是LRU?
LRU是Least Recently Used的缩写,即最近最少使用。它的主要思想就是,将最近最少使用的数据,置换出容器内,一般是链表。
如果实现LRU?
一般实现的方法,一个映射加一个链表。
映射:是为了对应的键快速找到List中对应的值,一般是保存值的指针,对于STL,可以保存List的迭代器。
列表:自然就是保存实际值的数据结构, 这样方便将最近访问的数据,放到列表头,如果数据超出指定大小,则将列表尾置换出去。
整个结构如下图:
如何发现热数据?
回答这个问题前,首先需要知道什么是冷数据,怎样界定冷数据。
如果同一条数据,两次访问之间请求的时间大于数据缓存的超时时间,那则可以认为是冷数据。否则,则可以认为是热数据,只是热度不一样。
如果无论什么数据,都将其加入到LRU中,对于热数据,这是没有问题的。但是如果是冷数据,这就有问题了;对于冷数据,是不会命中缓存的,或者说是,命中缓存的概率极低。所以对于这种数据,不仅不能缓解数据库的压力,而且会消耗掉机器资源,加大了延迟。而且实际的应用中,往往热数据只占少数。
这就引申回到这个问题,如何发现热数据?
下面说一个常用的方法,令牌桶。我们知道,令牌桶,一般是解决限流的问题,但是在这里如果应用到发现热数据的呢?
原理就是,如果数据的访问单位时间访问的次数,触发了令牌桶的限制,则表示这是热数据,当然令牌桶的容量可以配置,单位时间内配置的容量越小,越容易触发限制。
如何不降低命中率的情况下,并行访问?
现阶段,为了增加程序的并行并发效率,一般使用线程来实现。如果使用了线程,访问共同的资源,一般都需要加锁。当然在所有的线程中分配相同的资源,这样就不需要竞争,自然就不需要加锁。但是同时就降低了命中率。很明显,一个线程访问的数据是热数据,但是将访问分摊到N个线程,就不一定了。同时N个线程,在缓存数据超时时,需要从数据库加载N次数据。
所以在不降低命中率,只有通过加锁来访问LRU的数据结构。但是如果减少资源竞争呢?答案就是减少锁的粒度;减少锁的粒度,可以通过分桶的方式。将不同的数据哈希到不同的桶;这样,只有相同的桶会发生资源竞争,不同的桶则可以并行访问。同时,桶的数量可以控制。
核心代码
#ifndef LRU_H
#define LRU_H
#include <mutex>
#include <list>
#include <map>
#include <algorithm>
class TokenBucket
{
public:
TokenBucket(long cap)
: m_lastTime(0)
, m_currTocken(cap)
, m_cap(cap)
{
}
~TokenBucket() {
}
//为了动态可变,需要从外部传入
/*
例如:rate:1000 cap:100
每秒可以取100个令牌
*/
bool getToken(long rate, long timeNow, int num = 1)
{
long inc = long((timeNow - m_lastTime) * rate) / 1000;
std::lock_guard<std::mutex> g(m_mutex);
m_currTocken = std::min(m_currTocken + inc, m_cap);
if (num > m_currTocken) {
return false;
}
m_lastTime = timeNow;
m_currTocken -= num;
return true;
}
private:
long m_lastTime;
long m_currTocken;
long m_cap;
std::mutex m_mutex;
};
template <class Key, class Value>
class LRUBucket
{
public:
LRUBucket(long cap)
: m_token(cap)
{
}
~LRUBucket()
{
}
bool get(const Key &k, Value &v, long timeNow, long lastTime, long rate, int num = 1)
{
if (m_token.getToken(rate, timeNow, num))
{
return false;
}
std::lock_guard<std::mutex> g(m_mutex);
auto rItr = m_listInfo.rbegin();
while (rItr != m_listInfo.rend())
{
if (rItr->time >= lastTime)
{
break;
}
m_mInfo.erase(rItr->key);
rItr = typename std::list<LRUValue>::reverse_iterator(m_listInfo.erase((++rItr).base()));
}
auto itr = m_mInfo.find(k);
if (itr != m_mInfo.end())
{
v = itr->second->value;
return true;
}
return false;
}
void put(const Key &k, Value &v, long timeNow)
{
LRUValue lv;
lv.key = k;
lv.value = v;
lv.time = timeNow;
std::lock_guard<std::mutex> g(m_mutex);
m_listInfo.emplace_front(lv);
m_mInfo[k] = m_listInfo.begin();
}
size_t size()
{
return m_mInfo.size();
}
private:
struct LRUValue
{
Key key;
Value value;
long time;
};
std::mutex m_mutex;
std::list<LRUValue> m_listInfo;
std::map<Key, typename std::list<LRUValue>::iterator > m_mInfo;
TokenBucket m_token;
};
// 如果传入的值是指针,目前暂时不支持回收
template <class Key, class Value>
class LocalCacheLocker
{
public:
LocalCacheLocker(size_t size, long tokenCap)
: m_tokenCap(tokenCap)
{
resize(size);
}
virtual ~LocalCacheLocker()
{
for (size_t i = 0; i < m_vecBucket.size(); ++i)
{
delete m_vecBucket[i];
}
m_vecBucket.clear();
}
bool get(const Key &k, Value&v, long timeNow, long lastTime, long rate, int num = 1)
{
return m_vecBucket[m_hashFunc(k) % m_vecBucket.size()]->get(k, v, timeNow, lastTime, rate, num);
}
void put(const Key &k, Value&v, long timeNow)
{
m_vecBucket[m_hashFunc(k) % m_vecBucket.size()]->put(k, v, timeNow);
}
private:
void resize(size_t size)
{
if (size < 1) size = 1;
if (size > m_vecBucket.size())
{
size_t num = size - m_vecBucket.size();
for (size_t i = 0; i < num; ++i)
{
m_vecBucket.emplace_back(new LRUBucket<Key, Value>(m_tokenCap));
}
}
else
{
size_t num = m_vecBucket.size() - size;
for (size_t i = 0; i < num; ++i)
{
delete m_vecBucket.back();
m_vecBucket.pop_back();
}
}
}
private:
std::vector<LRUBucket<Key, Value> *> m_vecBucket;
long m_tokenCap;
std::hash<Key> m_hashFunc;
};
#endif
测试代码
#ifndef TEST_LRU_H
#define TEST_LRU_H
#include <algorithm>
#include <chrono>
#include <iostream>
#include <thread>
#include "lru.h"
long getNowMs()
{
std::chrono::system_clock::duration d = std::chrono::system_clock::now().time_since_epoch();
std::chrono::milliseconds mil = std::chrono::duration_cast<std::chrono::milliseconds>(d);
return mil.count();
// cout << min.count() << "分钟" << endl;
// cout << sec.count() << "秒" << endl;
// cout << mil.count() << "毫秒" << endl;
// cout << mic.count() << "微妙" << endl;
// cout << nan.count() << "纳秒" << endl;
}
struct Context
{
Context():lru(10, 2){}
LocalCacheLocker<int, int> lru;
};
void *Update(void * arg)
{
Context *c = (Context *)arg;
int v = 0;
int hitTimes = 0;
for (int i = 0; i < 100; ++i)
{
usleep(1000);
auto nowMs = getNowMs();
if (!c->lru.get(123, v, nowMs, nowMs - 1000, 100))
{
c->lru.put(123, i, nowMs);
}
else
{
hitTimes++;
}
}
std::cout << hitTimes << std::endl;
return NULL;
}
int main()
{
Context c;
std::thread *t[4];
for (int i = 0; i < 4; ++i)
{
t[i] = new std::thread(::Update, &c);
}
for (int i = 0; i < 4; ++i)
{
t[i]->join();
}
return 0;
}
#endif