摘要:現(xiàn)象先搭建一個(gè)服務(wù)器,版本號(hào)為,看看它的內(nèi)存信息接著用給創(chuàng)建一個(gè)名為的大,有個(gè),每個(gè)的值都是這時(shí)候我們看看的內(nèi)存占用情況由于大的創(chuàng)建,內(nèi)存占用多了多兆。但其實(shí)這個(gè)不起眼的命令也可能造成一樣的問(wèn)題,使用時(shí)需要謹(jǐn)慎對(duì)待。
背景
rename是redis中給key重命名命令,rename key newkey的意思就是將key重命名為newkey。
大部分文檔在介紹rename的時(shí)候只將它描述成一個(gè)時(shí)間復(fù)雜度為O(1)的命令,卻忘了說(shuō)明它可能導(dǎo)致的性能問(wèn)題(涉及覆蓋舊值的時(shí)候 時(shí)間復(fù)雜度應(yīng)該是O(1)+O(M))。
我們先做個(gè)試驗(yàn)看看rename的問(wèn)題。
現(xiàn)象先搭建一個(gè)redis服務(wù)器,版本號(hào)為3.2,看看它的內(nèi)存信息
127.0.0.1:8401> info memory # Memory used_memory:842416 used_memory_human:822.67K
接著用lua給redis創(chuàng)建一個(gè)名為 test的大key,test有500w個(gè)field,每個(gè)field的值都是1
127.0.0.1:8401> eval "for i=1,5000000,1 do redis.call("hset","test", i,1) end" 0 (nil) (11.61s) 127.0.0.1:8401> hlen test (integer) 5000000
這時(shí)候我們看看redis的內(nèi)存占用情況
127.0.0.1:8401> info memory # Memory used_memory:381185592 used_memory_human:363.53M
由于大key test的創(chuàng)建,redis內(nèi)存占用多了300多兆。
接下來(lái)我們創(chuàng)建一個(gè)臨時(shí)key,并用它來(lái)rename掉大key test
127.0.0.1:8401> set tmp 1 OK 127.0.0.1:8401> rename tmp test OK (2.36s)
這時(shí)就能看到執(zhí)行時(shí)間的異常了,rename執(zhí)行時(shí)間長(zhǎng)達(dá)2.36秒,這是為什么呢?我們?cè)倏纯磖edis內(nèi)存占用情況:
127.0.0.1:8401> info memory # Memory used_memory:821528 used_memory_human:802.27K
通過(guò)info返回的信息我們可以發(fā)現(xiàn)在執(zhí)行rename之后redis將大key test大小為300多兆的值對(duì)象直接刪除并回收掉了,而redis刪除一個(gè)key的時(shí)間復(fù)雜度是O(M),在這里M是被刪除的成員數(shù)量---500w。應(yīng)該就是這個(gè)"隱式"刪除操作導(dǎo)致了高延遲的產(chǎn)生。
文檔我們看看官方文檔是怎么描述rename這一行為的:
RENAME key newkeyRenames?key?to?newkey. It returns an error when?key?does not exist. If?newkey?already exists it is overwritten, when this happens?RENAMEexecutes an implicit?DEL?operation, so if the deleted key contains a very big value it may cause high latency even if?RENAME?itself is usually a constant-time operation.
newkey如果本就存在,redis會(huì)用key的值覆蓋掉newkey的值,而newkey原本的值會(huì)被redis隱式地刪除。我們知道大key的刪除伴隨著高延遲(redis是單進(jìn)程服務(wù),服務(wù)器會(huì)在刪除大key期間block住接下來(lái)其他命令的執(zhí)行),這就導(dǎo)致時(shí)間復(fù)雜度本為O(1)的rename也有可能卡住redis。
這句官方文檔的原話我沒(méi)在其他文檔里找到類似的翻譯,看這些文檔的開(kāi)發(fā)者可能會(huì)誤以為這是個(gè)特別安全的O(1)命令。
既然文檔里已經(jīng)說(shuō)明了這種行為的存在,我就順便看看源碼這塊邏輯是怎么走的:
源碼分析db.c void renameCommand(client *c) { renameGenericCommand(c,0); } void renameGenericCommand(client *c, int nx) { robj *o; ... if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.nokeyerr)) == NULL) //舊key的值對(duì)象地址復(fù)制給o return; ... incrRefCount(o); //舊key的值對(duì)象引用計(jì)數(shù)+1(被o引用) if (lookupKeyWrite(c->db,c->argv[2]) != NULL) { //如果新key已經(jīng)有值對(duì)象了 ... dbDelete(c->db,c->argv[2]); //新key從db中移除、并將新key的值對(duì)象引用計(jì)數(shù)-1(變?yōu)?),并釋放內(nèi)存 } dbAdd(c->db,c->argv[2],o); //將新key => 舊key的值對(duì)象的組合放入db中 ... dbDelete(c->db,c->argv[1]); //舊key從db中移除、并將舊key的值對(duì)象引用計(jì)數(shù)-1(不會(huì)變?yōu)?),不釋放內(nèi)存 ... }
正常O(1)重命名的邏輯不用多說(shuō),涉及到覆蓋的過(guò)程可以簡(jiǎn)化成如下圖:
在改變指針的指向之前,redis會(huì)先用if (lookupKeyWrite(c->db,c->argv[2]) != NULL)判斷newkey是否有對(duì)應(yīng)的值,若有 則調(diào)用dbDelete(c->db,c->argv[2]);將newkey的值v2刪掉。
結(jié)論用redis的時(shí)候,keys、 hgetall、 del 這些命令我們會(huì)多加小心,因?yàn)椴缓侠淼卣{(diào)用它們可能會(huì)長(zhǎng)時(shí)間block住redis的其他請(qǐng)求 甚至導(dǎo)致CPU使用率居高不下從而卡住整個(gè)服務(wù)器。但其實(shí)rename這個(gè)不起眼的命令也可能造成一樣的問(wèn)題,使用時(shí)需要謹(jǐn)慎對(duì)待。
參考資料RENAME – Redis
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/29641.html
摘要:查看擴(kuò)展探針擴(kuò)展相關(guān)函數(shù)管理擴(kuò)展配置下安裝流程下載對(duì)應(yīng)的版本一定要選擇正確下載擴(kuò)展的版本下載地址選擇對(duì)應(yīng)的版本版本版本在中開(kāi)啟擴(kuò)展,配置擴(kuò)展相關(guān)的參數(shù)有需要的話重啟服務(wù)器下安裝流程直裝流程把相應(yīng)的擴(kuò)展移動(dòng)到你的文件夾下面然后在中開(kāi)啟相應(yīng)的擴(kuò) 1 查看php擴(kuò)展(1)phpinfo 探針(2)php擴(kuò)展相關(guān)函數(shù)get_loaded_extensions() arrayextension...
閱讀 3111·2023-04-26 00:23
閱讀 3515·2021-09-13 10:28
閱讀 2340·2021-08-31 14:18
閱讀 3044·2019-08-30 15:54
閱讀 2072·2019-08-30 15:43
閱讀 1423·2019-08-29 16:56
閱讀 2888·2019-08-29 14:16
閱讀 2145·2019-08-28 17:51