背景
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
tonewkey
. It returns an error whenkey
does not exist. Ifnewkey
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
这个不起眼的命令也可能造成一样的问题,使用时需要谨慎对待。
好在Redis4.0之后,Redis提供了异步删除的方法,仅需在配置文件里加上lazyfree-lazy-server-del yes
即可,原理与unlink
一样,释放内存的操作丢入了另一个线程里。这样就避免了rename
大key的效率问题。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。