缓存算法之LRU与LFU

时间:2021-01-27 16:52:56

1. LRU算法
1.1 背景
      目前尽量由于摩尔定律,但是在存储硬件方面始终存在着差异,并且这种差异是不在同一数量级别的区别,例如在容量方面,内存<<外存;而在硬件成本与访问效率方面,内存>>外存。而目前互联网服务平台存在的特点:a. 读多写少,快速ms级响应,因此需要把数据搁在内存上;b. 数据规模巨大,长尾效应,由于数据规模巨大,只能把全量数据搁在外存上。正是由于服务场景需求与存储硬件特征之间的本身矛盾,缓存及相应的淘汰算法由此产生了:

      一个在线服务平台其读取数据的过程:总是优先去离CPU最近的地方内存中读取数据,当有限的内存容量空间读取命中为空被击穿时,则会去外存的数据库或文件系统中读取;而当有限的缓存空间里“人满为患”时,而又有新的热点成员需要加入时,自然需要一定的淘汰机制。本能的基础淘汰算法:最后访问时间最久的成员最先会被淘汰(LRU)。

1.2 基本原理 

    由于队列具有先进先出的操作特点,所以通常用队列实现LRU,按最后访问的时间先进先出。

   a.  利用队列类型对象,记录最近操作的元素,总是放在队首,这样最久未操作的元素自然被相对移动到队尾;同时,当元素总数达到上限值时,优先移除与淘汰队尾元素。
   b. 利用HashMap辅助对象,快速检索队列中存在的元素。
1.3 操作
  a. 写入操作: 新建一个元素,把元素插入队列首,当元素总和达到上限值时,同时删除队尾元素。
  b. 读取操作:利用map快速检索,队列相关联的元素。
1.4 实现源码
  a. 自定义链表方式

  1 // A simple LRU cache written in C++
  2 // Hash map + doubly linked list
  3 #include <iostream>
  4 #include <vector>
  5 #include <ext/hash_map>
  6 using namespace std;
  7 using namespace __gnu_cxx;
  8 
  9 template <class K, class T>
 10 struct Node{
 11   K key;
 12   T data;
 13   Node *prev, *next;
 14 };
 15 
 16 template <class K, class T>
 17 class LRUCache{
 18  public:
 19   LRUCache(size_t size){
 20     entries_ = new Node<K,T>[size];
 21     for(int i=0; i<size; ++i)// 存储可用结点的地址
 22       free_entries_.push_back(entries_+i);
 23     head_ = new Node<K,T>;
 24     tail_ = new Node<K,T>;
 25     head_->prev = NULL;
 26     head_->next = tail_;
 27     tail_->prev = head_;
 28     tail_->next = NULL;
 29   }
 30   ~LRUCache(){
 31     delete head_;
 32     delete tail_;
 33     delete[] entries_;
 34   }
 35   void Put(K key, T data){
 36     Node<K,T> *node = hashmap_[key];
 37     if(node){ // node exists
 38       detach(node);
 39       node->data = data;
 40       attach(node);
 41     }
 42     else
 43     {
 44       if(free_entries_.empty())
 45       {// 可用结点为空,即cache已满
 46         node = tail_->prev;
 47         detach(node);
 48         hashmap_.erase(node->key);
 49       }
 50       else{
 51         node = free_entries_.back();
 52         free_entries_.pop_back();
 53       }
 54       node->key = key;
 55       node->data = data;
 56       hashmap_[key] = node;
 57       attach(node);
 58     }
 59   }
 60   
 61   T Get(K key){
 62     Node<K,T> *node = hashmap_[key];
 63     if(node){
 64       detach(node);
 65       attach(node);
 66       return node->data;
 67     }
 68     else{// 如果cache中没有,返回T的默认值。与hashmap行为一致
 69       return T();
 70     }
 71   }
 72  private:
 73   // 分离结点
 74   void detach(Node<K,T>* node){
 75     node->prev->next = node->next;
 76     node->next->prev = node->prev;
 77   }
 78   // 将结点插入头部
 79   void attach(Node<K,T>* node){
 80     node->prev = head_;
 81     node->next = head_->next;
 82     head_->next = node;
 83     node->next->prev = node;
 84   }
 85  private:
 86   hash_map<K, Node<K,T>* > hashmap_;
 87   vector<Node<K,T>* > free_entries_; // 存储可用结点的地址
 88   Node<K,T> *head_, *tail_;
 89   Node<K,T> *entries_; // 双向链表中的结点
 90 };
 91 
 92 int main(){
 93   hash_map<int, int> map;
 94   map[9]= 999;
 95   cout<<map[9]<<endl;
 96   cout<<map[10]<<endl;
 97   LRUCache<int, string> lru_cache(100);
 98   lru_cache.Put(1, "one");
 99   cout<<lru_cache.Get(1)<<endl;
100   if(lru_cache.Get(2) == "")
101     lru_cache.Put(2, "two");
102   cout<<lru_cache.Get(2);
103   return 0;
104 }

  b. 采用stl::list类型实现方式

  1 #include <iostream>
  2 #include <list>
  3 #include <ext/hash_map>
  4 #include <stdint.h>
  5 #include <string>
  6 
  7 template<class T1, class T2>
  8 class Lru
  9 {
 10  public:
 11   typedef std::list<std::pair<T1, T2> > List;
 12   typedef typename List::iterator iterator;
 13   typedef __gnu_cxx::hash_map<T1, iterator> Map;
 14 
 15   Lru()
 16   {
 17     size_ = 1000;
 18     resize(size_);
 19   }
 20 
 21   ~Lru()
 22   {
 23     clear();
 24   }
 25 
 26   void resize(int32_t size)
 27   {
 28     if (size > 0)
 29     {
 30       size_ = size;
 31     }
 32   }
 33 
 34   T2* find(const T1& first)
 35   {
 36     typename Map::iterator i = index_.find(first);
 37 
 38     if (i == index_.end())
 39     {
 40       return NULL;
 41     }
 42     else
 43     {
 44       typename List::iterator n = i->second;
 45       list_.splice(list_.begin(), list_, n);
 46       return &(list_.front().second);
 47     }
 48   }
 49 
 50   void remove(const T1& first)
 51   {
 52     typename Map::iterator i = index_.find(first);
 53     if (i != index_.end())
 54     {
 55       typename List::iterator n = i->second;
 56       list_.erase(n);
 57       index_.erase(i);
 58     }
 59   }
 60 
 61   void insert(const T1& first, const T2& second)
 62   {
 63     typename Map::iterator i = index_.find(first);
 64     if (i != index_.end())
 65     { // found
 66       typename List::iterator n = i->second;
 67       list_.splice(list_.begin(), list_, n);
 68       index_.erase(n->first);
 69       n->first = first;
 70       n->second = second;
 71       index_[first] = n;
 72     }
 73     else if (size() >= size_ )
 74     { // erase the last element
 75       typename List::iterator n = list_.end();
 76       --n; // the last element
 77       list_.splice(list_.begin(), list_, n);
 78       index_.erase(n->first);
 79       n->first = first;
 80       n->second = second;
 81       index_[first] = n;
 82     }
 83     else
 84     {
 85       list_.push_front(std::make_pair(first, second));
 86       typename List::iterator n = list_.begin();
 87       index_[first] = n;
 88     }
 89   }
 90 
 91   /// Random access to items
 92   iterator begin()
 93   {
 94     return list_.begin();
 95   }
 96 
 97   iterator end()
 98   {
 99     return list_.end();
100   }
101 
102   int size()
103   {
104     return index_.size();
105   }
106 
107   // Clear cache
108   void clear()
109   {
110     index_.clear();
111     list_.clear();
112   }
113 
114  private:
115   int32_t size_;
116   List list_;
117   Map index_;
118 };
119 
120 
121 
122 int main(void)
123 {
124   std::cout << "hello world " << std::endl;
125   tfs::Lru<uint32_t, std::string> name_cache;
126   name_cache.insert(1, "one");
127   name_cache.insert(2, "two");
128 
129   std::string* value = name_cache.find(1);
130   const char* v = value->c_str();
131   std::cout << "result of the key 1 is: " << *name_cache.find(1) << std::endl;
132       
133   return 0;
134 }

2. LRFU缓存算法 

2.1 前言
      缓存算法有许多种,各种缓存算法的核心区别在于它们的淘汰机制。通常的淘汰机制:所有元素按某种量化的重要程度进行排序,分值最低者则会被优先淘汰。而重要程度的量化指标又通常可以参考了两个维度:最后被访问的时间和最近被访问的频率次数。依次例如如下:

    1. LRU(Least Recently Used ),总是淘汰最后访问时间最久的元素。

       这种算法存在着问题:可能由于一次冷数据的批量查询而误淘汰大量热点的数据。

    2. LFU(Least Frequently Used ),总是淘汰最近访问频率最小的元素。

       这种算法也存在明显的问题:  a. 如果频率时间度量是1小时,则平均一天每个小时内的访问频率1000的热点数据可能会被2个小时的一段时间内的访问频率是1001的数据剔除掉;b. 最近新加入的数据总会易于被剔除掉,由于其起始的频率值低。本质上其“重要性”指标访问频率是指在多长的时间维度进行衡量?其难以标定,所以在业界很少单一直接使用。也由于两种算法的各自特点及缺点,所以通常在生产线上会根据业务场景联合LRU与LFU一起使用,称之为LRFU。

2.2 实现机制
       正是由于LRU与LFU算法各具特点,所以在行业生产线上通常会联合两者使用。我们可以在LRU的实现基础上稍作衍生,能实现LRFU缓存机制,即可以采用队列分级的思想,例如oracle利用两个队列维护访问的数据元素,按被访问的频率的维度把元素分别搁在热端与冷端队列;而在同一个队列内,最后访问时间越久的元素会越被排在队列尾。

   
参考
  1.   https://en.wikipedia.org/wiki/Least_frequently_used
  2.   http://code.taobao.org/p/tfs/src/
  3.   http://www.hawstein.com/posts/lru-cache-impl.html