Redis del bigkey之后為啥還是阻塞的呢度秘?明明開啟了lazyfree,為啥別人立馬可以刪除亭珍?
干貨:[公粽號(hào):堆棧future]
lazyfree redis 4.0引入
lazyfree-lazy-user-del 6.0引入
為什么del刪除bigkey是阻塞的
lazy-free是4.0新增的功能敷钾,但是默認(rèn)是關(guān)閉的,需要手動(dòng)開啟肄梨。你開啟之后阻荒,然后用del刪除一個(gè)幾萬(wàn)的key,發(fā)現(xiàn)命令阻塞在那里了众羡,你很郁悶侨赡,我都開啟了,為撒還阻塞呢?莫非在逗我羊壹?
先說(shuō)答案:你用錯(cuò)了命令蓖宦,在你開啟lazyfree之后,你得用redis提供的命令:unlink油猫,用這個(gè)命令去刪除就是異步了稠茂。
<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">redis> UNLINK key1 key2 key3 (integer) 2
</pre>
源碼角度解釋下為什么
手動(dòng)開啟lazy-free時(shí),有4個(gè)選項(xiàng)可以控制情妖,分別對(duì)應(yīng)不同場(chǎng)景下睬关,要不要開啟異步釋放內(nèi)存機(jī)制:
- lazyfree-lazy-expire:key在過(guò)期刪除時(shí)嘗試異步釋放內(nèi)存
- lazyfree-lazy-eviction:內(nèi)存達(dá)到maxmemory并設(shè)置了淘汰策略時(shí)嘗試異步釋放內(nèi)存
- lazyfree-lazy-server-del:執(zhí)行RENAME/MOVE等命令或需要覆蓋一個(gè)key時(shí),刪除舊key嘗試異步釋放內(nèi)存
- replica-lazy-flush:主從全量同步毡证,從庫(kù)清空數(shù)據(jù)庫(kù)時(shí)異步釋放內(nèi)存
除了replica-lazy-flush之外电爹,其他情況都只是可能去異步釋放key的內(nèi)存,并不是每次必定異步釋放內(nèi)存的料睛。
開啟lazy-free后丐箩,Redis在釋放一個(gè)key的內(nèi)存時(shí),首先會(huì)評(píng)估代價(jià)恤煞,如果釋放內(nèi)存的代價(jià)很小屎勘,那么就直接在主線程中操作了,沒(méi)必要放到異步線程中執(zhí)行(不同線程傳遞數(shù)據(jù)也會(huì)有性能消耗)阱州。
什么情況才會(huì)真正異步釋放內(nèi)存挑秉?這和key的類型、編碼方式苔货、元素?cái)?shù)量都有關(guān)系(詳細(xì)可參考源碼中的lazyfreeGetFreeEffort函數(shù)):
- 當(dāng)Hash/Set底層采用哈希表存儲(chǔ)(非ziplist/int編碼存儲(chǔ))時(shí)犀概,并且元素?cái)?shù)量超過(guò)64個(gè)
- 當(dāng)ZSet底層采用跳表存儲(chǔ)(非ziplist編碼存儲(chǔ))時(shí),并且元素?cái)?shù)量超過(guò)64個(gè)
- 當(dāng)List鏈表節(jié)點(diǎn)數(shù)量超過(guò)64個(gè)(注意夜惭,不是元素?cái)?shù)量姻灶,而是鏈表節(jié)點(diǎn)的數(shù)量,List的實(shí)現(xiàn)是在每個(gè)節(jié)點(diǎn)包含了若干個(gè)元素的數(shù)據(jù)诈茧,這些元素采用ziplist存儲(chǔ))
- 再加一個(gè)條件就是refcount=1 就是沒(méi)有人在引用這個(gè)key的時(shí)候
只有以上這些情況产喉,在刪除key釋放內(nèi)存時(shí),才會(huì)真正放到異步線程中執(zhí)行敢会,其他情況一律還是在主線程操作曾沈。
也就是說(shuō)String(不管內(nèi)存占用多大)、List(少量元素)鸥昏、Set(int編碼存儲(chǔ))塞俱、Hash/ZSet(ziplist編碼存儲(chǔ))這些情況下的key在釋放內(nèi)存時(shí),依舊在主線程中操作吏垮。
直接看源碼:
<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">size_t lazyfreeGetFreeEffort(robj *key, robj *obj, int dbid) { if (obj->type == OBJ_LIST) { quicklist *ql = obj->ptr; return ql->len; } else if (obj->type == OBJ_SET && obj->encoding == OBJ_ENCODING_HT) { dict *ht = obj->ptr; return dictSize(ht); } else if (obj->type == OBJ_ZSET && obj->encoding == OBJ_ENCODING_SKIPLIST){ zset *zs = obj->ptr; return zs->zsl->length; } else if (obj->type == OBJ_HASH && obj->encoding == OBJ_ENCODING_HT) { dict *ht = obj->ptr; return dictSize(ht); } else if (obj->type == OBJ_STREAM) { size_t effort = 0; stream *s = obj->ptr; } }
</pre>
lazyfreeGetFreeEffort函數(shù)判斷下刪除一個(gè)key的付出有多大障涯,然后
<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`#define LAZYFREE_THRESHOLD 64
/* Free an object, if the object is huge enough, free it in async way. */
void freeObjAsync(robj *key, robj *obj, int dbid) {
size_t free_effort = lazyfreeGetFreeEffort(key,obj,dbid);
if (free_effort > LAZYFREE_THRESHOLD && obj->refcount == 1) {
atomicIncr(lazyfree_objects,1);
bioCreateLazyFreeJob(lazyfreeFreeObject,1,obj);
} else {
decrRefCount(obj);
}
}` </pre>
它被freeObjAsync這個(gè)函數(shù)調(diào)用罐旗,用來(lái)和64去判斷并且同時(shí)判斷引用是否為1.只有滿足了大于64并且refcount==1,那么就會(huì)異步刪除唯蝶,異步刪除就是把這個(gè)刪除任務(wù)交由(bio系統(tǒng))去處理九秀,否則調(diào)用decrRefCount在主線稱中同步刪除,如果是用zmalloc申請(qǐng)的內(nèi)存最后都是調(diào)用zfree刪除粘我。
可見(jiàn)鼓蜒,即使開啟了lazy-free,String類型的bigkey涂滴,在刪除時(shí)依舊有阻塞主線程的風(fēng)險(xiǎn)友酱。所以晴音,即便Redis提供了lazy-free柔纵,我建議還是盡量不要在Redis中存儲(chǔ)bigkey。
那為什么有人能del bigkey成功呢锤躁?
能del一個(gè)bigkey成功的搁料,一定是因?yàn)樗玫膔edis版本是6.0及以上的。redis增加了一個(gè)配置項(xiàng):lazyfree-lazy-user-del
系羞,只要開啟了郭计,del直接可以異步刪除key了,這就和unlink沒(méi)啥區(qū)別了椒振。