日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 运维知识 > 数据库 >内容正文

数据库

Redis的LRU缓存淘汰算法实现

發(fā)布時(shí)間:2024/1/18 数据库 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Redis的LRU缓存淘汰算法实现 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

1 標(biāo)準(zhǔn)LRU的實(shí)現(xiàn)原理

LRU,最近最少使用(Least Recently Used,LRU),經(jīng)典緩存算法。

LRU會(huì)使用一個(gè)鏈表維護(hù)緩存中每個(gè)數(shù)據(jù)的訪問(wèn)情況,并根據(jù)數(shù)據(jù)的實(shí)時(shí)訪問(wèn),調(diào)整數(shù)據(jù)在鏈表中的位置,然后通過(guò)數(shù)據(jù)在鏈表中的位置,表示數(shù)據(jù)是最近剛訪問(wèn)的,還是已有段時(shí)間未訪問(wèn)。

LRU會(huì)把鏈頭、尾分別設(shè)為MRU端和LRU端:

  • MRU,Most Recently Used 縮寫(xiě),表示此處數(shù)據(jù)剛被訪問(wèn)
  • LRU端,此處數(shù)據(jù)最近最少被訪問(wèn)的數(shù)據(jù)

LRU可分成如下情況:

  • case1:當(dāng)有新數(shù)據(jù)插入,LRU會(huì)把該數(shù)據(jù)插入到鏈?zhǔn)?#xff0c;同時(shí)把原來(lái)鏈表頭部的數(shù)據(jù)及其之后的數(shù)據(jù),都向尾部移動(dòng)一位
  • case2:當(dāng)有數(shù)據(jù)剛被訪問(wèn)一次后,LRU會(huì)把該數(shù)據(jù)從它在鏈表中當(dāng)前位置,移動(dòng)到鏈?zhǔn)住0褟逆湵眍^部到它當(dāng)前位置的其他數(shù)據(jù),都向尾部移動(dòng)一位
  • case3:當(dāng)鏈表長(zhǎng)度無(wú)法再容納更多數(shù)據(jù),再有新數(shù)據(jù)插入,LRU去除鏈表尾部的數(shù)據(jù),這也相當(dāng)于將數(shù)據(jù)從緩存中淘汰掉

case2圖解:鏈表長(zhǎng)度為5,從鏈表頭部到尾部保存的數(shù)據(jù)分別是5,33,9,10,8。假設(shè)數(shù)據(jù)9被訪問(wèn)一次,則9就會(huì)被移動(dòng)到鏈表頭部,同時(shí),數(shù)據(jù)5和33都要向鏈表尾部移動(dòng)一位。

所以若嚴(yán)格按LRU實(shí)現(xiàn),假設(shè)Redis保存的數(shù)據(jù)較多,還要在代碼中實(shí)現(xiàn):

  • 為Redis使用最大內(nèi)存時(shí),可容納的所有數(shù)據(jù)維護(hù)一個(gè)鏈表

    需額外內(nèi)存空間來(lái)保存鏈表

  • 每當(dāng)有新數(shù)據(jù)插入或現(xiàn)有數(shù)據(jù)被再次訪問(wèn),需執(zhí)行多次鏈表操作

    在訪問(wèn)數(shù)據(jù)的過(guò)程中,讓Redis受到數(shù)據(jù)移動(dòng)和鏈表操作的開(kāi)銷(xiāo)影響

最終導(dǎo)致降低Redis訪問(wèn)性能。

所以,無(wú)論是為節(jié)省內(nèi)存 or 保持Redis高性能,Redis并未嚴(yán)格按LRU基本原理實(shí)現(xiàn),而是提供了一個(gè)近似LRU算法實(shí)現(xiàn)

2 Redis的近似LRU算法實(shí)現(xiàn)

Redis的內(nèi)存淘汰機(jī)制是如何啟用近似LRU算法的?redis.conf中的如下配置參數(shù):

  • maxmemory,設(shè)定Redis server可使用的最大內(nèi)存容量,一旦server使用實(shí)際內(nèi)存量超出該閾值,server會(huì)根據(jù)maxmemory-policy配置策略,執(zhí)行內(nèi)存淘汰操作

  • maxmemory-policy,設(shè)定Redis server內(nèi)存淘汰策略,包括近似LRU、LFU、按TTL值淘汰和隨機(jī)淘汰等

所以,一旦設(shè)定maxmemory選項(xiàng),且將maxmemory-policy配為allkeys-lru或volatile-lru,近似LRU就被啟用。allkeys-lru和volatile-lru都會(huì)使用近似LRU淘汰數(shù)據(jù),區(qū)別在于:

  • allkeys-lru是在所有的KV對(duì)中篩選將被淘汰的數(shù)據(jù)
  • volatile-lru在設(shè)置了TTL的KV對(duì)中篩選將被淘汰數(shù)據(jù)

Redis如何實(shí)現(xiàn)近似LRU算法的呢?

  • 全局LRU時(shí)鐘值的計(jì)算

    如何計(jì)算全局LRU時(shí)鐘值的,以用來(lái)判斷數(shù)據(jù)訪問(wèn)的時(shí)效性

  • 鍵值對(duì)LRU時(shí)鐘值的初始化與更新

    哪些函數(shù)中對(duì)每個(gè)鍵值對(duì)對(duì)應(yīng)的LRU時(shí)鐘值,進(jìn)行初始化與更新

  • 近似LRU算法的實(shí)際執(zhí)行

    如何執(zhí)行近似LRU算法,即何時(shí)觸發(fā)數(shù)據(jù)淘汰,以及實(shí)際淘汰的機(jī)制實(shí)現(xiàn)

2.1 全局LRU時(shí)鐘值的計(jì)算

近似LRU算法仍需區(qū)分不同數(shù)據(jù)的訪問(wèn)時(shí)效性,即Redis需知道數(shù)據(jù)的最近一次訪問(wèn)時(shí)間。因此,有了LRU時(shí)鐘:記錄數(shù)據(jù)每次訪問(wèn)的時(shí)間戳。

Redis對(duì)每個(gè)KV對(duì)中的V,會(huì)使用個(gè)redisObject結(jié)構(gòu)體保存指向V的指針。那redisObject除記錄值的指針,還會(huì)使用24 bits保存LRU時(shí)鐘信息,對(duì)應(yīng)的是lru成員變量。這樣,每個(gè)KV對(duì)都會(huì)把它最近一次被訪問(wèn)的時(shí)間戳,記錄在lru變量。

redisObject定義包含lru成員變量的定義:

每個(gè)KV對(duì)的LRU時(shí)鐘值是如何計(jì)算的?Redis Server使用一個(gè)實(shí)例級(jí)別的全局LRU時(shí)鐘,每個(gè)KV對(duì)的LRU time會(huì)根據(jù)全局LRU時(shí)鐘進(jìn)行設(shè)置。

這全局LRU時(shí)鐘保存在Redis全局變量server的成員變量lruclock

當(dāng)Redis Server啟動(dòng)后,調(diào)用initServerConfig初始化各項(xiàng)參數(shù)時(shí),會(huì)調(diào)用getLRUClock設(shè)置lruclock的值:

于是,就得注意,**若一個(gè)數(shù)據(jù)前后兩次訪問(wèn)的時(shí)間間隔<1s,那這兩次訪問(wèn)的時(shí)間戳就是一樣的!**因?yàn)長(zhǎng)RU時(shí)鐘精度就是1s,它無(wú)法區(qū)分間隔小于1秒的不同時(shí)間戳!

getLRUClock函數(shù)將獲得的UNIX時(shí)間戳,除以LRU_CLOCK_RESOLUTION后,就得到了以LRU時(shí)鐘精度來(lái)計(jì)算的UNIX時(shí)間戳,也就是當(dāng)前的LRU時(shí)鐘值。

getLRUClock會(huì)把LRU時(shí)鐘值和宏定義LRU_CLOCK_MAX(LRU時(shí)鐘能表示的最大值)做與運(yùn)算。

所以默認(rèn)情況下,全局LRU時(shí)鐘值是以1s為精度計(jì)算得UNIX時(shí)間戳,且是在initServerConfig中進(jìn)行的初始化。

那Redis Server運(yùn)行過(guò)程中,全局LRU時(shí)鐘值是如何更新的?和Redis Server在事件驅(qū)動(dòng)框架中,定期運(yùn)行的時(shí)間事件所對(duì)應(yīng)的serverCron有關(guān)。

serverCron作為時(shí)間事件的回調(diào)函數(shù),本身會(huì)周期性執(zhí)行,其頻率值由redis.conf的hz配置項(xiàng)決定,默認(rèn)值10,即serverCron函數(shù)會(huì)每100ms(1s/10 = 100ms)運(yùn)行一次。serverCron中,全局LRU時(shí)鐘值就會(huì)按該函數(shù)執(zhí)行頻率,定期調(diào)用getLRUClock進(jìn)行更新:

這樣,每個(gè)KV對(duì)就能從全局LRU時(shí)鐘獲取最新訪問(wèn)時(shí)間戳。

對(duì)于每個(gè)KV對(duì),它對(duì)應(yīng)的redisObject.lru在哪些函數(shù)進(jìn)行初始化和更新的呢?

2.2 鍵值對(duì)LRU時(shí)鐘值的初始化與更新

對(duì)于一個(gè)KV對(duì),其LRU時(shí)鐘值最初是在這KV對(duì)被創(chuàng)建時(shí),進(jìn)行初始化設(shè)置的,這初始化操作在createObject函數(shù)中調(diào)用,當(dāng)Redis要?jiǎng)?chuàng)建一個(gè)KV對(duì),就會(huì)調(diào)用該函數(shù)。

createObject除了會(huì)給redisObject分配內(nèi)存空間,還會(huì)根據(jù)maxmemory_policy配置,初始化設(shè)置redisObject.lru。

  • 若maxmemory_policy=LFU,則lru變量值會(huì)被初始化設(shè)置為L(zhǎng)FU算法的計(jì)算值
  • maxmemory_policy≠LFU,則createObject調(diào)用LRU_CLOCK設(shè)置lru值,即KV對(duì)對(duì)應(yīng)的LRU時(shí)鐘值。

LRU_CLOCK返回當(dāng)前全局LRU時(shí)鐘值。因?yàn)橐粋€(gè)KV對(duì)一旦被創(chuàng)建,就相當(dāng)于有了次訪問(wèn),其對(duì)應(yīng)LRU時(shí)鐘值就表示了它的訪問(wèn)時(shí)間戳:

那一個(gè)KV對(duì)的LRU時(shí)鐘值又是何時(shí)再被更新?

只要一個(gè)KV對(duì)被訪問(wèn),其LRU時(shí)鐘值就會(huì)被更新!而當(dāng)一個(gè)KV對(duì)被訪問(wèn)時(shí),訪問(wèn)操作最終都會(huì)調(diào)用lookupKey

lookupKey會(huì)從全局哈希表中查找要訪問(wèn)的KV對(duì)。若該KV對(duì)存在,則lookupKey會(huì)根據(jù)maxmemory_policy的配置值,來(lái)更新鍵值對(duì)的LRU時(shí)鐘值,也就是它的訪問(wèn)時(shí)間戳。

而當(dāng)maxmemory_policy沒(méi)有配置為L(zhǎng)FU策略時(shí),lookupKey函數(shù)就會(huì)調(diào)用LRU_CLOCK函數(shù),來(lái)獲取當(dāng)前的全局LRU時(shí)鐘值,并將其賦值給鍵值對(duì)的redisObject結(jié)構(gòu)體中的lru變量

這樣,每個(gè)KV對(duì)一旦被訪問(wèn),就能獲得最新的訪問(wèn)時(shí)間戳。但你可能好奇:這些訪問(wèn)時(shí)間戳最終是如何被用于近似LRU算法進(jìn)行數(shù)據(jù)淘汰的?

2.3 近似LRU算法的實(shí)際執(zhí)行

Redis之所以實(shí)現(xiàn)近似LRU,是為減少內(nèi)存資源和操作時(shí)間上的開(kāi)銷(xiāo)。

2.3.1 何時(shí)觸發(fā)算法執(zhí)行?

近似LRU主要邏輯在performEvictions。

performEvictions被evictionTimeProc調(diào)用,而evictionTimeProc函數(shù)又是被processCommand調(diào)用。

processCommand,Redis處理每個(gè)命令時(shí)都會(huì)調(diào)用:

然后,isSafeToPerformEvictions還會(huì)再次根據(jù)如下條件判斷是否繼續(xù)執(zhí)行performEvictions:

一旦performEvictions被調(diào)用,且maxmemory-policy被設(shè)置為allkeys-lru或volatile-lru,近似LRU就被觸發(fā)執(zhí)行了。

2.3.2 近似LRU具體執(zhí)行過(guò)程

執(zhí)行可分成如下步驟:

2.3.2.1 判斷當(dāng)前內(nèi)存使用情況

調(diào)用getMaxmemoryState評(píng)估當(dāng)前內(nèi)存使用情況,判斷當(dāng)前Redis Server使用內(nèi)存容量是否超過(guò)maxmemory配置值。

若未超過(guò)maxmemory,則返回C_OK,performEvictions也會(huì)直接返回。

getMaxmemoryState評(píng)估當(dāng)前內(nèi)存使用情況的時(shí)候,若發(fā)現(xiàn)已用內(nèi)存超出maxmemory,會(huì)計(jì)算需釋放的內(nèi)存量。這個(gè)釋放內(nèi)存大小=已使用內(nèi)存量-maxmemory。

但已使用內(nèi)存量并不包括用于主從復(fù)制的復(fù)制緩沖區(qū)大小,這是getMaxmemoryState通過(guò)調(diào)用freeMemoryGetNotCountedMemory計(jì)算的。

而若當(dāng)前Server使用的內(nèi)存量超出maxmemory上限,則performEvictions會(huì)執(zhí)行while循環(huán)淘汰數(shù)據(jù)釋放內(nèi)存。

為淘汰數(shù)據(jù),Redis定義數(shù)組EvictionPoolLRU,保存待淘汰的候選KV對(duì),元素類型是evictionPoolEntry結(jié)構(gòu)體,保存了待淘汰KV對(duì)的空閑時(shí)間idle、對(duì)應(yīng)K等信息:

這樣,Redis Server在執(zhí)行initSever進(jìn)行初始化時(shí),會(huì)調(diào)用evictionPoolAlloc為EvictionPoolLRU數(shù)組分配內(nèi)存空間,該數(shù)組大小由EVPOOL_SIZE決定,默認(rèn)可保存16個(gè)待淘汰的候選KV對(duì)。

performEvictions在淘汰數(shù)據(jù)的循環(huán)流程中,就會(huì)更新這個(gè)待淘汰的候選KV對(duì)集合,即EvictionPoolLRU數(shù)組。

2.3.2.2 更新待淘汰的候選KV對(duì)集合

performEvictions調(diào)用evictionPoolPopulate,其會(huì)先調(diào)用dictGetSomeKeys,從待采樣哈希表隨機(jī)獲取一定數(shù)量K:

  • dictGetSomeKeys采樣的哈希表,由maxmemory_policy配置項(xiàng)決定:
    • 若maxmemory_policy=allkeys_lru,則待采樣哈希表是Redis Server的全局哈希表,即在所有KV對(duì)中采樣
    • 否則,待采樣哈希表就是保存著設(shè)置了TTL的K的哈希表。
  • dictGetSomeKeys采樣的K的數(shù)量由配置項(xiàng)maxmemory-samples決定,默認(rèn)5:
  • 于是,dictGetSomeKeys返回采樣的KV對(duì)集合。evictionPoolPopulate根據(jù)實(shí)際采樣到的KV對(duì)數(shù)量count,執(zhí)行循環(huán):調(diào)用estimateObjectIdleTime計(jì)算在采樣集合中的每一個(gè)KV對(duì)的空閑時(shí)間:

    接著,evictionPoolPopulate遍歷待淘汰的候選KV對(duì)集合,即EvictionPoolLRU數(shù)組,嘗試把采樣的每個(gè)KV對(duì)插入EvictionPoolLRU數(shù)組,取決于如下條件之一:

  • 能在數(shù)組中找到一個(gè)尚未插入KV對(duì)的空位
  • 能在數(shù)組中找到一個(gè)KV對(duì)的空閑時(shí)間<采樣KV對(duì)的空閑時(shí)間
  • 有一成立,evictionPoolPopulate就能把采樣KV對(duì)插入EvictionPoolLRU數(shù)組。等所有采樣鍵值對(duì)都處理完后,evictionPoolPopulate函數(shù)就完成對(duì)待淘汰候選鍵值對(duì)集合的更新了。

    接下來(lái),performEvictions開(kāi)始選擇最終被淘汰的KV對(duì)。

    2.3.2.3 選擇被淘汰的KV對(duì)并刪除

    因evictionPoolPopulate已更新EvictionPoolLRU數(shù)組,且該數(shù)組里的K,是按空閑時(shí)間從小到大排好序了。所以,performEvictions遍歷一次EvictionPoolLRU數(shù)組,從數(shù)組的最后一個(gè)K開(kāi)始選擇,若選到的K非空,就把它作為最終淘汰的K。

    該過(guò)程執(zhí)行邏輯:

    一旦選到被淘汰的K,performEvictions就會(huì)根據(jù)Redis server的惰性刪除配置,執(zhí)行同步刪除或異步刪除:

    至此,performEvictions就淘汰了一個(gè)K。若此時(shí)釋放的內(nèi)存空間還不夠,即沒(méi)有達(dá)到待釋放空間,則performEvictions還會(huì)重復(fù)執(zhí)行前面所說(shuō)的更新待淘汰候選KV對(duì)集合、選擇最終淘汰K的過(guò)程,直到滿足待釋放空間的大小要求。

    performEvictions流程:

    近似LRU算法并未使用耗時(shí)且耗空間的鏈表,而使用固定大小的待淘汰數(shù)據(jù)集合,每次隨機(jī)選擇一些K加入待淘汰數(shù)據(jù)集合。

    最后,按待淘汰集合中K的空閑時(shí)間長(zhǎng)度,刪除空閑時(shí)間最長(zhǎng)的K。

    總結(jié)

    根據(jù)LRU算法的基本原理,發(fā)現(xiàn)若嚴(yán)格按基本原理實(shí)現(xiàn)LRU算法,則開(kāi)發(fā)的系統(tǒng)就需要額外內(nèi)存空間保存LRU鏈表,系統(tǒng)運(yùn)行時(shí)也會(huì)受到LRU鏈表操作的開(kāi)銷(xiāo)影響。

    而Redis的內(nèi)存資源和性能都很重要,所以Redis實(shí)現(xiàn)近似LRU算法:

    • 首先是設(shè)置了全局LRU時(shí)鐘,并在KV對(duì)創(chuàng)建時(shí)獲取全局LRU時(shí)鐘值作為訪問(wèn)時(shí)間戳,及在每次訪問(wèn)時(shí)獲取全局LRU時(shí)鐘值,更新訪問(wèn)時(shí)間戳
    • 然后,當(dāng)Redis每處理一個(gè)命令,都調(diào)用performEvictions判斷是否需釋放內(nèi)存。若已使用內(nèi)存超出maxmemory,則隨機(jī)選擇一些KV對(duì),組成待淘汰候選集合,并根據(jù)它們的訪問(wèn)時(shí)間戳,選出最舊數(shù)據(jù)淘汰

    一個(gè)算法的基本原理和算法的實(shí)際執(zhí)行,在系統(tǒng)開(kāi)發(fā)中會(huì)有一定折中,需綜合考慮所開(kāi)發(fā)的系統(tǒng),在資源和性能方面的要求,以避免嚴(yán)格按照算法實(shí)現(xiàn)帶來(lái)的資源和性能開(kāi)銷(xiāo)。

    總結(jié)

    以上是生活随笔為你收集整理的Redis的LRU缓存淘汰算法实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。