天天看點

Redis的緩存政策和主鍵失效機制

作為緩存系統都要定期清理無效資料,就需要一個主鍵失效和淘汰政策。

在redis當中,有生存期的key被稱為volatile,

在建立緩存時,要為給定的key設定生存期,當key過期的時候(生存期為0),它可能會被删除。

(1)影響生存時間的一些操作

生存時間可以通過使用 del 指令來删除整個 key 來移除,或者被 set 和 getset 指令覆寫原來的資料,

也就是說,修改key對應的value和使用另外相同的key和value來覆寫以後,目前資料的生存時間不同。

比如說,對一個 key 執行incr指令,對一個清單進行lpush指令,或者對一個哈希表執行hset指令,這類操作都不會修改 key 本身的生存時間。

另一方面,如果使用rename對一個 key 進行改名,那麼改名後的 key 的生存時間和改名前一樣。

rename指令的另一種可能是,嘗試将一個帶生存時間的 key 改名成另一個帶生存時間的 another_key ,這時舊的 another_key (以及它的生存時間)會被删除,然後舊的 key 會改名為 another_key ,是以,新的 another_key 的生存時間也和原本的 key 一樣。

使用persist指令可以在不删除 key 的情況下,移除 key 的生存時間,讓 key 重新成為一個persistent key 。

(2)如何更新生存時間

可以對一個已經帶有生存時間的 key 執行expire指令,新指定的生存時間會取代舊的生存時間。

過期時間的精度已經被控制在1ms之内,主鍵失效的時間複雜度是o(1),

expire和ttl指令搭配使用,ttl可以檢視key的目前生存時間

設定成功傳回 1;當 key 不存在或者不能為 key 設定生存時間時,傳回 0 。

在 redis 中,允許使用者設定最大使用記憶體大小

1

<code>server.maxmemory</code>

預設為0,沒有指定最大緩存,如果有新的資料添加,超過最大記憶體,則會使redis崩潰,是以一定要設定。

redis 記憶體資料集大小上升到一定大小的時候,就會實行資料淘汰政策。

redis 提供 6種資料淘汰政策:

volatile-lru:從已設定過期時間的資料集(server.db[i].expires)中挑選最近最少使用的資料淘汰

volatile-ttl:從已設定過期時間的資料集(server.db[i].expires)中挑選将要過期的資料淘汰

volatile-random:從已設定過期時間的資料集(server.db[i].expires)中任意選擇資料淘汰

allkeys-lru:從資料集(server.db[i].dict)中挑選最近最少使用的資料淘汰

allkeys-random:從資料集(server.db[i].dict)中任意選擇資料淘汰

no-enviction(驅逐):禁止驅逐資料

注意這裡的6種機制,volatile和allkeys規定了是對已設定過期時間的資料集淘汰資料還是從全部資料集淘汰資料,

後面的lru、ttl以及random是三種不同的淘汰政策,再加上一種no-enviction永不回收的政策。

使用政策規則:

(1)如果資料呈現幂律分布,也就是一部分資料通路頻率高,一部分資料通路頻率低,則使用allkeys-lru。

(2)如果資料呈現平等分布,也就是所有的資料通路頻率都相同,則使用allkeys-random。

三種資料淘汰政策:

ttl和random比較容易了解,實作也會比較簡單。主要是lru最近最少使用淘汰政策,設計上會對key 按失效時間排序,然後取最先失效的key進行淘汰。

redis 删除失效主鍵的方法主要有兩種:

消極方法(passive way),在主鍵被通路時如果發現它已經失效,那麼就删除它

積極方法(active way),周期性地從設定了失效時間的主鍵中選擇一部分失效的主鍵删除

主鍵具體的失效時間全部都維護在expires這個字典表中。

2

3

4

5

6

7

8

<code>typedef struct redisdb {</code>

<code>    </code><code>dict *dict; </code><code>//key-value</code>

<code>    </code><code>dict *expires;  </code><code>//維護過期key</code>

<code>    </code><code>dict *blocking_keys;</code>

<code>    </code><code>dict *ready_keys;</code>

<code>    </code><code>dict *watched_keys;</code>

<code>    </code><code>int</code> <code>id;</code>

<code>} redisdb;</code>

(1)passive way 消極方法

在passive way 中, redis在實作get、mget、hget、lrange等所有涉及到讀取資料的指令時都會調用 expireifneeded,它存在的意義就是在讀取資料之前先檢查一下它有沒有失效,如果失效了就删除它。

expireifneeded函數中調用的另外一個函數propagateexpire,這個函數用來在正式删除失效主鍵之前廣播這個主鍵已經失效的資訊,這個資訊會傳播到兩個目的地:

一個是發送到aof檔案,将删除失效主鍵的這一操作以del key的标準指令格式記錄下來;

另一個就是發送到目前redis伺服器的所有slave,同樣将删除失效主鍵的這一操作以del key的标準指令格式告知這些slave删除各自的失效主鍵。從中我們可以知道,所有作為slave來運作的redis伺服器并不需要通過消極方法來删除失效主鍵,它們隻需要執行master的删除指令即可。

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

<code>int</code> <code>expireifneeded(redisdb *db, robj *key) {</code>

<code>   </code><code>// 擷取主鍵的失效時間</code>

<code>    </code><code>long</code> <code>long</code> <code>when = getexpire(db,key);</code>

<code>    </code><code>//假如失效時間為負數,說明該主鍵未設定失效時間(失效時間預設為-1),直接傳回0</code>

<code>    </code><code>if</code> <code>(when &lt; 0) </code><code>return</code> <code>0;</code>

<code>   </code><code>// 假如redis伺服器正在從rdb檔案中加載資料,暫時不進行失效主鍵的删除,直接傳回0</code>

<code>    </code><code>if</code> <code>(server.loading) </code><code>return</code> <code>0;</code>

<code>   </code><code>// 假如目前的redis伺服器是作為slave運作的,那麼不進行失效主鍵的删除,因為slave</code>

<code>  </code><code>//  上失效主鍵的删除是由master來控制的,但是這裡會将主鍵的失效時間與目前時間進行</code>

<code>   </code><code>// 一下對比,以告知調用者指定的主鍵是否已經失效了</code>

<code>    </code><code>if</code> <code>(server.masterhost != null) {</code>

<code>        </code><code>return</code> <code>mstime() &gt; when;</code>

<code>    </code><code>}</code>

<code>    </code><code>//如果以上條件都不滿足,就将主鍵的失效時間與目前時間進行對比,如果發現指定的主鍵</code>

<code>   </code><code>// 還未失效就直接傳回0</code>

<code>    </code><code>if</code> <code>(mstime() &lt;= when) </code><code>return</code> <code>0;</code>

<code>   </code><code>// 如果發現主鍵确實已經失效了,那麼首先更新關于失效主鍵的統計個數,然後将該主鍵失</code>

<code>   </code><code>// 效的資訊進行廣播,最後将該主鍵從資料庫中删除</code>

<code>    </code><code>server.stat_expiredkeys++;</code>

<code>    </code><code>propagateexpire(db,key);</code>

<code>    </code><code>return</code> <code>dbdelete(db,key);</code>

<code>}</code>

<code>void</code> <code>propagateexpire(redisdb *db, robj *key) {</code>

<code>    </code><code>robj *argv[2];</code>

<code>   </code><code>// shared.del是在redis伺服器啟動之初就已經初始化好的一個常用redis對象,即del指令</code>

<code>    </code><code>argv[0] = shared.del;</code>

<code>    </code><code>argv[1] = key;</code>

<code>    </code><code>incrrefcount(argv[0]);</code>

<code>    </code><code>incrrefcount(argv[1]);</code>

<code>  </code><code>//  檢查redis伺服器是否開啟了aof,如果開啟了就為失效主鍵記錄一條del日志</code>

<code>    </code><code>if</code> <code>(server.aof_state != redis_aof_off)</code>

<code>        </code><code>feedappendonlyfile(server.delcommand,db-&gt;id,argv,2);</code>

<code>    </code><code>//檢查redis伺服器是否擁有slave,如果是就向所有slave發送del失效主鍵的指令,這就是</code>

<code>   </code><code>// 上面expireifneeded函數中發現自己是slave時無需主動删除失效主鍵的原因了,因為它</code>

<code>  </code><code>//  隻需聽從master發送過來的指令就ok了</code>

<code>    </code><code>if</code> <code>(listlength(server.slaves))</code>

<code>        </code><code>replicationfeedslaves(server.slaves,db-&gt;id,argv,2);</code>

<code>    </code><code>decrrefcount(argv[0]);</code>

<code>    </code><code>decrrefcount(argv[1]);</code>

(2)active way 積極方法

消極方法的缺點是,如果key 遲遲不被通路,就會占用很多記憶體空間,是以就出現了積極的方式(active way),

此方法利用了redis的時間事件,即每隔一段時間就中斷一下完成一些指定操作,其中就包括檢查并删除失效主鍵。

a.時間事件

建立時間事件, 回調函數就是servercron,它在redis伺服器啟動時建立,每秒的執行次數由宏定義redis_default_hz來指定,預設每秒鐘執行10次。

<code>//該代碼在redis.c檔案的initserver函數中。實際上,servercron這個回調函數不僅要進行失效主鍵的檢查與删除,還要進行統計資訊的更新、用戶端連接配接逾時的控制、bgsave和aof的觸發等等,這裡我們僅關注删除失效主鍵的實作,也就是函數activeexpirecycle。</code>

<code>if</code><code>(aecreatetimeevent(server.el, 1, servercron, null, null) == ae_err) {</code>

<code>        </code><code>redispanic(</code><code>"create time event failed"</code><code>);</code>

<code>        </code><code>exit</code><code>(1);</code>

b.使用activeexpirecycle 清除失效key

其實作原理是從redis中每個資料庫的expires字典表中,随機抽樣redis_expirelookups_per_cron(預設值為10)個設定了失效時間的主鍵,檢查它們是否已經失效并删除掉失效的主鍵,如果失效主鍵個數占本次抽樣個數的比例超過25%,它會繼續進行下一輪的随機抽樣和删除,直到剛才的比例低于25%才停止對目前資料庫的處理,轉向下一個資料庫。

注意,activeexpirecycle函數不會試圖一次性處理redis中的所有資料庫,而是最多隻處理redis_dbcron_dbs_per_call(預設值為16),此外activeexpirecycle函數還有處理時間上的限制,不是想執行多久就執行多久,凡此種種都隻有一個目的,那就是避免失效主鍵删除占用過多的cpu資源。

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

<code>void</code> <code>activeexpirecycle(</code><code>void</code><code>) {</code>

<code>    </code><code>/*因為每次調用activeexpirecycle函數不會一次性檢查所有redis資料庫,是以需要記錄下</code>

<code>        </code><code>每次函數調用處理的最後一個redis資料庫的編号,這樣下次調用activeexpirecycle函數</code>

<code>        </code><code>還可以從這個資料庫開始繼續處理,這就是current_db被聲明為static的原因,而另外一</code>

<code>        </code><code>個變量timelimit_exit是為了記錄上一次調用activeexpirecycle函數的執行時間是否達</code>

<code>        </code><code>到時間限制了,是以也需要聲明為static</code>

<code>    </code><code>*/</code>

<code>    </code><code>static</code> <code>unsigned </code><code>int</code> <code>current_db = 0;</code>

<code>    </code><code>static</code> <code>int</code> <code>timelimit_exit = 0;</code>

<code>    </code><code>unsigned </code><code>int</code> <code>j, iteration = 0;</code>

<code>    </code><code>/**</code>

<code>        </code><code>每次調用activeexpirecycle函數處理的redis資料庫個數為redis_dbcron_dbs_per_call</code>

<code>        </code><code>unsigned int dbs_per_call = redis_dbcron_dbs_per_call;</code>

<code>        </code><code>long long start = ustime(), timelimit;</code>

<code>        </code><code>如果目前redis伺服器中的資料庫個數小于redis_dbcron_dbs_per_call,則處理全部資料庫,</code>

<code>        </code><code>如果上一次調用activeexpirecycle函數的執行時間達到了時間限制,說明失效主鍵較多,也</code>

<code>        </code><code>會選擇處理全部資料庫</code>

<code>    </code><code>if</code> <code>(dbs_per_call &gt; server.dbnum || timelimit_exit)</code>

<code>        </code><code>dbs_per_call = server.dbnum;</code>

<code>    </code><code>/*</code>

<code>        </code><code>執行activeexpirecycle函數的最長時間(以微秒計),其中redis_expirelookups_time_perc</code>

<code>        </code><code>是機關時間内能夠配置設定給activeexpirecycle函數執行的cpu時間比例,預設值為25,server.hz</code>

<code>        </code><code>即為一秒内activeexpirecycle的調用次數,是以這個計算公式更明白的寫法應該是這樣的,即</code>

<code>            </code><code>(1000000 * (redis_expirelookups_time_perc / 100)) / server.hz</code>

<code>    </code><code>timelimit = 1000000*redis_expirelookups_time_perc/server.hz/100;</code>

<code>    </code><code>timelimit_exit = 0;</code>

<code>    </code><code>if</code> <code>(timelimit &lt;= 0) timelimit = 1;</code>

<code>    </code><code>//周遊處理每個redis資料庫中的失效資料</code>

<code>    </code><code>for</code> <code>(j = 0; j &lt; dbs_per_call; j++) {</code>

<code>        </code><code>int</code> <code>expired;</code>

<code>        </code><code>redisdb *db = server.db+(current_db % server.dbnum);</code>

<code>      </code><code>// 此處立刻就将current_db加一,這樣可以保證即使這次無法在時間限制内删除完所有目前</code>

<code>      </code><code>// 資料庫中的失效主鍵,下一次調用activeexpirecycle一樣會從下一個資料庫開始處理,</code>

<code>       </code><code>//進而保證每個資料庫都有被處理的機會</code>

<code>        </code><code>current_db++;</code>

<code>       </code><code>// 開始處理目前資料庫中的失效主鍵</code>

<code>        </code><code>do</code> <code>{</code>

<code>            </code><code>unsigned </code><code>long</code> <code>num, slots;</code>

<code>            </code><code>long</code> <code>long</code> <code>now;</code>

<code>           </code><code>// 如果expires字典表大小為0,說明該資料庫中沒有設定失效時間的主鍵,直接檢查下</code>

<code>          </code><code>// 一資料庫</code>

<code>            </code><code>if</code> <code>((num = dictsize(db-&gt;expires)) == 0) </code><code>break</code><code>;</code>

<code>            </code><code>slots = dictslots(db-&gt;expires);</code>

<code>            </code><code>now = mstime();</code>

<code>          </code><code>//  如果expires字典表不為空,但是其填充率不足1%,那麼随機選擇主鍵進行檢查的代價</code>

<code>           </code><code>//會很高,是以這裡直接檢查下一資料庫</code>

<code>            </code><code>if</code> <code>(num &amp;&amp; slots &gt; dict_ht_initial_size &amp;&amp;</code>

<code>                </code><code>(num*100/slots &lt; 1)) </code><code>break</code><code>;</code>

<code>            </code><code>expired = 0;</code>

<code>            </code><code>//如果expires字典表中的entry個數不足以達到抽樣個數,則選擇全部key作為抽樣樣本</code>

<code>            </code><code>if</code> <code>(num &gt; redis_expirelookups_per_cron)</code>

<code>                </code><code>num = redis_expirelookups_per_cron;</code>

<code>            </code><code>while</code> <code>(num--) {</code>

<code>                </code><code>dictentry *de;</code>

<code>                </code><code>long</code> <code>long</code> <code>t;</code>

<code>              </code><code>//  随機擷取一個設定了失效時間的主鍵,檢查其是否已經失效</code>

<code>                </code><code>if</code> <code>((de = dictgetrandomkey(db-&gt;expires)) == null) </code><code>break</code><code>;</code>

<code>                </code><code>t = dictgetsignedintegerval(de);</code>

<code>                </code><code>if</code> <code>(now &gt; t) {</code>

<code>           </code><code>// 發現該主鍵确實已經失效,删除該主鍵</code>

<code>                    </code><code>sds key = dictgetkey(de);</code>

<code>                    </code><code>robj *keyobj = createstringobject(key,sdslen(key));</code>

<code>                    </code><code>//同樣要在删除前廣播該主鍵的失效資訊</code>

<code>                    </code><code>propagateexpire(db,keyobj);</code>

<code>                    </code><code>dbdelete(db,keyobj);</code>

<code>                    </code><code>decrrefcount(keyobj);</code>

<code>                    </code><code>expired++;</code>

<code>                    </code><code>server.stat_expiredkeys++;</code>

<code>                </code><code>}</code>

<code>            </code><code>}</code>

<code>           </code><code>// 每進行一次抽樣删除後對iteration加一,每16次抽樣删除後檢查本次執行時間是否</code>

<code>          </code><code>// 已經達到時間限制,如果已達到時間限制,則記錄本次執行達到時間限制并退出</code>

<code>            </code><code>iteration++;</code>

<code>            </code><code>if</code> <code>((iteration &amp; 0xf) == 0 &amp;&amp;</code>

<code>                </code><code>(ustime()-start) &gt; timelimit)</code>

<code>            </code><code>{</code>

<code>                </code><code>timelimit_exit = 1;</code>

<code>                </code><code>return</code><code>;</code>

<code>        </code><code>//如果失效的主鍵數占抽樣數的百分比大于25%,則繼續抽樣删除過程</code>

<code>        </code><code>} </code><code>while</code> <code>(expired &gt; redis_expirelookups_per_cron/4);</code>

  

redis 會定期地檢查設定了失效時間的主鍵并删除已經失效的主鍵,但是通過對每次處理資料庫個數的限制、activeexpirecycle 函數在一秒鐘内執行次數的限制、配置設定給 activeexpirecycle 函數cpu時間的限制、繼續删除主鍵的失效主鍵數百分比的限制,redis 已經大大降低了主鍵失效機制對系統整體性能的影響,但是如果在實際應用中出現大量主鍵在短時間内同時失效的情況還是會産生很多問題,

也就是緩存穿透的情況。

合理的配置緩存可以增加系統的健壯性,避免緩存失效造成的事故。

1.在緩存失效後,通過加鎖或者隊列來控制讀資料庫寫緩存的線程數量。比如對某個key隻允許一個線程查詢資料和寫緩存,其他線程等待。

2.可以通過緩存reload機制,預先去更新緩存.

2.不同的key,設定不同的過期時間,讓緩存失效的時間點盡量均勻。

3.做二級緩存,或者雙緩存政策。a1為原始緩存,a2為拷貝緩存,a1失效時,可以通路a2,a1緩存失效時間設定為短期,a2設定為長期。

memcached 在删除失效主鍵時采用的消極方法,即 memcached 内部不會監視主鍵是否失效,而是在通過 get 通路主鍵時才會檢查其是否已經失效。

其次,memcached 與 redis 在主鍵失效機制上的最大不同是,memcached 不會像 redis 那樣真正地去删除失效的主鍵,而隻是簡單地将失效主鍵占用的空間回收。

這樣當有新的資料寫入到系統中時,memcached 會優先使用那些失效主鍵的空間。

如果失效主鍵的空間用光了,memcached 還可以通過 lru 機制來回收那些長期得不到通路的空間,是以 memcached 并不需要像 redis 中那樣的周期性删除操作,這也是由 memcached 使用的記憶體管理機制決定的。

同時, redis 在出現 oom時同樣可以通過配置 maxmemory-policy 這個參數來決定是否采用 lru 機制來回收記憶體空間。