<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Redis 過(guò)期 key 刪除,那些不得不說(shuō)的事情!

          共 11897字,需瀏覽 24分鐘

           ·

          2022-03-18 19:44

          點(diǎn)擊上方“程序員大白”,選擇“星標(biāo)”公眾號(hào)

          重磅干貨,第一時(shí)間送達(dá)

          1.過(guò)期key的刪除策略

          • 定時(shí)刪除:當(dāng)為key設(shè)置過(guò)期時(shí)間的時(shí)候,設(shè)置一個(gè)定時(shí)任務(wù),到時(shí)間后即刻調(diào)用并刪除
          • 定期刪除:每隔一定的時(shí)間,對(duì)某些key進(jìn)行掃描,并刪除掉其中已經(jīng)過(guò)期的key
          • 惰性刪除:不進(jìn)行任何操作,只有訪問(wèn)到當(dāng)前key時(shí),如果已經(jīng)過(guò)期再去刪除該key

          定時(shí)刪除策略對(duì)于內(nèi)存來(lái)說(shuō)是最友好的,過(guò)期的key立刻被刪除,不會(huì)過(guò)多的占用內(nèi)存,但是會(huì)消耗大部分的時(shí)間片,對(duì)cpu很不友好。

          惰性刪除平時(shí)不做任何操作,只有key被訪問(wèn)到的時(shí)候才會(huì)進(jìn)行判斷與刪除,對(duì)于cpu非常友好,但是這些已經(jīng)過(guò)期的key會(huì)占用大量的內(nèi)存,造成極大的浪費(fèi)。

          定期刪除是每隔一段時(shí)間進(jìn)行一次掃描,這是一種折衷的方案,既不會(huì)對(duì)cpu造成很高的負(fù)擔(dān),也不會(huì)讓大量過(guò)期的key未被刪除而造成浪費(fèi),但是對(duì)于掃描的頻率和時(shí)間間隔設(shè)置較為復(fù)雜,如果設(shè)置的不科學(xué),也會(huì)產(chǎn)生與其他方式類似的問(wèn)題。

          redis使用了后面兩種方式來(lái)進(jìn)行過(guò)期key的刪除,在內(nèi)存使用和cpu使用率上做了一定的取舍。與過(guò)期key刪除策略經(jīng)常弄混的是redis的內(nèi)存逐出策略,redis的逐出策略只有在內(nèi)存容量到達(dá)上線maxmemory之后才會(huì)使用,平時(shí)狀態(tài)是不會(huì)使用的;而redis的過(guò)期key刪除策略是每次循環(huán)都會(huì)使用的。

          redis的redis的內(nèi)存逐出策略有八種算法:

          • volatile-lru:設(shè)置了過(guò)期時(shí)間的key使用LRU算法淘汰;
          • allkeys-lru:所有key使用LRU算法淘汰;
          • volatile-lfu:設(shè)置了過(guò)期時(shí)間的key使用LFU算法淘汰;
          • allkeys-lfu:所有key使用LFU算法淘汰;
          • volatile-random:設(shè)置了過(guò)期時(shí)間的key使用隨機(jī)淘汰;
          • allkeys-random:所有key使用隨機(jī)淘汰;
          • volatile-ttl:設(shè)置了過(guò)期時(shí)間的key根據(jù)過(guò)期時(shí)間淘汰,越早過(guò)期越早淘汰;
          • noeviction:默認(rèn)策略,當(dāng)內(nèi)存達(dá)到設(shè)置的最大值時(shí),所有申請(qǐng)內(nèi)存的操作都會(huì)報(bào)錯(cuò),只讀命令可以正常執(zhí)行;

          2.redis過(guò)期key刪除詳解

          2.1 reids中的字典

          redis作為一種k-v數(shù)據(jù)庫(kù),支持多種數(shù)據(jù)類型,這些數(shù)據(jù)的保存都和一個(gè)結(jié)構(gòu)體redisDB相關(guān)。

          在redis中,有一個(gè)鏈表用來(lái)做數(shù)據(jù)上的邏輯區(qū)分,鏈表上的每個(gè)節(jié)點(diǎn)都是一個(gè)redisDb,編號(hào)從0到n(可以在配置文件中修改,默認(rèn)為16,通過(guò)select命令可以切換編號(hào),編號(hào)對(duì)應(yīng)結(jié)構(gòu)體中的id字段)。

          typedef?struct?redisDb?{
          ????dict?*dict;?????????????????/*?DB的鍵空間,保存了所有的key?*/
          ????dict?*expires;??????????????/*?保存了所有的過(guò)期key,dict的子集?*/
          ????dict?*blocking_keys;????????/*?實(shí)現(xiàn)阻塞客戶端使用該字典?*/
          ????dict?*ready_keys;???????????/*??喚醒阻塞客戶端使用該字典?*/
          ????dict?*watched_keys;?????????/*?實(shí)現(xiàn)簡(jiǎn)單事物使用該字典?*/
          ????int?id;?????????????????????/*?數(shù)據(jù)庫(kù)ID?*/
          ????long?long?avg_ttl;??????????/*?平均過(guò)期時(shí)間?*/
          }

          每次對(duì)數(shù)據(jù)庫(kù)進(jìn)行寫命令操作時(shí),就會(huì)在字典dict中進(jìn)行相應(yīng)的操作(增加或者修改),如果對(duì)該key的過(guò)期時(shí)間有操作,則去expires字典中進(jìn)行操作(增加或者修改)。

          字典的實(shí)現(xiàn)主要是由下面的結(jié)構(gòu)體來(lái)實(shí)現(xiàn),與本文有關(guān)的兩個(gè)結(jié)構(gòu)是ht[2]rehashidxht[2]里面含有兩個(gè)哈希表,哈希表1用來(lái)進(jìn)行節(jié)點(diǎn)的存儲(chǔ),哈希表2通常情況下是空指針,沒(méi)有被真實(shí)分配空間;當(dāng)哈希表1的負(fù)載因子達(dá)到了擴(kuò)容的要求時(shí),則對(duì)哈希表2進(jìn)行內(nèi)存分配,用于進(jìn)行哈希表擴(kuò)容。

          由于redis主線程是單線程的reactor模型,為了防止rehash造成線程阻塞,所以redis采用了漸進(jìn)式rehash來(lái)進(jìn)行哈希表的擴(kuò)縮容,每次對(duì)一定數(shù)量的槽位進(jìn)行rehash,并將下一次需要進(jìn)行rehash的槽位索引位置保存在rehashidx中,方便下一次進(jìn)行rehash

          //?字典結(jié)構(gòu)體
          typedef?struct?dict?{
          ????dictType?*type;??/*?保存了一些功能函數(shù)?*/
          ????void?*privdata;??/*?保存一些數(shù)據(jù)用于修改字典?*/
          ????dictht?ht[2];????/*?哈希表?*/
          ????long?rehashidx;?/*?rehash進(jìn)行到的索引位置?*/??
          ????unsigned?long?iterators;?/*?當(dāng)前迭代器的運(yùn)行數(shù)量?*/
          }

          redis中的哈希表的實(shí)現(xiàn)方式可以歸結(jié)為數(shù)組加鏈表,使用拉鏈法(鏈地址法)來(lái)解決哈希地址沖突;哈希表的槽位(哈希桶)個(gè)數(shù)為2^n個(gè),每次擴(kuò)縮容的結(jié)果都是2的冪次。

          進(jìn)行key操作的時(shí)候,首先計(jì)算放入的索引位置,idx=hash(key)%sizemark,由于掩碼的值為 2^n-1,所以這里的取模運(yùn)算可以簡(jiǎn)化為按位與,提高運(yùn)算速度。

          typedef?struct?dictht?{
          ????dictEntry?**table;?//?哈希槽數(shù)組
          ????unsigned?long?size;?//?哈希槽的個(gè)數(shù)(數(shù)組長(zhǎng)度)
          ????unsigned?long?sizemask;?//?哈希掩碼,size-1
          ????unsigned?long?used;?//?哈希表實(shí)際所擁有的元素個(gè)數(shù)
          }?dictht;

          哈希表里有一個(gè)重要的指標(biāo):負(fù)載因子,可以通過(guò)used/size來(lái)計(jì)算。當(dāng)負(fù)載超過(guò)一定限制的時(shí)候,需要進(jìn)行擴(kuò)容。與java不同的是,redis的哈希表在某個(gè)槽位過(guò)多的時(shí)候,并不會(huì)轉(zhuǎn)化為紅黑樹(shù);同時(shí)在負(fù)載因子過(guò)高的時(shí)候,進(jìn)行漸進(jìn)式的rehash。

          • 當(dāng)沒(méi)有bgsave或者bgrewriteaof的時(shí)候,負(fù)載因子大于1.0時(shí)
          • 當(dāng)進(jìn)行bgsave或者bgrewriteaof的時(shí)候,負(fù)載因子大于5.0時(shí)
          • 當(dāng)為ht[1]預(yù)分配空間后,內(nèi)存超過(guò)了maxmemory且負(fù)載因子大于1.618時(shí)(黃金比例0.618的倒數(shù),此功能在redis6.2版本加入)

          redis從4.0之后開(kāi)始使用siphash算法來(lái)進(jìn)行運(yùn)算,相比之前相比(murmurhash)提高了運(yùn)行速度,并且降低了哈希碰撞的概率,可以有效防止hash-dos。一個(gè)良好的hash算法能夠?qū)ey分散的比較均勻,在負(fù)載因子較小的情況下,可以保證每個(gè)槽位上的元素個(gè)數(shù)都是比較少的。hash表中每個(gè)槽位上的元素個(gè)數(shù)符合Poisson分布:

          當(dāng)負(fù)載因子為0.5時(shí),每個(gè)槽位上的元素個(gè)數(shù)概率分布如下所示:

          P(x=k)?=?exp(-0.5)?*?pow(0.5,?k)?/?factorial(k)
          *0?????0.606530659713
          *1?????0.303265329856
          *2?????0.0758163324641
          *3?????0.0126360554107
          *4?????0.00157950692633
          *5?????0.000157950692633
          *6?????1.31625577195e-05
          *7?????9.40182694247e-07
          *8?????5.87614183904e-08

          當(dāng)元素個(gè)數(shù)多于8個(gè)時(shí),概率小于千萬(wàn)分之一

          當(dāng)負(fù)載因子為1.0時(shí),每個(gè)槽位上當(dāng)元素個(gè)數(shù)如下所示:

          P(x=k)?=?exp(-1)?*?pow(1,?k)?/?factorial(k)
          *0?????0.367879441171
          *1?????0.367879441171
          *2?????0.183939720586
          *3?????0.0613132401952
          *4?????0.0153283100488
          *5?????0.00306566200976
          *6?????0.000510943668294
          *7?????7.29919526134e-05
          *8?????9.12399407667e-06

          當(dāng)元素個(gè)數(shù)多于8個(gè)時(shí),概率約為十萬(wàn)分之一

          在正常情況下,每個(gè)哈希槽位上的節(jié)點(diǎn)一般都很少,可以認(rèn)為在常數(shù)級(jí)時(shí)間復(fù)雜度O(1) 獲取到元素。

          2.2 redis的內(nèi)存刪除邏輯

          2.2.1 redis的事件驅(qū)動(dòng)模型

          redis是一個(gè)單線程事件驅(qū)動(dòng)的模型,主線程主要在aeMain中進(jìn)行,通過(guò)配置文件中的屬性hz來(lái)確定每秒鐘含有幾個(gè)循環(huán)周期,默認(rèn)為10。

          redis在初始化和加載后,就在aeMain中進(jìn)行循環(huán),直到接收到信號(hào)或發(fā)生錯(cuò)誤才會(huì)退出,每個(gè)周期都會(huì)按照如下步驟進(jìn)行處理:

          1. 處理beforesleep回調(diào)函數(shù)相關(guān)功能
          2. 處理時(shí)間事件
          3. 處理aftersleep回調(diào)函數(shù)相關(guān)功能
          4. 處理文件事件
          5. 進(jìn)入下一次循環(huán)(回到a)
          void?aeMain(aeEventLoop?*eventLoop)?{
          ????eventLoop->stop?=?0;
          ????while?(!eventLoop->stop)?{
          ????????if?(eventLoop->beforesleep?!=?NULL)
          ????????????eventLoop->beforesleep(eventLoop);
          ????????aeProcessEvents(eventLoop,?AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
          ????}
          }
          • beforesleep回調(diào)函數(shù)主要功能有進(jìn)行一次快速的過(guò)期key淘汰,發(fā)送命令要求slave上報(bào)backlog偏移量,嘗試釋放blocking的客戶端,aof文件刷盤等
          • 時(shí)間事件主要放在一個(gè)無(wú)序鏈表里,正常運(yùn)行的redis只有一個(gè)事件事件ServerCron(redis啟動(dòng)加載數(shù)據(jù)時(shí),有兩個(gè)時(shí)間事件),發(fā)起一次慢速的過(guò)期key淘汰,嘗試進(jìn)行rehash,發(fā)送定時(shí)心跳,檢測(cè)部分后臺(tái)bio線程的狀態(tài)等
          • aftersleep回調(diào)函數(shù)目前只有module相關(guān)功能的處理
          • 文件事件主要是通過(guò)io多路復(fù)用功能,獲取就緒的文件事件進(jìn)行相應(yīng)的讀寫處理
          2.2.2 redis過(guò)期key刪除的實(shí)現(xiàn)

          這里使用了pyhton的偽代碼代替了原始的c代碼做說(shuō)明,主要承接上文介紹了兩種清理模式(快速清理和慢速清理),這個(gè)函數(shù)是過(guò)期key淘汰功能的核心,可以動(dòng)態(tài)的調(diào)節(jié)cpu用在掃描過(guò)期鍵的時(shí)間,當(dāng)過(guò)期鍵較少時(shí),使用更少的cpu時(shí)間片;當(dāng)過(guò)期key較多時(shí),則會(huì)表現(xiàn)的比較激進(jìn)。

          beforesleep回調(diào)函數(shù)調(diào)用時(shí),會(huì)嘗試一次快速清理功能,這種清理最大能持續(xù)EXPIRE_FAST_CYCLE_DURATION時(shí)間,并且在相同的時(shí)間內(nèi)不會(huì)重復(fù)調(diào)用;

          aftersleep回調(diào)函數(shù)階段,會(huì)調(diào)用慢速清理功能,最大能調(diào)用每個(gè)redis處理周期ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC百分比的時(shí)間片。

          通常情況下慢速淘汰功能能夠淘汰大量的key,只有頻繁觸發(fā)時(shí)間限制的時(shí)候,說(shuō)明慢速清理還剩下很多過(guò)期的key沒(méi)有清理,需要穿插一些快速清理功能來(lái)盡可能的刪除過(guò)期key。

          ACTIVE_EXPIRE_CYCLE_SLOW?=?0??#?type標(biāo)志位,持續(xù)時(shí)間比較久的慢速清理模式
          ACTIVE_EXPIRE_CYCLE_FAST?=?1??#?type標(biāo)志位,持續(xù)時(shí)間比短的快速清理模式

          ACTIVE_EXPIRE_CYCLE_FAST_DURATION?=?1000??#?快速清理模式的最長(zhǎng)持續(xù)時(shí)間,單位:微秒
          ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP?=?20??#?每次掃描隨機(jī)抽取的key個(gè)數(shù)
          ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC?=?25??#?掃描key所占用的cpu最長(zhǎng)時(shí)間片
          CRON_DBS_PER_CALL?=?16??#?每次隨機(jī)掃描的db個(gè)數(shù)

          dbnums?=?0??#?server里面設(shè)置了多少個(gè)redisDb的邏輯分區(qū)
          current_db?=?0??#?當(dāng)前遍歷到的數(shù)據(jù)庫(kù)編號(hào)
          timelimit_exit?=?0??#?flag標(biāo)識(shí)位,用于判斷是否是到達(dá)了時(shí)間上限才退出循環(huán)
          last_fast_cycle?=?0??#?上一次快速淘汰開(kāi)始的時(shí)間


          def?activeExpireCycle(type):
          ????#?獲取現(xiàn)在的運(yùn)行時(shí)間,用于信息push和計(jì)算是否需要退出的baseline
          ????start?=?time.time()
          ????#?設(shè)定每次需要遍歷的數(shù)據(jù)庫(kù)
          ????dbs_per_call?=?CRON_DBS_PER_CALL

          ????if?type?==?ACTIVE_EXPIRE_CYCLE_FAST:
          ????????#?如果上一次掃描沒(méi)有觸發(fā)時(shí)間限制就退出了,說(shuō)明過(guò)期key數(shù)量不多,不要浪費(fèi)性能在掃描key上
          ????????if?not?timelimit_exit:
          ????????????return
          ????????#?如果距離上次快速淘汰開(kāi)始的時(shí)間間隔小于2倍的快速淘汰持續(xù)時(shí)間,直接返回
          ????????if?start?????????????return
          ????????#?設(shè)定本次快速淘汰開(kāi)始的時(shí)間,為下一次調(diào)用做時(shí)間baseline
          ????????last_fast_cycle?=?start

          ????#?如果每次掃描的數(shù)據(jù)庫(kù)數(shù)量大于實(shí)際系統(tǒng)分配的邏輯分區(qū)數(shù)量,按照實(shí)際邏輯分區(qū)數(shù)量進(jìn)行掃描
          ????#?如果上次掃描觸發(fā)了時(shí)間限制,說(shuō)明過(guò)期key比較多,需要進(jìn)行全部邏輯分區(qū)的掃描
          ????if?dbs_per_call?>?server.dbnum?or?timelimit_exit:
          ????????dbs_per_call?=?dbnums

          ????#?計(jì)算本次循環(huán)最多能持續(xù)的時(shí)間,1000000微秒*(25/100)/?10
          ????#?1秒鐘10hz,每次redis的處理周期為100毫秒(100000微秒),25%cpu時(shí)間分片就是25000微秒
          ????timelimit?=?1000000?*?ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC?/?server.hz?/?100
          ????#?如果是快速清理模式,則每次最多能持續(xù)1000微秒
          ????if?type?==?ACTIVE_EXPIRE_CYCLE_FAST:
          ????????timelimit?=?ACTIVE_EXPIRE_CYCLE_FAST_DURATION

          ????#?遍歷指定數(shù)量(dbs_per_call)的數(shù)據(jù)庫(kù)
          ????for?i?in?range(0,?dbs_per_call):
          ????????#?如果觸發(fā)了時(shí)間限制,直接退出循環(huán)
          ????????if?time.time()?-?start?>?timelimit:
          ????????????break
          ????????#?當(dāng)前數(shù)據(jù)庫(kù)中不存在過(guò)期的key,直接掃描下一個(gè)數(shù)據(jù)庫(kù)
          ????????if?redisDb[i].expires.size()?==?0:
          ????????????continue
          ????????#?如果當(dāng)前數(shù)據(jù)庫(kù)含有過(guò)期時(shí)間的key數(shù)量小于1%,直接掃描下一個(gè)數(shù)據(jù)庫(kù)
          ????????if?redisDb[i].expires.size()?*?100.0?/?redisDb[i].dict.size()?????????????continue

          ????????while?True:
          ????????????#?獲取每次掃描的key的數(shù)量
          ????????????lookup_nums?=?min(redisDb[i].expires.size(),?ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
          ????????????#?已經(jīng)過(guò)期的key的數(shù)量
          ????????????expired?=?0

          ????????????while?lookup_nums?>?0:
          ????????????????#?隨機(jī)獲取一個(gè)key判斷是否過(guò)期
          ????????????????lookup_key?=?random_get_key_from_expire(redisDb[i])
          ????????????????#?如果過(guò)期了,就刪除
          ????????????????if?is_expire(lookup_key):
          ????????????????????del?(lookup_key)
          ????????????????????expired?+=?1
          ????????????#?判斷當(dāng)前時(shí)刻是否觸發(fā)了時(shí)間限制,如果觸發(fā)了限制,直接退出
          ????????????if?time.time()?-?start?>?timelimit:
          ????????????????break
          ????????????????break??#?兩個(gè)break,不符合python語(yǔ)法,這里表示結(jié)束全部的循環(huán)

          ????????????#?如果本次循環(huán)掃描出來(lái)的key已經(jīng)過(guò)期的比例小于25%,則去掃描下一個(gè)數(shù)據(jù)庫(kù)
          ????????????#?否則說(shuō)明本數(shù)據(jù)庫(kù)過(guò)期的key較多,立刻在本數(shù)據(jù)庫(kù)中再次掃描和清理
          ????????????if?expired?*?4?/?lookup_nums?????????????????break
          2.2.3 redis過(guò)期key優(yōu)化點(diǎn)

          上文中提到了,過(guò)期key是隨機(jī)從全部的含有過(guò)期時(shí)間的key中進(jìn)行選擇。使用的算法如下,這里存在的不足是,當(dāng)過(guò)期key數(shù)量較少時(shí),哈希桶槽位上大部分的元素為空,隨機(jī)數(shù)生成后所得到的哈希桶槽位經(jīng)常miss,需要再次進(jìn)行隨機(jī),會(huì)消耗過(guò)多的時(shí)間片在生成隨機(jī)數(shù)上,而不是清理過(guò)期key。

          def?random_get_key_from_expire(redisDb):
          ????slot_nums?=?0
          ????#?如果在進(jìn)行rehash,則兩個(gè)哈希表都可能有key存在,需要從中隨機(jī)選擇一個(gè)
          ????if?(is_rehash(redisDb.expires)):
          ????????slot_nums?=?ht[0].size?+?ht[1].size
          ????else:
          ????????slot_nums?=?ht[0].size

          ????headEntry?=?None
          ????while?True:
          ????????#?從所有的槽位中隨機(jī)選擇一個(gè)
          ????????slot_idx?=?random.randint(0,?slot_nums)
          ????????#?直到找到一個(gè)槽位不為空的哈希桶,就停止循環(huán)
          ????????if?(slot_idx?>?ht[0].size):
          ????????????#?如果所得到的索引大于ht[0]的大小,說(shuō)明落在了ht[1]中
          ????????????tempEntry?=?ht[1].get_index(slot_idx?-?ht[0].size)
          ????????????if?tempEntry?!=?None:
          ????????????????headEntry?=?tempEntry
          ????????????????break
          ????????else:
          ????????????tempEntry?=?ht[0].get_index(slot_idx)
          ????????????if?tempEntry?!=?None:
          ????????????????headEntry?=?tempEntry
          ????????????????break

          ????currEntry?=?headEntry
          ????len?=?0
          ????#?遍歷鏈表獲得長(zhǎng)度
          ????while?currEntry.next:
          ????????currEntry?=?currEntry.next
          ????????len?+=?1

          ????#?從鏈表中隨機(jī)選擇一個(gè)節(jié)點(diǎn)返回
          ????listele?=?random.randint(0,?len)
          ????while?listele:
          ????????headEntry?=?headEntry.next
          ????????listele?-=?1
          ????return?headEntry

          此外,如果redis的寫入量比較大,且key過(guò)期時(shí)間比較短,即使兩種淘汰方式同時(shí)進(jìn)行,也會(huì)超過(guò)redis的處理能力,依然會(huì)造成數(shù)據(jù)的堆積,如果容量繼續(xù)上漲超過(guò),就會(huì)進(jìn)行內(nèi)存淘汰(使用allkeys-lru淘汰策略,會(huì)優(yōu)先刪除lru較小的key,由于惰性刪除的原因,已過(guò)期的key的lru會(huì)通常情況下比較小,會(huì)被優(yōu)先逐出)。

          由于走了另外一套邏輯(逐出邏輯)造成了cpu時(shí)間片的浪費(fèi),它們最終的結(jié)果都是key刪除,但是卻進(jìn)行了不同的篩選策略。實(shí)際使用場(chǎng)景中遇到這種情況的時(shí)刻不多,對(duì)于性能和吞吐量的影響不大,但是也是一個(gè)可以優(yōu)化的點(diǎn)。

          實(shí)際使用中,還有一種人為觸發(fā)過(guò)期key淘汰的方法,就是通過(guò)scan命令來(lái)進(jìn)行全庫(kù)的掃描,通過(guò)控制scan命令的游標(biāo)和count數(shù)量,可以預(yù)期的控制淘汰的激烈程度,不會(huì)對(duì)主線程造成阻塞。

          redis在進(jìn)行key的刪除的時(shí)候,如果開(kāi)啟了異步刪除,則當(dāng)被刪除的key所對(duì)應(yīng)的val占用空間大于64字節(jié)時(shí),會(huì)將這個(gè)key標(biāo)記為刪除后直接返回+OK,然后將val放到后臺(tái)的bio線程里面進(jìn)行刪除,防止阻塞主線程;如果占用的空間小于64字節(jié),即使開(kāi)啟了異步刪除,在最后運(yùn)行的時(shí)候也會(huì)同步的進(jìn)行刪除(redis優(yōu)秀的性能優(yōu)化在細(xì)節(jié)之末隨處可見(jiàn),針對(duì)很多場(chǎng)景都做了優(yōu)化,并抽象出參數(shù)給用戶動(dòng)態(tài)配置,它的高性能是與redis作者精益求精的修改分不開(kāi)的)。

          2.2.4 redis6對(duì)過(guò)期key刪除的優(yōu)化

          redis6針對(duì)以上的問(wèn)題提出了幾個(gè)改進(jìn)點(diǎn),首先將原來(lái)的隨機(jī)抽樣檢查過(guò)期key變成了按照哈希桶順序遍歷檢查過(guò)期key。通過(guò)在redisDb結(jié)構(gòu)體中增加long expires_cursor字段,用來(lái)記錄上一次遍歷到的游標(biāo)位置,每次遍歷都會(huì)取到這個(gè)游標(biāo)位置對(duì)應(yīng)的索引,然后繼續(xù)下去,使得cpu的時(shí)間片更多的用來(lái)進(jìn)行key過(guò)期的判斷和刪除上。

          typedef?struct?redisDb?{
          ????dict?*dict;?????????????????
          ????dict?*expires;??????????????
          ????dict?*blocking_keys;????????
          ????dict?*ready_keys;???????????
          ????dict?*watched_keys;?????????
          ????int?id;?????????????????????
          ????long?long?avg_ttl;??????????
          ????unsigned?long?expires_cursor;?/*?主動(dòng)過(guò)期循環(huán)的游標(biāo)*/
          ????list?*defrag_later;?????????/*?key組成的鏈表,用來(lái)進(jìn)行內(nèi)存碎片整理?*/
          }?redisDb;

          另外在server的配置中增加了activate_expire_effort標(biāo)識(shí)位,可以被設(shè)定為1-10,用來(lái)表示定期刪除淘汰key的激烈程度。在系統(tǒng)設(shè)定好activate_expire_effort之后,redis的定期刪除循環(huán)邏輯每次都會(huì)重新計(jì)算閾值,比如快速淘汰循環(huán)最大的持續(xù)時(shí)間,慢速淘汰循環(huán)最大的持續(xù)時(shí)間,這些參數(shù)在之前的淘汰算法中都是一個(gè)確定的值。通過(guò)抽象成一個(gè)配置文件的參數(shù),可以通過(guò)命令熱加載動(dòng)態(tài)的調(diào)節(jié),達(dá)到redis吞吐量和容量使用的“均衡”。

          struct?redisServer?{
          ????????????...
          ????int?active_expire_enabled;??????/*?Can?be?disabled?for?testing?purposes.?*/
          ????int?active_expire_effort;???????/*?From?1?(default)?to?10,?active?effort.?*/
          ????????????...
          }
          ACTIVE_EXPIRE_CYCLE_SLOW?=?0??#?type標(biāo)志位,持續(xù)時(shí)間比較久的慢速清理模式
          ACTIVE_EXPIRE_CYCLE_FAST?=?1??#?type標(biāo)志位,持續(xù)時(shí)間比短的快速清理模式

          ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP?=?20??#?每次每個(gè)db掃描的key個(gè)數(shù)
          ACTIVE_EXPIRE_CYCLE_FAST_DURATION?=?1000??#?快速清理模式的最長(zhǎng)持續(xù)時(shí)間,這里只是一個(gè)基線
          ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC?=?25??#?掃描key所占用的cpu最長(zhǎng)時(shí)間片
          ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE?=?10??#?開(kāi)啟efforts后,過(guò)期key占據(jù)的百分比
          CRON_DBS_PER_CALL?=?16??#?每次隨機(jī)掃描的db個(gè)數(shù)

          current_db?=?0??#?當(dāng)前遍歷到的數(shù)據(jù)庫(kù)編號(hào)
          timelimit_exit?=?0??#?flag標(biāo)識(shí)位,用于判斷是否是到達(dá)了時(shí)間上限才退出循環(huán)
          last_fast_cycle?=?0??#?上一次快速淘汰開(kāi)始的時(shí)間


          def?activeExpireCycle(type):
          ????#?一個(gè)統(tǒng)計(jì)量,表示系統(tǒng)對(duì)于過(guò)期key掃描的激進(jìn)程度(0~9)
          ????#?該配置可以通過(guò)配置文件來(lái)進(jìn)行設(shè)置,根據(jù)集群的平均過(guò)期時(shí)間,動(dòng)態(tài)的設(shè)定
          ????effort?=?server.active_expire_effort?-?1,
          ????#?每次循環(huán)掃描的key的數(shù)量
          ????config_keys_per_loop?=?ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP?+?ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP?/?4?*?effort
          ????#?快速清理循環(huán)的持續(xù)時(shí)間
          ????config_cycle_fast_duration?=?ACTIVE_EXPIRE_CYCLE_FAST_DURATION?+?ACTIVE_EXPIRE_CYCLE_FAST_DURATION?/?4?*?effort
          ????#?清理循環(huán)周期所占用的最大cpu時(shí)間片百分比
          ????config_cycle_slow_time_perc?=?ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC?+?2?*?effort
          ????#?有過(guò)期時(shí)間的key所全部key占的最多的百分比
          ????config_cycle_acceptable_stale?=?ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE?-?effort

          ????#?全局變量,函數(shù)退出后值仍然保存,留待下一次函數(shù)調(diào)用
          ????global?last_fast_cycle
          ????global?current_db
          ????global?timelimit_exit

          ????start?=?time.time()
          ????dbs_per_call?=?CRON_DBS_PER_CALL
          ????iteration?=?0

          ????if?type?==?ACTIVE_EXPIRE_CYCLE_FAST:
          ????????#?如果上一次掃描沒(méi)有觸發(fā)時(shí)間限制就退出,并且系統(tǒng)所含有的過(guò)期key的比例小于配置的閾值直接返回
          ????????#?因?yàn)榇藭r(shí)過(guò)期快key的數(shù)量不多,并不需要浪費(fèi)過(guò)多的cpu時(shí)間片在掃描過(guò)期key上
          ????????if?(not?timelimit_exit)?and?(server.stat_expired_stale_perc?????????????return
          ????????#?如果距離上次快速周期循環(huán)開(kāi)始的時(shí)間間隔小于2倍的快速周期循環(huán)持續(xù)時(shí)間,直接返回
          ????????if?start?????????????return
          ????????#?設(shè)定本次快速淘汰開(kāi)始的時(shí)間,為下一次調(diào)用做時(shí)間baseline
          ????????last_fast_cycle?=?start

          ????#?如果每次掃描的數(shù)據(jù)庫(kù)數(shù)量大于實(shí)際系統(tǒng)分配的邏輯分區(qū)數(shù)量,按照實(shí)際邏輯分區(qū)數(shù)量進(jìn)行掃描
          ????#?如果上次掃描觸發(fā)了時(shí)間限制,說(shuō)明過(guò)期key比較多,需要進(jìn)行全部邏輯分區(qū)的掃描
          ????if?dbs_per_call?>?server.dbnum?or?timelimit_exit:
          ????????dbs_per_call?=?server.dbnums

          ????#?計(jì)算本次循環(huán)最多能持續(xù)的時(shí)間,1000000微秒*(25/100)/?10
          ????#?1秒鐘10hz,每次redis的處理周期為100毫秒(100000微秒),25%cpu時(shí)間分片就是25000微秒
          ????#?如果設(shè)置了effort,則會(huì)增加用于過(guò)期key刪除的cpu時(shí)間片,例如effort=6,那么將有35000微秒用于處理過(guò)期key
          ????timelimit?=?1000000?*?config_cycle_slow_time_perc?/?server.hz?/?100

          ????#?如果是快速清理模式,則每次最多能持續(xù)config_cycle_fast_duration微秒
          ????if?type?==?ACTIVE_EXPIRE_CYCLE_FAST:
          ????????timelimit?=?config_cycle_fast_duration

          ????#?遍歷指定數(shù)量(dbs_per_call)的數(shù)據(jù)庫(kù)
          ????for?i?in?range(0,?dbs_per_call):
          ????????#?如果觸發(fā)了時(shí)間限制,直接退出循環(huán)
          ????????if?time.time()?-?start?>?timelimit:
          ????????????break

          ????????#?獲取每次要掃描的數(shù)據(jù)庫(kù)編號(hào),并記錄下來(lái),這樣可以讓cpu將時(shí)間片公平的分給每個(gè)數(shù)據(jù)庫(kù)
          ????????db_idx?=?current_db?%?server.dbnum
          ????????#?先變更下一次要掃描的數(shù)據(jù)庫(kù),方式程序因超時(shí)返回時(shí)忘記變更需要掃描的數(shù)據(jù)庫(kù),
          ????????#?也是為了將時(shí)間片公平的分散到每個(gè)數(shù)據(jù)庫(kù)
          ????????db_idx?+=?1

          ????????while?True:

          ????????????#?當(dāng)前數(shù)據(jù)庫(kù)中不存在過(guò)期的key,直接掃描下一個(gè)數(shù)據(jù)庫(kù)
          ????????????if?redisDb[db_idx].expires.size()?==?0:
          ????????????????break
          ????????????#?如果當(dāng)前數(shù)據(jù)庫(kù)含有過(guò)期時(shí)間的key數(shù)量小于1%,直接掃描下一個(gè)數(shù)據(jù)庫(kù)
          ????????????if?redisDb[db_idx].expires.size()?*?100.0?/?redisDb[db_idx].dict.size()?????????????????break

          ????????????#?獲取每次掃描的key的數(shù)量
          ????????????num?=?redisDb[db_idx].expires.size()
          ????????????if?num?>?config_keys_per_loop:
          ????????????????num?=?config_keys_per_loop??#?每次至多只會(huì)選擇config_keys_per_loop個(gè)key進(jìn)行查詢

          ????????????#?最多遍歷的哈希桶數(shù)量
          ????????????max_buckets?=?num?*?20
          ????????????#?已經(jīng)遍歷的哈希桶數(shù)量
          ????????????checked_buckets?=?0
          ????????????#?已經(jīng)掃描的過(guò)期key的個(gè)數(shù)
          ????????????sampled?=?0
          ????????????#?已經(jīng)過(guò)期key的個(gè)數(shù)
          ????????????expires?=?0

          ????????????#?如果掃描的哈希桶數(shù)量過(guò)多或者已經(jīng)掃描了規(guī)定數(shù)量的key,就退出循環(huán)防止占用過(guò)多時(shí)間
          ????????????while?sampled?????????????????#?字典里只有兩個(gè)哈希表ht[0],ht[1],每次循環(huán)會(huì)選擇其中的一個(gè)
          ????????????????for?table?in?(0,?1):
          ????????????????????#?沒(méi)有進(jìn)行rehash時(shí),ht[1]為空,遍歷到此處時(shí)直接返回
          ????????????????????if?table?==?1?and?is_rehash(redisDb[db_idx].expires):
          ????????????????????????break

          ????????????????????#?保存了上一次遍歷到的哈希桶索引位置
          ????????????????????idx?=?redisDb[db_idx].expires_cursor
          ????????????????????#?與掩碼進(jìn)行計(jì)算,確定本次開(kāi)始遍歷的哈希桶索引位置
          ????????????????????idx?=?idx?&?redisDb[db_idx].expires.ht[i].sizemark
          ????????????????????#?獲取哈希桶索引位置對(duì)應(yīng)的鏈表的頭節(jié)點(diǎn)
          ????????????????????dictEntry?=?redisDb[db_idx].expires.ht[i].bucket[idx]


          ????????????????????while?dictEntry:
          ????????????????????????#?遍歷鏈表,如果過(guò)期了就刪除,并計(jì)數(shù)已經(jīng)過(guò)期的key
          ????????????????????????if?is_expire(dictEntry):
          ????????????????????????????del?(dictEntry)
          ????????????????????????????expires?+=?1
          ????????????????????????dictEntry?=?dictEntry.next
          ????????????????????????sampled?+=?1

          ????????????????#?記錄下一次要遍歷的哈希桶的索引位置
          ????????????????redisDb[db_idx].expires_cursor?+=?1
          ????????????????#?如果掃描時(shí)間過(guò)長(zhǎng)達(dá)到了規(guī)定的閾值,則直接返回
          ????????????????if?time.time()?-?start?>?timelimit:
          ????????????????????return??#?直接返回全部的
          ????????????#?如果過(guò)期的key占比高于閾值,說(shuō)明當(dāng)前庫(kù)里面的過(guò)期key很多,需要再次遍歷進(jìn)行淘汰
          ????????????if?expires?*?100.0?/?sampled?>?config_cycle_acceptable_stale:
          ????????????????continue
          ????????????#?如果掃描到的所有的桶都是空的,觸發(fā)了max_buckets限制而退出,說(shuō)明沒(méi)有清理到過(guò)期的key
          ????????????#?過(guò)期的key都在其他的桶之中,需要進(jìn)行再次的掃描
          ????????????elif?sampled?==?0:
          ????????????????continue
          ????????????else:
          ????????????????break

          來(lái)源:blog.csdn.net/qq_38856118/article/

          details/115796879


          13個(gè)你一定要知道的PyTorch特性

          解讀:為什么要做特征歸一化/標(biāo)準(zhǔn)化?

          一文搞懂 PyTorch 內(nèi)部機(jī)制

          張一鳴:每個(gè)逆襲的年輕人,都具備的底層能力


          關(guān)


          學(xué)西學(xué)學(xué)運(yùn)營(yíng)護(hù)號(hào)樂(lè)質(zhì)結(jié)識(shí)關(guān)[]學(xué)習(xí)進(jìn)


          瀏覽 72
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日日摸日日添日日躁AV | 亚洲AV无码A片在线观看蜜桃 | 麻豆成人精品视频三级 | 欧美成人免费在线观看 | 色综合视频二区偷拍在线 |