ABAP Netweaver, Hybris Commerce和SAP 云平臺的登錄認證
1302
2025-04-02
Lazy Free會影響緩存替換嗎?
redis緩存淘汰是為了在Redis server內存使用量超過閾值時,篩選一些冷數(shù)據(jù),從Redis server中刪除。我們在前兩節(jié)課,LRU和LFU在最后淘汰數(shù)據(jù)時,都會刪除被淘汰數(shù)據(jù)。
但它們在刪除淘汰數(shù)據(jù)時,會根據(jù)如下配置項決定是否啟用Lazy Free(惰性刪除)
惰性刪除,Redis 4.0后功能,使用后臺線程執(zhí)行刪除數(shù)據(jù)的任務,避免了刪除操作阻塞主線程。但后臺線程異步刪除數(shù)據(jù)能及時釋放內存嗎?它會影響到redis緩存的正常使用嗎?
1 配置惰性刪除
當Redis server希望啟動惰性刪除時,需在redis.conf設置惰性刪除相關配置項,包括如下場景:
默認值都是no。所以,要在緩存淘汰時啟用,就要將lazyfree-lazy-eviction置yes。Redis server在啟動過程中進行配置參數(shù)初始化時,會根據(jù)redis.conf,設置全局變量server的lazyfree_lazy_eviction成員變量。
若看到對server.lazyfree_lazy_eviction變量值進行條件判斷,那就是Redis根據(jù)lazyfree-lazy-eviction配置項,決定是否執(zhí)行惰性刪除。
2 被淘汰數(shù)據(jù)的刪除過程
getMaxmemoryState負責執(zhí)行數(shù)據(jù)淘汰,篩選出被淘汰的鍵值對后,就要開始刪除被淘汰的數(shù)據(jù):
為被淘汰的key創(chuàng)建一個SDS對象,然后調用propagateExpire:
Redis server可能針對緩存淘汰場景啟用惰性刪除,propagateExpire會根據(jù)全局變量server.lazyfree_lazy_eviction決定刪除操作對應命令:
lazyfree_lazy_eviction=1(啟用緩存淘汰時的惰性刪除),則刪除操作對應UNLINK命令
否則,就是DEL命令
因為這些命令經常使用,所以Redis為這些命令創(chuàng)建共享對象,即sharedObjectsStruct結構體,并用一個全局變量shared表示


在該結構體中包含了指向共享對象的指針,這其中就包括了unlink和del命令對象。
然后,propagateExpire在為刪除操作創(chuàng)建命令對象時,就使用了shared變量中的unlink或del對象:
接著,propagateExpire會判斷Redis server是否啟用AOF日志:
若啟用,則propagateExpire會調用feedAppendOnlyFile,把被淘汰key的刪除操作記錄到AOF文件,保證后續(xù)使用AOF文件進行Redis數(shù)據(jù)庫恢復時,可以和恢復前保持一致。通過實現(xiàn)。
然后,propagateExpire調用propagate,把刪除操作同步給從節(jié)點,以保證主從節(jié)點的數(shù)據(jù)一致。propagate流程:
接下來,performEvictions就會開始執(zhí)行刪除。
performEvictions根據(jù)server是否啟用了惰性刪除,分別執(zhí)行:
Case1:若server啟用惰性刪除,performEvictions調用dbAsyncDelete異步刪除
Case2:若server未啟用惰性刪除,performEvictions調用dbSyncDelete同步刪除
performEvictions在調用刪除函數(shù)前,都會調用zmalloc_used_memory計算當前使用內存量。然后,它在調用刪除函數(shù)后,會再次調用zmalloc_used_memory函數(shù)計算此時的內存使用量,并計算刪除操作導致的內存使用量差值,這個差值就是通過刪除操作而被釋放的內存量。
performEvictions最后把這部分釋放的內存量和已釋放內存量相加,得到最新內存釋放量:
所以performEvictions在選定被刪除的KV對后,可通過異步或同步操作來完成數(shù)據(jù)的實際刪除。那數(shù)據(jù)異步刪除和同步刪除到底如何執(zhí)行的?
3 數(shù)據(jù)刪除操作
刪除操作包含兩步:
將被淘汰的KV對從哈希表剔除,這哈希表既可能是設置了過期key的哈希表,也可能是全局哈希表
釋放被淘汰KV對所占用的內存空間
若這倆操作一起做,就是同步刪除;只做1,而2由后臺線程執(zhí)行,就是異步刪除。
Redis使用dictGenericDelete實現(xiàn)了這倆操作。
**首先,dictGenericDelete函數(shù)會先在哈希表中查找要刪除的key。**它會計算被刪除key的哈希值,然后根據(jù)哈希值找到key所在的哈希桶。
因為不同key的哈希值可能相同,而Redis的哈希表是采用了鏈式哈希(你可以回顧下第3講中介紹的鏈式哈希),所以即使我們根據(jù)一個key的哈希值,定位到了它所在的哈希桶,我們也仍然需要在這個哈希桶中去比對查找,這個key是否真的存在。
也正是由于這個原因,dictGenericDelete函數(shù)緊接著就會在哈希桶中,進一步比對查找要刪除的key。如果找到了,它就先把這個key從哈希表中去除,也就是把這個key從哈希桶的鏈表中去除。
然后,dictGenericDelete會根據(jù)入參nofree,決定是否實際釋放K和V的內存空間:
dictGenericDelete根據(jù)nofree決定執(zhí)行同步or異步刪除。
dictDelete V.S dictUnlink
給dictGenericDelete傳遞的nofree參數(shù)值是0 or 1:
nofree=0:同步刪除
nofree=1,異步刪除
好了,到這里,我們就了解了同步刪除和異步刪除的基本代碼實現(xiàn)。下面我們就再來看下,在剛才介紹的performEvictions函數(shù)中,它在刪除鍵值對時,所調用的dbAsyncDelete和dbSyncDelete這兩個函數(shù),是如何使用dictDelete和dictUnlink來實際刪除被淘汰數(shù)據(jù)的。
基于異步刪除的數(shù)據(jù)淘汰
由dbAsyncDelete執(zhí)行:
調用dictDelete
調用dictUnlink:
被淘汰的KV對只是在全局哈希表中被移除,其占用內存空間還是沒有實際釋放。所以dbAsyncDelete會調用lazyfreeGetFreeEffort,計算釋放被淘汰KV對內存空間的開銷:
若開銷較小,dbAsyncDelete直接在主I/O線程中進行同步刪除
否則,dbAsyncDelete創(chuàng)建惰性刪除任務,并交給后臺線程完成
雖dbAsyncDelete說是執(zhí)行惰性刪除,但在實際執(zhí)行過程中,會使用lazyfreeGetFreeEffort評估刪除開銷。
lazyfreeGetFreeEffort根據(jù)要刪除的KV對的類型計算刪除開銷:
若KV對類型屬于List、Hash、Set和Sorted Set這四種集合類型中的一種,且未使用緊湊型內存結構,則該KV對的刪除開銷就等于集合中的元素個數(shù)
否則,刪除開銷等于1
舉個例子,如下代碼就展示了azyfreeGetFreeEffort計算List和Set類型鍵值對的刪除開銷:KV對是Set類型,同時它是使用哈希表結構而不是整數(shù)集合來保存數(shù)據(jù)的話,那么它的刪除開銷就是Set中的元素個數(shù)。
這樣,當dbAsyncDelete通過lazyfreeGetFreeEffort計得被淘汰KV對的刪除開銷后:
把刪除開銷和宏定義LAZYFREE_THRESHOLD(默認64)比較。
當被淘汰KV對為包含超過64個元素的集合類型時,dbAsyncDelete才會調用bioCreateBackgroundJob實際創(chuàng)建后臺任務執(zhí)行惰性刪除。
若被淘汰KV對不是集合類型或是集合類型但包含元素個數(shù)≤64個,則dbAsyncDelete調用dictFreeUnlinkedEntry釋放KV對所占的內存空間。
dbAsyncDelete使用后臺任務或主IO線程釋放內存空間:
基于異步刪除的數(shù)據(jù)淘汰過程,實際上會根據(jù)要刪除的KV對包含的元素個數(shù),決定是使用后臺線程還是主線程執(zhí)行刪除操作。
主線程如何知道后臺線程釋放的內存空間,已滿足待釋放空間的大小?performEvictions在調用dbAsyncDelete或dbSyncDelete前后,都會統(tǒng)計已使用內存量,并計算調用刪除函數(shù)前后的差值,這就能知曉已釋放的內存空間大小。
此外,performEvictions在調用dbAsyncDelete后,會再主動檢測當前內存使用量,是否已滿足最大內存容量要求。一旦滿足,performEvictions就會停止淘汰數(shù)據(jù)的執(zhí)行流程。
可以。主線程決定淘汰這個 key 后,會先把這個 key 從「全局哈希表」剔除,然后評估釋放內存代價,如符合條件,則丟到「后臺線程」執(zhí)行「釋放內存」操作。
之后就可繼續(xù)處理客戶端請求,盡管后臺線程還未完成釋放內存,但因 key 已被全局哈希表剔除,所以主線程已查詢不到該 key,對客戶端無影響。
同步刪除的數(shù)據(jù)淘汰
dbSyncDelete實現(xiàn)。
首先,調用dictDelete,在過期key的哈希表中刪除被淘汰的KV對
再次調用dictDelete,在全局哈希表中刪除被淘汰的KV對
dictDelete調用dictGenericDelete同步釋放KV對的內存空間時,最終分別調用dictFreeKey、dictFreeVal和zfree釋放K、V和KV對所對應的哈希項這三者所占內存空間。
它們根據(jù)操作的哈希表類型,調用相應valDestructor和keyDestructor函數(shù)釋放內存:
為方便能找到最終進行內存釋放操作的函數(shù),以全局哈希表為例,看當操作全局哈希表時,KV對的dictFreeVal和dictFreeKey兩個宏定義對應的函數(shù)。
全局哈希表是在initServer中創(chuàng)建:
dbDictType是個dictType類型結構體:
dbDictType作為全局哈希表,保存:
SDS類型的key
多種數(shù)據(jù)類型的value
所以,dbDictType類型哈希表的K和V釋放函數(shù),分別是dictSdsDestructor和dictObjectDestructor:
dictSdsDestructor直接調用sdsfree,釋放SDS字符串占用的內存空間。dictObjectDestructor會調用decrRefCount,執(zhí)行釋放操作:
decrRefCount會判斷待釋放對象的引用計數(shù)。:
只有當引用計數(shù)為1,才會根據(jù)待釋放對象的類型,調用具體類型的釋放函數(shù)來釋放內存空間
否則,decrRefCount只是把待釋放對象的引用計數(shù)減1
若待釋放對象的引用計數(shù)為1,且String類型,則decrRefCount調用freeStringObject執(zhí)行最終的內存釋放操作。
若對象是List類型,則decrRefCount調用freeListObject最終釋放內存:
基于同步刪除的數(shù)據(jù)淘汰過程,就是通過dictDelete將被淘汰KV對從全局哈希表移除,并通過dictFreeKey、dictFreeVal和zfree釋放內存空間。
釋放v空間的函數(shù)是decrRefCount,根據(jù)V的引用計數(shù)和類型,最終調用不同數(shù)據(jù)類型的釋放函數(shù)來完成內存空間釋放。
基于異步刪除的數(shù)據(jù)淘汰,它通過后臺線程執(zhí)行的函數(shù)是lazyfreeFreeObjectFromBioThread函數(shù),該函數(shù)實際上也是調用了decrRefCount釋放內存空間。
總結
Redis 4.0后提供惰性刪除功能,所以Redis緩存淘汰數(shù)據(jù)時,就會根據(jù)是否啟用惰性刪除,決定是執(zhí)行同步刪除還是異步的惰性刪除。
同步刪除還是異步的惰性刪除,都會先把被淘汰的KV對從哈希表中移除。然后,同步刪除就會緊接著調用dictFreeKey、dictFreeVal和zfree分別釋放key、value和鍵值對哈希項的內存空間。而異步的惰性刪除,則是把空間釋放任務交給了后臺線程完成。
雖惰性刪除是由后臺線程異步完成,但后臺線程啟動后會監(jiān)聽惰性刪除的任務隊列,一旦有惰性刪除任務,后臺線程就會執(zhí)行并釋放內存空間。所以,從淘汰數(shù)據(jù)釋放內存空間的角度來說,惰性刪除并不影響緩存淘汰時的空間釋放要求。
后臺線程需通過同步機制獲取任務,這過程會引入一些額外時間開銷,會導致內存釋放不像同步刪除那樣非常及時。這也是Redis在被淘汰數(shù)據(jù)是小集合(元素不超過64個)時,仍使用主線程進行內存釋放的考慮因素。
Redis 專家
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。