Redis隨筆-rename效率問題


背景

rename是redis中給key重命名命令,rename key newkey的意思就是將key重命名為newkey。
大部分文檔在介紹rename的時候只將它描述成一個時間復雜度為O(1)的命令,卻忘了說明它可能導致的性能問題(涉及覆蓋舊值的時候 時間復雜度應該是O(1)+O(M))。

我們先做個試驗看看rename的問題。

現象

先搭建一個redis服務器,版本號為3.2,看看它的內存信息

127.0.0.1:8401> info memory
# Memory used_memory:842416 used_memory_human:822.67K

接着用lua給redis創建一個名為 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的創建,redis內存占用多了300多兆。
接下來我們創建一個臨時key,並用它來rename掉大key test

127.0.0.1:8401> set tmp 1 OK 127.0.0.1:8401> rename tmp test OK (2.36s)

這時就能看到執行時間的異常了,rename執行時間長達2.36秒,這是為什么呢?我們再看看redis內存占用情況:

127.0.0.1:8401> info memory
# Memory used_memory:821528 used_memory_human:802.27K

通過info返回的信息我們可以發現在執行rename之后redis將大key test大小為300多兆的值對象直接刪除並回收掉了,而redis刪除一個key的時間復雜度是O(M),在這里M是被刪除的成員數量---500w。應該就是這個"隱式"刪除操作導致了高延遲的產生。

文檔

我們看看官方文檔是怎么描述rename這一行為的:

RENAME key newkey

Renames 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住接下來其他命令的執行),這就導致時間復雜度本為O(1)的rename也有可能卡住redis。

這句官方文檔的原話我沒在其他文檔里找到類似的翻譯,看這些文檔的開發者可能會誤以為這是個特別安全的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的值對象引用計數+1(被o引用) if (lookupKeyWrite(c->db,c->argv[2]) != NULL) { //如果新key已經有值對象了 ... dbDelete(c->db,c->argv[2]); //新key從db中移除、並將新key的值對象引用計數-1(變為0),並釋放內存 } dbAdd(c->db,c->argv[2],o); //將新key => 舊key的值對象的組合放入db中 ... dbDelete(c->db,c->argv[1]); //舊key從db中移除、並將舊key的值對象引用計數-1(不會變為0),不釋放內存 ... }

正常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這個不起眼的命令也可能造成一樣的問題,使用時需要謹慎對待。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM