背景

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这个不起眼的命令也可能造成一样的问题,使用时需要谨慎对待。

Redis随笔-rename效率问题的更多相关文章

  1. [Database] Redis 随笔

    Redis 随笔 1. 特点 非关系数据库 non-relational database 内存数据库 高性能 主从复制 可持久化存储 发布与订阅 支持脚本 2. 数据类型5种 STRING 可以是字 ...

  2. redis为何单线程 效率还这么高 为何使用跳表不使用B+树做索引(阿里)

    如果想了解 redis 与Memcache的区别参考:Redis和Memcache的区别总结 阿里的面试官问问我为何redis 使用跳表做索引,却不是用B+树做索引 因为B+树的原理是 叶子节点存储数 ...

  3. 本地缓存google.guava及分布式缓存redis 随笔

    近期项目用到了缓存,我选用的是主流的google.guava作本地缓存,redis作分布式 缓存,先说说我对本地缓存和分布式缓存的理解吧,可能不太成熟的地方,大家指出,一起 学习.本地缓存的特点是速度 ...

  4. Redis随笔(四)Centos7 搭redis3.2.9集群-3主3从的6个节点服务

    1.虚拟机环境 使用的Linux环境已经版本: Centos 7   64位系统 主机ip: 192.168.56.180 192.168.56.181 192.168.56.182 每台服务器是1主 ...

  5. Redis随笔(三)主从搭建

    1.安装redis cd /root/svr/wget http://download.redis.io/releases/redis-3.2.9.tar.gz tar -zxvf redis-3.2 ...

  6. Redis随笔(二)redis desktop manager 安装并且连接redis服务器

    1.首先在win10下安装redis desktop manager 2.查看虚拟机防火墙状态,启动状态,则关闭掉 查看防火墙状态: systemctl status firewalld.servic ...

  7. Redis随笔(一)Linux Redis 搭建

    1.到官网下载redis上传服务器或者使用wget 下载 wget redis下载的路径 2.查看linux是否安装编译环境gcc,没有先安装 yum -y install gcc 3.解压redis ...

  8. Redis随笔

    dump.rdb:快照文件 删除这个文件 rm -f dump.rdb 第一步:创建6个redis实例,端口号从7001~7006 第二步:修改redis的配置文件 1.修改端口号 修改redis.c ...

  9. Redis随笔(六)RESP的协议规范

    1.官网文档 https://redis.io/topics/protocol http://www.redis.cn/topics/protocol.html 2.协议介绍 redis协议规范(Re ...

随机推荐

  1. c# 利用MailKit.IMap 收取163邮件

    最近我要做一个爬虫.这个爬虫需要如下几个步骤: 1 填写注册内容(需要邮箱注册) 2 过拖拽验证码(geetest) 3 注册成功会给邮箱发一封确认邮箱 4 点击确认邮箱中的链接 完成注册 我这里就采 ...

  2. gdb强制生成core文件

    如何为自己的进程产生core 文件,又不想退出这个进程? 系统只在程序崩溃退出时自动产生core file. 有的人像自己处理异常信号,然后自己产生一个core file,然后继续运行.那该怎么办呢? ...

  3. 正则表达式识别js跳转的链接

    用Webclient访问链接后返回了跳转页面,页面代码如下,需要把 http://kd.szty56.com:8086/xms/client/order_online!print.action?use ...

  4. Xshell连接不上虚拟机提示ssh服务器拒绝了密码,请再试一次

    1. 设置ubuntu的管理员root的密码 hughes@hughes-virtual:~$ sudo passwd (供xshell连接时使用) 2. 确保源文件和系统已更新 hughes@hug ...

  5. Bootstrap-常用图标glyphicon

    <!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="utf-8& ...

  6. spark2.2.1 sql001

    package sql import org.apache.spark.sql.SparkSession import org.apache.spark.SparkContext object Par ...

  7. Android -- 仿小米锁屏画报

    1,首先看一下我们今天实现的效果,效果图如下: 2,首先说一下大体的实现思路,首先这个视图一共分为三层,最外层是一个RecyclerView,第二层是一个被虚化的ImageView,第三层是一个正常的 ...

  8. 大话设计模式C++ 适配器模式

    适配器模式:将一个类的接口转换成客户希望的另外一个接口.Adapter模式使得原来由于接口不兼容而不能一起工作的那些类可以一起工作. 角色: (1)Target:这是客户所期待的接口,Target可以 ...

  9. [转]js刷新父窗体

    浮层内嵌iframe及frame集合窗口,刷新父页面的多种方法   <script language=JavaScript>       parent.location.reload(); ...

  10. lua学习之循环求一个数的阶乘

    --第3题 利用循环求n的阶乘 --参数检查是否是自然数 function IsNaturalNumber(n) ~= )then return false else return true end ...