摘要:現(xiàn)象先搭建一個服務器,版本號為,看看它的內存信息接著用給創(chuàng)建一個名為的大,有個,每個的值都是這時候我們看看的內存占用情況由于大的創(chuàng)建,內存占用多了多兆。但其實這個不起眼的命令也可能造成一樣的問題,使用時需要謹慎對待。
背景
rename是redis中給key重命名命令,rename key newkey的意思就是將key重命名為newkey。
大部分文檔在介紹rename的時候只將它描述成一個時間復雜度為O(1)的命令,卻忘了說明它可能導致的性能問題(涉及覆蓋舊值的時候 時間復雜度應該是O(1)+O(M))。
我們先做個試驗看看rename的問題。
現(xiàn)象先搭建一個redis服務器,版本號為3.2,看看它的內存信息
127.0.0.1:8401> info memory # Memory used_memory:842416 used_memory_human:822.67K
接著用lua給redis創(chuàng)建一個名為 test的大key,test有500w個field,每個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
這時候我們看看redis的內存占用情況
127.0.0.1:8401> info memory # Memory used_memory:381185592 used_memory_human:363.53M
由于大key test的創(chuàng)建,redis內存占用多了300多兆。
接下來我們創(chuàng)建一個臨時key,并用它來rename掉大key test
127.0.0.1:8401> set tmp 1 OK 127.0.0.1:8401> rename tmp test OK (2.36s)
這時就能看到執(zhí)行時間的異常了,rename執(zhí)行時間長達2.36秒,這是為什么呢?我們再看看redis內存占用情況:
127.0.0.1:8401> info memory # Memory used_memory:821528 used_memory_human:802.27K
通過info返回的信息我們可以發(fā)現(xiàn)在執(zhí)行rename之后redis將大key test大小為300多兆的值對象直接刪除并回收掉了,而redis刪除一個key的時間復雜度是O(M),在這里M是被刪除的成員數(shù)量---500w。應該就是這個"隱式"刪除操作導致了高延遲的產生。
文檔我們看看官方文檔是怎么描述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會用key的值覆蓋掉newkey的值,而newkey原本的值會被redis隱式地刪除。我們知道大key的刪除伴隨著高延遲(redis是單進程服務,服務器會在刪除大key期間block住接下來其他命令的執(zhí)行),這就導致時間復雜度本為O(1)的rename也有可能卡住redis。
這句官方文檔的原話我沒在其他文檔里找到類似的翻譯,看這些文檔的開發(fā)者可能會誤以為這是個特別安全的O(1)命令。
既然文檔里已經說明了這種行為的存在,我就順便看看源碼這塊邏輯是怎么走的:
源碼分析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的值對象地址復制給o return; ... incrRefCount(o); //舊key的值對象引用計數(shù)+1(被o引用) if (lookupKeyWrite(c->db,c->argv[2]) != NULL) { //如果新key已經有值對象了 ... dbDelete(c->db,c->argv[2]); //新key從db中移除、并將新key的值對象引用計數(shù)-1(變?yōu)?),并釋放內存 } dbAdd(c->db,c->argv[2],o); //將新key => 舊key的值對象的組合放入db中 ... dbDelete(c->db,c->argv[1]); //舊key從db中移除、并將舊key的值對象引用計數(shù)-1(不會變?yōu)?),不釋放內存 ... }
正常O(1)重命名的邏輯不用多說,涉及到覆蓋的過程可以簡化成如下圖:
在改變指針的指向之前,redis會先用if (lookupKeyWrite(c->db,c->argv[2]) != NULL)判斷newkey是否有對應的值,若有 則調用dbDelete(c->db,c->argv[2]);將newkey的值v2刪掉。
結論用redis的時候,keys、 hgetall、 del 這些命令我們會多加小心,因為不合理地調用它們可能會長時間block住redis的其他請求 甚至導致CPU使用率居高不下從而卡住整個服務器。但其實rename這個不起眼的命令也可能造成一樣的問題,使用時需要謹慎對待。
參考資料RENAME – Redis
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/29641.html
摘要:查看擴展探針擴展相關函數(shù)管理擴展配置下安裝流程下載對應的版本一定要選擇正確下載擴展的版本下載地址選擇對應的版本版本版本在中開啟擴展,配置擴展相關的參數(shù)有需要的話重啟服務器下安裝流程直裝流程把相應的擴展移動到你的文件夾下面然后在中開啟相應的擴 1 查看php擴展(1)phpinfo 探針(2)php擴展相關函數(shù)get_loaded_extensions() arrayextension...
閱讀 2984·2023-04-26 00:23
閱讀 3399·2021-09-13 10:28
閱讀 2178·2021-08-31 14:18
閱讀 2885·2019-08-30 15:54
閱讀 1939·2019-08-30 15:43
閱讀 1276·2019-08-29 16:56
閱讀 2800·2019-08-29 14:16
閱讀 2054·2019-08-28 17:51