redis lru實現策略

來源:互聯網
上載者:User

redis lru實現策略
在使用redis作為緩衝的情境下,記憶體淘汰策略決定的redis的記憶體使用量效率。在大部分情境下,我們會採用LRU(Least Recently Used)來作為redis的淘汰策略。本文將由淺入深的介紹redislru策略的具體實現。
首先我們來科普下,什麼是LRU ?(以下來自維基百科)
Discards the least recently used items first. This algorithm requires keeping track of what was used when, which is expensive if one wants to make sure the algorithm always discards the least recently used item. General implementations of this technique require keeping "age bits" for cache-lines and track the "Least Recently Used" cache-linebased on age-bits. In such an implementation, every time a cache-line is used, the age of all other cache-lines changes.
簡而言之,就是每次淘汰最近最少使用的元素。一般的實現,都是採用對儲存在記憶體的元素採用'agebits’來標記該元素從上次訪問到現在為止的時間長度,從而在每次用LRU淘汰時,淘汰這些最長時間未被訪問的元素。

這裡我們先實現一個簡單的LRUCache,以便於後續內容的理解 。(來自leetcod,不過這裡我重新用Python語言實現了) 實現該緩衝滿足如下兩點:
1.get(key) - 如果該元素(總是正數)存在,將該元素移動到lru頭部,並返回該元素的值,否則返回-1。
2.set(key,value) - 設定一個key的值為value(如果該元素存在),並將該元素移動到LRU頭部。否則插入一個key,且值為value。如果在設定前檢查到,該key插入後,會超過cache的容量,則根據LRU策略,刪除最近最少使用的key。
分析
這裡我們採用雙向鏈表來實現元素(k-v索引值對)的儲存,同時採用hash表來儲存相關的key與item的對應關係。這樣,我們既能在O(1)的時間對key進行操作,同時又能利用DoubleLinkedList的添加和刪除節點的便利性。(get/set都能在O(1)內完成)。


具體實現(Python語言)

 
  1. class Node:
  2. key=None
  3. value=None
  4. pre=None
  5. next=None

  6. def __init__(self,key,value):
  7. self.key=key
  8. self.value=value

  9. class LRUCache:
  10. capacity=0
  11. map={} # key is string ,and value is Node object
  12. head=None
  13. end=None

  14. def __init__(self,capacity):
  15. self.capacity=capacity

  16. def get(self,key):
  17. if key in self.map:
  18. node=self.map[key]
  19. self.remove(node)
  20. self.setHead(node)
  21. return node.value
  22. else:
  23. return -1

  24. def getAllKeys(self):
  25. tmpNode=None
  26. if self.head:
  27. tmpNode=self.head
  28. while tmpNode:
  29. print (tmpNode.key,tmpNode.value)
  30. tmpNode=tmpNode.next

  31. def remove(self,n):
  32. if n.pre:
  33. n.pre.next=n.next
  34. else:
  35. self.head=n.next

  36. if n.next:
  37. n.next.pre=n.pre
  38. else:
  39. self.end=n.pre

  40. def setHead(self,n):
  41. n.next=self.head
  42. n.pre=None

  43. if self.head:
  44. self.head.pre=n

  45. self.head=n

  46. if not self.end:
  47. self.end=self.head

  48. def set(self,key,value):
  49. if key in self.map:
  50. oldNode=self.map[key]
  51. oldNode.value=value
  52. self.remove(oldNode)
  53. self.setHead(oldNode)
  54. else:
  55. node=Node(key,value)
  56. if len(self.map) >= self.capacity:
  57. self.map.pop(self.end.key)
  58. self.remove(self.end)
  59. self.setHead(node)
  60. else:
  61. self.setHead(node)

  62. self.map[key]=node


  63. def main():
  64. cache=LRUCache(100)

  65. #d->c->b->a
  66. cache.set('a','1')
  67. cache.set('b','2')
  68. cache.set('c',3)
  69. cache.set('d',4)

  70. #遍曆lru鏈表
  71. cache.getAllKeys()

  72. #修改('a','1') ==> ('a',5),使該節點從LRU尾端移動到開頭.
  73. cache.set('a',5)
  74. #LRU鏈表變為 a->d->c->b

  75. cache.getAllKeys()
  76. #訪問key='c'的節點,是該節點從移動到LRU頭部
  77. cache.get('c')
  78. #LRU鏈表變為 c->a->d->b
  79. cache.getAllKeys()

  80. if __name__ == '__main__':
  81. main()
通過上面簡單的介紹與實現,現在我們基本已經瞭解了什麼是LRU,下面我們來看看LRU演算法在redis 內部的實現細節,以及其會在什麼情況下帶來問題。在redis內部,是通過全域結構體struct redisServer 儲存redis啟動之後相關的資訊,比如:

 
  1. struct redisServer {
  2. pid_t pid; /* Main process pid. */
  3. char *configfile; /* Absolute config file path, or NULL */
  4. …..
  5. unsigned lruclock:LRU_BITS; /* Clock for LRU eviction */
  6. ...
  7. };
redisServer 中包含了redis伺服器啟動之後的基本資料(PID,設定檔路徑,serverCron運行頻率hz等),外部可調用模組資訊,網路資訊,RDB/AOF資訊,日誌資訊,複製資訊等等。 我們看到上述結構體中lruclock:LRU_BITS,其中儲存了伺服器自啟動之後的lru時鐘,該時鐘是全域的lru時鐘。該時鐘100ms(可以通過hz來調整,預設情況hz=10,因此每1000ms/10=100ms執行一次定時任務)更新一次。

接下來我們看看LRU時鐘的具體實現:

 
  1. server.lruclock = getLRUClock();
  2. getLRUClock函數如下:
  3. #define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */
  4. #define LRU_BITS 24
  5. #define LRU_CLOCK_MAX ((1<lru */
  6. /* Return the LRU clock, based on the clock resolution. This is a time
  7. * in a reduced-bits format that can be used to set and check the
  8. * object->lru field of redisObject structures. */

  9. unsigned int getLRUClock(void) {
  10. return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
  11. }
因此lrulock最大能到(2**24-1)/3600/24= 194天,如果超過了這個時間,lrulock重新開始。對於redisserver來說,server.lrulock表示的是一個全域的lrulock,那麼對於每個redisObject都有一個自己的lrulock。這樣每redisObject就可以根據自己的lrulock和全域的server.lrulock比較,來確定是否能夠被淘汰掉。
redis key對應的value的存放對象:

 
  1. typedef struct redisObject {
  2. unsigned type:4;
  3. unsigned encoding:4;
  4. unsigned lru:LRU_BITS; /* LRU time (relative to server.lruclock) or
  5. * LFU data (least significant 8 bits frequency
  6. * and most significant 16 bits decreas time). */
  7. int refcount;
  8. void *ptr;
  9. } robj

那麼什麼時候,lru會被更新呢 ?訪問該key,lru都會被更新,這樣該key就能及時的被移動到lru頭部,從而避免從lru中淘汰。下面是這一部分的實現:

 
  1. /* Low level key lookup API, not actually called directly from commands
  2. * implementations that should instead rely on lookupKeyRead(),
  3. * lookupKeyWrite() and lookupKeyReadWithFlags(). */
  4. robj *lookupKey(redisDb *db, robj *key, int flags) {
  5. dictEntry *de = dictFind(db->dict,key->ptr);
  6. if (de) {
  7. robj *val = dictGetVal(de);

  8. /* Update the access time for the ageing algorithm.
  9. * Don't do it if we have a saving child, as this will trigger
  10. * a copy on write madness. */
  11. if (server.rdb_child_pid == -1 &&
  12. server.aof_child_pid == -1 &&
  13. !(flags & LOOKUP_NOTOUCH))
  14. {
  15. if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
  16. unsigned long ldt = val->lru >> 8;
  17. unsigned long counter = LFULogIncr(val->lru & 255);
  18. val->lru = (ldt << 8) | counter;
  19. } else {
  20. val->lru = LRU_CLOCK();
  21. }
  22. }
  23. return val;
  24. } else {
  25. return NULL;
  26. }
  27. }

接下來,我們在來分析,key的lru淘汰策略如何?,分別有哪幾種:

 
  1. # MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
  2. # is reached. You can select among five behaviors:
  3. #
  4. # volatile-lru -> Evict using approximated LRU among the keys with an expire set. //在設定了到期時間的key中,使用近似的lru淘汰策略
  5. # allkeys-lru -> Evict any key using approximated LRU. //所有的key均使用近似的lru淘汰策略
  6. # volatile-lfu -> Evict using approximated LFU among the keys with an expire set. //在設定了到期時間的key中,使用lfu淘汰策略
  7. # allkeys-lfu -> Evict any key using approximated LFU. //所有的key均使用lfu淘汰策略
  8. # volatile-random -> Remove a random key among the ones with an expire set. //在設定了到期時間的key中,使用隨機淘汰策略
  9. # allkeys-random -> Remove a random key, any key. //所有的key均使用隨機淘汰策略
  10. # volatile-ttl -> Remove the key with the nearest expire time (minor TTL) //使用ttl淘汰策略
  11. # noeviction -> Don't evict anything, just return an error on write operations . //不允許淘汰,在寫操作發生,但記憶體不夠時,將會返回錯誤
  12. #
  13. # LRU means Least Recently Used
  14. # LFU means Least Frequently Used
  15. #
  16. # Both LRU, LFU and volatile-ttl are implemented using approximated
  17. # randomized algorithms.
這裡暫不討論LFU,TTL淘汰演算法和noeviction的情況,僅僅討論lru所有情境下的,淘汰策略具體實現。(LFU和TTL將在下一篇文章中詳細分析)。
LRU淘汰的情境: 1.主動淘汰。 1.1 通過定時任務serverCron週期性清理到期的key。 2.被動淘汰 2.1 每次寫入key時,發現記憶體不夠,調用activeExpireCycle釋放一部分記憶體。 2.2 每次訪問相關的key,如果發現key到期,直接釋放掉該key相關的記憶體。

首先我們來分析 LRU主動淘汰的情境: serverCron每間隔1000/hz ms會調用databasesCron方法來檢測並淘汰到期的key。

   
  1. void databasesCron(void){
  2. /* Expire keys by random sampling. Not required for slaves
  3. * as master will synthesize DELs for us. */
  4. if (server.active_expire_enabled && server.masterhost == NULL)
  5. activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
  6. …..
  7. }
主動淘汰是通過activeExpireCycle 來實現的,這部分的邏輯如下:
1.遍曆至多16個DB 。【由宏CRON_DBS_PER_CALL定義,預設為16】 2.隨機挑選20個帶到期時間的key。【由宏ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP定義,預設20】 3.如果key到期,則將key相關的記憶體釋放,或者放入失效隊列。 4.如果操作時間超過允許的限定時間,至多25ms。(timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100, ,ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC=25,server.hz預設為10), 則此次淘汰操作結束返回,否則進入5。 5.如果該DB下,有超過5個key(ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4=5) 實際失效,則進入2,否則選擇下一個DB,再次進入2。 6.遍曆完成,結束。
流程圖如下
註:(圖中大於等於%5的可以是實際到期的,應改為大於等於%25的key是實際到期的。iteration++是在遍曆20個key的時候,每次加1)。
被動淘汰-記憶體不夠,調用activeExpireCycle釋放 該步驟的實現方式如下:

   
  1. processCommand 函數關於記憶體淘汰策略的邏輯:
  2. /* Handle the maxmemory directive.
  3. *
  4. * First we try to free some memory if possible (if there are volatile
  5. * keys in the dataset). If there are not the only thing we can do
  6. * is returning an error. */
  7. if (server.maxmemory) {
  8. int retval = freeMemoryIfNeeded();
  9. /* freeMemoryIfNeeded may flush slave output buffers. This may result
  10. * into a slave, that may be the active client, to be freed. */
  11. if (server.current_client == NULL) return C_ERR;

  12. /* It was impossible to free enough memory, and the command the client
  13. * is trying to execute is denied during OOM conditions? Error. */
  14. if ((c->cmd->flags & CMD_DENYOOM) && retval == C_ERR) {
  15. flagTransaction(c);
  16. addReply(c, shared.oomerr);
  17. return C_OK;
  18. }
  19. }
每次執行命令前,都會調用freeMemoryIfNeeded來檢查記憶體的情況,並釋放相應的記憶體,如果釋放後,記憶體仍然不夠,直接向請求的用戶端返回OOM。具體的步驟如下:
1.擷取redis server當前已經使用的記憶體mem_reported。 2.如果mem_reported < server.maxmemory ,則返回ok。否則mem_used=mem_reported,進入步驟3。 3.遍曆該redis的所slaves,mem_used減去所有slave佔用的ClientOutputBuffer。 4.如果配置了AOF,mem_used減去AOF佔用的空間。sdslen(server.aof_buf)+aofRewriteBufferSize()。 5.如果mem_used < server.maxmemory,返回ok。否則進入步驟6。 6.如果記憶體策略配置為noeviction,返回錯誤。否則進入7。 7.如果是LRU策略,如果是VOLATILE的LRU,則每次從可失效的資料集中,每次隨機採樣maxmemory_samples(預設為5)個key,從中選取idletime最大的key進行淘汰。 否則,如果是ALLKEYS_LRU則從全域資料中進行採樣,每次隨機採樣maxmemory_samples(預設為5)個key,並從中選擇idletime最大的key進行淘汰。 8.如果釋放記憶體之後,還是超過了server.maxmemory,則繼續淘汰,只到釋放後剩下的記憶體小於server.maxmemory為止。
被動淘汰-每次訪問相關的key,如果發現key到期,直接釋放掉該key相關的記憶體: 每次訪問key,都會調用expireIfNeeded來判斷key是否到期,如果到期,則釋放掉,並返回null,否則返回key的值。

總結 1.redis做為緩衝,經常採用LRU的策略來淘汰資料,所以如果同時到期的資料太多,就會導致redis發起主動式偵測時耗費的時間過長(最大為250ms),從而導致最大應用逾時>= 250ms。

   
  1. timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100
  2. ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC=25
  3. server.hz>=1(預設為10)
  4. timelimit<=250ms
2.記憶體使用量率過高,則會導致記憶體不夠,從而發起被動淘汰策略,從而使應用訪問逾時。 3.合理的調整hz參數,從而控制每次主動淘汰的頻率,從而有效緩解到期的key數量太多帶來的上述逾時問題。










聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.