当前位置: 首页 > 后端技术 > PHP

Redis随笔-重命名效率问题

时间:2023-03-30 01:18:22 PHP

后台rename是redis中重命名一个key的命令,renamekeynewkey就是将key重命名为newkey。大部分文档在介绍rename的时候,只把它描述成一个时间复杂度为O(1)的命令,却忘了说明它可能带来的性能问题(时间复杂度应该是O(1,涉及覆盖旧值时))+O(M))。让我们做一个实验来看看重命名问题。现象先搭建一个版本号为3.2的redis服务器,查看其内存信息127.0.0.1:8401>infomemory#Memoryused_memory:842416used_memory_human:822.67K然后用lua为redis创建一个名为test的大key,test有500w字段,每个字段的值为1127.0.0.1:8401>eval"fori=1,5000000,1doredis.call('hset','test',i,1)end"0(nil)(11.61s)127.0.0.1:8401>hlentest(integer)5000000这个时候再看看redis的内存使用情况127.0.0.1:8401>infomemory#Memoryused_memory:381185592used_memory_human:363.53M由于创建了一个大key测试,redis内存使用超过300兆字节。接下来我们创建一个临时key,用它来重命名bigkeytest127.0.0.1:8401>settmp1OK127.0.0.1:8401>renametmptestOK(2.36s)然后可以看到执行时间的异常是的,rename的执行时间长达2.36秒。为什么是这样?再来看看redis的内存使用情况:127.0.0.1:8401>infomemory#Memoryused_memory:821528used_memory_human:802.27K通过info返回的信息我们可以发现,在执行rename之后,redis直接将值对象转换成一个largekeytestsizeofmorethan300megabytesDeletedandrecycled,redis删除一个key的时间复杂度为O(M),其中M为删除成员数---500w。应该是这种“隐式”删除操作导致了高延迟。Documentation我们来看看官方文档是如何描述rename这个行为的:RENAMEkeynewkey将key重命名为newkey。当键不存在时,它会返回错误。如果newkey已经存在,它会被覆盖,当这种情况发生时,RENAME会执行一个隐式的DEL操作,所以如果删除的key包含一个非常大的值,即使RENAME本身通常是一个常量时间操作,它也可能导致高延迟。如果newkey已经存在,redis会用key的值覆盖newkey的值,newkey原来的值会被redis隐式移除。我们知道大key的删除伴随着高延迟(redis是单进程服务,删除大key的过程中服务器会阻塞其他命令的执行),这就导致renamewithatime的可能性O(1)的复杂性。卡住了redis。我没有在其他文件中找到与此官方文件原句类似的翻译。阅读这些文档的开发人员可能会误以为这是一个特别安全的O(1)命令。既然文档中已经说明了这种行为的存在,那我就来看看源码的工作逻辑:源码分析db.cvoidrenameCommand(client*c){renameGenericCommand(c,0);}voidrenameGenericCommand(client*c,intnx){robj*o;...if((o=lookupKeyWriteOrReply(c,c->argv[1],shared.nokeyerr))==NULL)//复制旧key的值对象地址给oreturn;...incrRefCount(o);//旧键值对象引用计数+1(被o引用)if(lookupKeyWrite(c->db,c->argv[2])!=NULL){//如果新键已经有值对象...dbDelete(c->db,c->argv[2]);//新key从db中移除,新key的值对象引用计数为-1(变为0),释放内存}dbAdd(c->db,c->argv[2],o);//将新键=>旧键值对象的组合放入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等命令,因为不合理地调用它们可能会长时间阻塞redis的其他请求,甚至会导致CPU占用率过高而阻塞整个服务器。但实际上,不起眼的命令rename也可能导致同样的问题,所以在使用时需要小心。好在Redis4.0之后,Redis提供了异步删除的方法,只需要在配置文件中加上lazyfree-lazy-server-delyes即可。原理和unlink一样,把释放内存的操作丢给另一个线程。这避免了重命名大密钥的效率问题。参考重命名——Redis