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