緩存算法之LRU與LFU


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM