资讯详情

Redis中DEL与UNLINK的区别:大key删除如何避免阻塞?

📅 2026/9/11 15:12:36 | 华诺云谱 👁 阅读
Redis中DEL与UNLINK的区别:大key删除如何避免阻塞?
先聊个我最近排查的线上问题。某个业务在做定时清理任务把一批不再使用的会话数据从Redis里删掉结果删除操作直接把Redis主线程卡了接近两秒。查了一圈罪魁祸首就是一条DEL命令删的是一个包含几十万字段的Hash key。后来我把删除命令从DEL换成了UNLINK问题立刻消失整个清理过程对线上几乎零影响。这个案例很典型也正好回应了很多人问过我的一个问题Redis里删key用del和unlink到底有什么区别这两条命令表面上看都是删除但背后的执行机制完全不同选错了在特定场景下是真的会出事故的。这篇文章我就从原理、执行流程、适用场景和实战配置这几个维度把这两个命令彻底讲清楚顺便把我自己踩过的坑和排查经验一并分享出来。1. 先从两条命令的“表面区别”说起1.1 del和unlink的基本行为先看最直观的层面。DEL是Redis从最开始就有的删除命令它的语义是同步删除一个或多个key。当执行DEL key时Redis会在主线程里立刻找到这个key对应的对象释放它占用的内存然后返回删除了多少个key。UNLINK是Redis 4.0引入的命令官方文档里的描述是“异步删除key”。所谓的异步是指UNLINK命令执行时Redis会把key从键空间中摘除然后返回成功但真正释放内存的操作被放到后台线程去处理。也就是说UNLINK执行完的那一刻这个key对客户端已经不可见了但它的内存可能还在。在命令行里看两条命令的返回区别也很明显127.0.0.1:6379 SET user:1001 zhangsan OK 127.0.0.1:6379 DEL user:1001 (integer) 1 127.0.0.1:6379 SET user:1002 lisi OK 127.0.0.1:6379 UNLINK user:1002 (integer) 1表面上看起来结果是一样的都是返回1表示成功删除了1个key。如果删除一个不存在的key两条命令也都返回0127.0.0.1:6379 DEL not_exist_key (integer) 0 127.0.0.1:6379 UNLINK not_exist_key (integer) 0正因为从返回值和命令格式上看UNLINK和DEL几乎一模一样很多人才会误以为它们只是名称不同、功能完全等价。我在一些项目里甚至见过有人统一用UNLINK替代DEL理由是“听说UNLINK更牛”。这种理解其实是不完整的因为两者的执行模型和适用场景有本质差异。1.2 返回值的差异与语义变化虽然返回结果相同但要留意一个语义层面的差异。DEL返回的是实际被删除的key数量这个数字是准确且即时的因为DEL确实在返回前把内存释放干净了。UNLINK返回的“1”则更像是一个确认信号它告诉你Redis已经接受了这个删除请求并且把这个key从键空间目录里移除但不代表内存立即被回收。为了严谨验证这点我自己在测试环境里做过一个实验。构造一个包含一百万个元素的Hash key然后分别用DEL和UNLINK删除观察命令耗时和内存变化。DEL执行耗时大约是几百毫秒命令返回后立刻用INFO memory查看used_memory内存已经降下去了。UNLINK执行耗时不到一毫秒命令返回后立刻查看used_memory内存基本没变然后过一小段时间才会逐步下降。这就引出一个重要结论在“删除”这个动作的完成度上DEL是彻底完成UNLINK是“逻辑删除延迟回收”。对于普通的、很小的keyUNLINK会在内部直接走同步释放逻辑此时和DEL没有太大区别。对于大keyUNLINK的延迟回收机制才真正发挥作用。1.3 一个容易忽视的细节unlink不保证立即释放内存很多人第一次用UNLINK时都会犯一个“经验主义错误”执行完UNLINK之后马上看内存发现used_memory没变就以为命令没生效然后又去执行DEL结果反而把主线程卡住了。这里必须说清楚UNLINK在遇到大key时是把“释放内存”这个耗时操作交给了后台线程后台线程回收内存是有时间成本的。回收速度取决于key的大小、内存分配器的表现、系统当前的负载等。如果你的key非常大比如一个包含上千万元素的Set后台回收也可能需要几百毫秒甚至更久但这个过程中Redis主线程不会阻塞客户端体验不受影响。所以不要用“内存立刻下降”这个标准去验证UNLINK是否生效。判断UNLINK是否真正生效应该看Redis是否还存在这个key以及后面会讲到的异步删除统计指标。2. 为什么Redis要提供unlink单线程模型与大key之痛2.1 Redis单线程执行模型回顾要理解UNLINK的价值必须先理解Redis的执行模型。Redis的核心命令处理是单线程的也就是说同一时刻只有一个命令在执行其他命令都在排队等待。这个模型让Redis的实现变得简单也避免了大量并发锁竞争的问题性能极高但也带来一个致命弱点任何一个命令执行时间过长都会阻塞后面所有命令。打个比方Redis就像一个人在一个窗口处理业务正常业务一两秒就能办完队伍排得再长也能快速消化。但如果来了一个超级耗时的业务这个人就得一直处理它后面的所有人都得等着。DEL删大key就是那个超级耗时的业务。我遇到过最夸张的一个案例是一个用来存储用户标签的Set key里面有大概三百万个元素。当时用DEL删除命令执行花了1.8秒。这1.8秒内这个Redis实例上所有读写请求全部阻塞包括一些核心业务的数据读取导致接口超时报警一片。当时看到监控图上那条笔直的横线我就知道是主线程被卡住了。2.2 大key删除为什么是“定时炸弹”那么问题来了为什么DEL一个包含很多元素的集合类型key会这么慢Redis里的大key通常指的是单个key对应的value非常大。可以是简单的String类型但value特别长比如一个几MB的字符串更常见的是集合类型比如List里有几十万条记录Hash里有几十万个字段Set里有几百万个成员ZSet里有几十万个带分数的成员。DEL删除这些集合类型时Redis需要遍历这个集合逐个释放每个元素的内存。释放内存不是简单地把指针丢掉而是要调用内存分配器去归还内存这个过程本身就有开销。几百万个元素就意味着几百万次内存释放操作累计起来就是秒级延迟。这种大key在日常开发中很容易被忽略。很多业务初期数据量小从来没出过问题。等业务跑了一两年某些key攒了几百万个字段某个清理任务一触发DELRedis瞬间就卡死。这就是一颗定时炸弹你不知道它什么时候会炸。更麻烦的是大key在Redis里不仅影响DEL。读写大key时比如HGETALL一个几十万字段的Hash或者LRANGE一个很长很长的List也会产生大流量和阻塞风险。但删除操作是最容易被忽视的一个触发点因为它看起来太平常了。2.3 unlink背后的lazyfree机制原理UNLINK之所以能解决大key删除阻塞问题核心在于Redis引入了一套lazyfree机制直译就是“懒释放”。所谓懒释放就是不立刻释放找机会再释放。具体流程是这样的客户端发送UNLINK key命令到Redis。Redis在主线程中执行UNLINK逻辑它会先判断这个key对应的对象是否“值得异步释放”。如果key很小Redis会直接在主线程里同步释放内存效果等同于DEL。如果key很大Redis会将这个key对应的对象放入后台释放队列主线程立即返回。后台会有专门的bio线程background I/O线程从队列中取出对象真正地遍历并释放内存。这里有个关键点Redis判断“大”和“小”的标准是什么实际上Redis内部定义了一个宏判断依据是对象中元素的数量是否超过一定阈值。如果元素个数很少直接同步释放如果元素个数很多就异步释放。这个阈值在源码里有默认是64也就是说只有元素数量大于64的集合类型key才会真正走异步释放路径。对于String类型由于释放内存本身速度很快通常直接同步释放。我最初研究这块时也觉得奇怪为什么是64这个数字后来想明白了释放内存的开销和元素数量正相关64个元素以内的集合释放操作耗时极短走异步反而增加线程切换和队列调度的开销得不偿失。Redis做事很实在小key就直接顺手干了。另外要说明一点UNLINK也不是“完全异步”。它本质上是一条主线程命令只是命令内部做了一个决策小key自己干大key交给后台。所以UNLINK本身执行几乎不会阻塞但后台线程如果积压了太多待释放对象内存回收会滞后可能需要一段时间内存才会降下来。3. 什么场景该用unlink什么场景继续用del3.1 实战选型看key大小和业务容忍度聊完原理自然到了最核心的问题我到底该用DEL还是UNLINK我的经验是分场景来看不能无脑选。对于绝大多数普通key比如用户会话、临时状态、计数器这些key的value都很小用DEL或UNLINK差别可以忽略不计。统一用UNLINK也不是不行因为小key本身会走同步释放不会带来额外负担。但要注意如果业务中对删除有严格的时序依赖比如删除后必须在同一事务里立刻读不到那UNLINK也是满足的因为它会先把key从键空间摘除逻辑上已经不可见了。真正需要谨慎选择的是大key场景。如果你的Redis里存在大key一定要用UNLINK。典型的大key包括缓存某个用户的所有好友ID的Set可能上百万成员。记录某个商品的所有评价ID的List可能几十万条。存储全量设备信息的Hash可能几十万字段。作为消息队列使用的List积压了几百万条消息。这些key在删除时用DEL几乎必然导致长时间阻塞。正确的做法是使用UNLINK把耗时的内存回收丢给后台线程。我在实际项目中定过一个简单的规范凡是删除集合类型的key如果预估元素数量可能超过一万一律使用UNLINK如果业务无法预估key大小也统一用UNLINK反正小key它自己会走同步路径。唯一的例外是如果在某些极端情况下你需要删除key后立刻确保内存已经释放且实例承载的QPS很低此时可以手动评估后使用DEL。这种需求在实际业务中非常少见一般来说UNLINK足够。3.2 集群、主从与持久化场景下的差异分布式环境下DEL和UNLINK的区别就更值得留意了。在Redis Cluster集群模式下这两条命令都要求key落在同一个节点不能跨key批量操作其他方面的行为与单机一致。这里说的行为一致是指客户端视角的逻辑删除和后台异步释放与集群架构无关。在主从复制架构下要特别小心。主节点执行UNLINK后会生成一条DEL命令传播给从节点。注意是DEL不是UNLINK。这意味着什么意味着主节点用异步方式释放内存但从节点收到DEL后依然会在自己的主线程里同步释放内存。如果从节点上这个key也很大从节点一样会被卡住。这点非常关键很多人只关注主节点忽略了从节点。我在生产环境就遇到过这种怪事主节点切换使用UNLINK后主节点一点不卡了但从节点每隔一段时间就出现一次延迟飙升节点监控图上沿着时间轴出现规则的尖峰。后来查从节点日志发现就是大key在从节点上同步删除导致的。所以在主从架构里判断是否安全卸载大key不能只看主节点的处理方式还要评估从节点的状态。如果条件允许可以先在从节点上把大key通过UNLINK删掉再在主节点上删但这需要业务的配合比较复杂。更实际的做法是提前规划大key的清理在低峰期执行并且监控所有从节点的延迟。还有一个容易忽略的点是持久化。Redis开启AOF持久化时UNLINK带来的实际效果是写一条DEL命令到AOF文件之后AOF重写时会基于当前数据集生成新的AOF文件不会把那条DEL之前的大key数据保留下来。RDB持久化则是在快照时记录当前数据集状态大key在快照期间还是会被完整序列化到RDB文件中。也就是说UNLINK不会让持久化文件中自动省略大key的历史数据只有在AOF重写或新的RDB快照之后大key才真正从持久化文件里消失。3.3 别把unlink当万能药虽然UNLINK解决了大key删除阻塞的大问题但它不是万能药。我见过不少同学把UNLINK当作Redis的“免死金牌”以为只要删除时用了UNLINK就万事大吉结果其他地方照样出问题。首先要明确大key的问题不只是删除时产生的。如果一个大key一直在被读取比如频繁执行HGETALL、SMEMBERS、LRANGE 0 -1每一次全量读取都会产生大量网络流量和序列化时间同样可能拖垮主线程。UNLINK只解决了删除阶段的阻塞解决不了大key的读写放大问题。另外UNLINK只适用于删除整个key。如果你只想删除一个大key中的部分元素比如从一个百万成员的Set里移除几万个成员UNLINK帮不上忙因为SREM、HDEL、ZREM这些部分删除操作依然会同步执行元素量太大时也照样会阻塞。这种场景正确的做法是分批删除用每次删一小批的方式把阻塞时长控制在可接受范围内。还有一点UNLINK不会降低大key在内存中的存储开销。它只是在删除时把释放动作延后减少对主线程的影响。如果业务里的大key本身是长期存在的需要的是拆key、换数据结构或者设置合理过期时间而不是指望删除时做个异步就解决问题。4. 关联的异步化配置与监控4.1 lazyfree相关配置项解析UNLINK只是lazyfree机制的一部分Redis中还有其他几条路径也支持异步化释放都是通过配置开启的。这些配置在生产环境里同样值得关注。相关配置项主要是这几个都是可以在redis.conf里设置也可以通过CONFIG SET命令动态修改配置项默认值作用lazyfree-lazy-evictionno当Redis内存达到maxmemory上限触发淘汰策略时是否用异步方式释放被淘汰的keylazyfree-lazy-expireno过期key被定时任务清理时是否用异步方式释放lazyfree-lazy-server-delno某些隐含删除操作比如rename覆盖旧key是否用异步方式释放replica-lazy-flushno从节点在SYNC全量同步时是否用异步方式清空旧数据这几个配置我建议在涉及大key的实例上开启。因为除了显式的DEL命令Redis还有很多隐性的删除场景。比如内存淘汰如果maxmemory策略是allkeys-lru当内存满了Redis会在主线程里逐步淘汰key如果正好选中了一个大keyDEL式的同步释放还是会造成卡顿。开启lazyfree-lazy-eviction后淘汰大key的操作也会转入后台。过期key清理同理。Redis默认每100毫秒做一次过期key扫描如果扫描到很多大key过期同步清理也会带来明显延迟。开启lazyfree-lazy-expire后这部分清理动作变为异步。不过要注意开启异步淘汰和异步过期后内存回收的时机变得不确定。在高写入压力下如果一边不断触发淘汰一边后台线程来不及释放可能会出现内存暂时超过maxmemory的情况。官方对这块没有给出强约束实际生产中需要结合监控观察我个人建议这些配置在大多数场景下可以打开但一定要配上内存和后台释放积压量的监控方便及时发现问题。4.2 如何确认异步删除真的发生了说了这么多那怎么确认一条UNLINK命令到底有没有走异步释放路径呢总不能靠猜。其实Redis提供了一些统计指标最直接的一个是INFO stats里的lazyfree_pending_objects它表示当前正在等待后台线程释放的对象数量。执行UNLINK删除一个大key后立刻查看这个指标如果数值大于0说明确实走了后台异步释放。过一会儿再查数值应该回落到某个更小的数说明后台线程已经逐步回收了对象最终如果积压队列被清空这个值会回到0。一个比较实用的排查流程是这样的# 先构造一个大key 127.0.0.1:6379 EVAL for i1,200000 do redis.call(hset,KEYS[1],i,i) end 1 big_hash (nil) # 确认当前积压对象数 127.0.0.1:6379 INFO stats | grep lazyfree lazyfree_pending_objects:0 # 执行UNLINK 127.0.0.1:6379 UNLINK big_hash (integer) 1 # 立刻查看积压对象数应该大于0 127.0.0.1:6379 INFO stats | grep lazyfree lazyfree_pending_objects:1 # 稍等片刻再查看应该回到0说明后台释放完成 127.0.0.1:6379 INFO stats | grep lazyfree lazyfree_pending_objects:0这个指标对排查问题很有价值。如果lazyfree_pending_objects持续很高说明后台线程处理不过来可能的原因包括短时间内删了太多大key、后台线程数不足、系统IO调度异常等。此时虽然主线程没被阻塞但内存释放滞后可能带来内存水位升高和OOM的风险。另外一个辅助手段是查看慢日志。虽然UNLINK本身不太会成为慢命令但DEL删小key偶尔也会因为某些特殊对象构造进入慢日志。通过SLOWLOG命令可以看哪些删除命令耗时较长反推数据结构是否合理。4.3 生产环境实操建议根据我自己的运维经验给出一套比较稳妥的落地建议第一先把实例的Redis版本升级到4.0以上。UNLINK和lazyfree机制是4.0引入的如果你的版本停留在3.x后面这些内容都不用看了。Redis 7.x对lazyfree还有一些细节优化比如在AOF重写时对异步释放的key处理更优雅建议有条件的话用较新的稳定版本。第二梳理业务里的大key清单。用redis-cli的--bigkeys参数可以扫描出大key或者用SCAN命令配合TYPE和STRLEN/HLEN/LLEN等命令自己写个小脚本。这一步的目的是建立大key台账知道哪些key有删除风险。第三制定删除规范。我一般在项目文档里写清楚单个key元素量预估超过一万或者value大小超过10MB删除时必须用UNLINK未知大小的key删除时统一使用UNLINK需要确保内存立即释放的场景先评估再决定。核心是让团队成员都有一致的判断标准。第四开启lazyfree相关配置并配套监控。至少要监控lazyfree_pending_objects这个指标建议和Redis内存使用率、主线程延迟放在同一个监控面板上。一旦出现积压持续增长要能第一时间发现。第五定期演练大key清理。不要等线上出故障了再临场想办法。可以在测试环境模拟一个百万元素的key分别用DEL和UNLINK删除对比耗时和内存变化。团队里每个人都亲手试一遍比看十篇文章都管用。5. 常见问题与排查实录5.1 为什么unlink之后内存没有立刻下降这个问题我在前面已经提过但这里想再系统地说一下。执行UNLINK后如果key很大内存释放确实需要时间。这个“时间”取决于几个因素一是后台线程的调度。Redis的后台bio线程是共享的不只处理内存释放还要处理AOF刷盘等任务。如果AOF刷盘压力大内存释放的优先级可能被挤占。二是内存分配器的行为。Redis默认使用jemallocjemalloc释放内存时不一定立刻把内存归还给操作系统而是可能缓存在内存池里复用。所以即使Redis内部已经释放了对象used_memory这个指标也可能不会立刻降下去。这是正常现象不用慌。三是并发环境。如果同一时间有很多UNLINK和DEL在执行后台队列里可能积压多个待释放对象处理完需要一点时间。如果是删除大key后很久内存都没降下来建议先看lazyfree_pending_objects确认是否还有积压再看系统层面的内存占用排除是否有内存碎片的问题可以通过INFO memory里的mem_fragmentation_ratio判断数值明显大于1.5时可以考虑开启activedefrag。5.2 del和unlink对过期key、淘汰策略的影响这个问题其实比看起来复杂。DEL是所有删除操作里最“激进”的它不区分key是手动删除还是自然过期。Redis处理过期key的方式有两种惰性删除和定期删除。惰性删除发生在访问key时发现已过期立即删除定期删除是后台每100毫秒抽查一批key删除其中过期的。这两种方式默认都是同步删除。如果你没有开启lazyfree-lazy-expire那么一个包含几十万元素的大key到了过期时间定期删除在扫描到它时会同步释放内存同样会阻塞主线程。这个问题的隐蔽性在于它和你手动执行DEL的时机完全无关是Redis自己触发的你甚至不知道什么时候会发生。生产环境里我见过一些诡异的延迟尖峰最后发现是大量带过期时间的Hash key在同一时间段集体过期导致的。解决方案有两个方向一是开启lazyfree-lazy-expire让过期key的清理也走异步二是尽量避免在同一个时间窗口内让大量大key集中过期可以在设置过期时间时加一个随机偏移量把过期时间打散。这两种方法可以同时用。淘汰策略这块我前面也提到了lazyfree-lazy-eviction。这里补充一个细节Redis的内存淘汰是发生在新key写入时如果内存已达上限写入命令会先触发淘汰再执行写入。如果淘汰的是一个大key且走同步释放这个写入命令就会被拖慢。在高并发写入场景下这会导致写入QPS大幅波动。开启异步淘汰后写入命令可以快速返回但后台释放跟不上时会短暂出现内存超过maxmemory的情况。实操中我会给maxmemory留一点余量比如配置为物理内存的70%到80%给异步释放留出缓冲空间。5.3 我在生产环境踩过的坑最后分享几个我亲身踩过的坑希望后来者少走弯路。第一个坑用DEL删除大key导致集群节点切换。那次是承接一个老项目的Redis集群发现有一个Set key的成员数量到了一个惊人的数字要清理掉。当时经验不足直接用DEL执行结果命令执行了接近三秒主节点发生了故障转移原因就是主节点疑似阻塞被哨兵判定为不可用。那次故障后我才开始认真研究UNLINK。第二个坑主从架构下从节点同步卡顿。有一次我在主节点上用UNLINK删除了一个大key主节点很丝滑一点感觉都没有。但过了几秒钟从节点报延迟报警随后发生主从切换。原因我在前面已经解释过主节点会把UNLINK转换为DEL同步给从节点从节点在同步执行DEL时又被卡住了。那次之后我学会了在评估删除影响时一定要把从节点一起纳入评估。第三个坑把UNLINK当成异步删除后内存立刻释放。有一次我执行完UNLINK后业务系统马上发了内存告警我一度以为UNLINK出了问题。后来才明白是后台异步释放还没完成加上那天恰好有其他大key在写入内存自然还在高位。等后台线程处理完内存就恢复正常了。现在我会在运维手册里明确写上UNLINK不保证删除后内存立刻下降这是正常现象关键是看lazyfree_pending_objects和整体内存趋势。第四个坑在事务和Lua脚本里用UNLINK。Redis事务和Lua脚本要求命令在执行过程中不能穿插后台操作。UNLINK虽然本身不会在事务或脚本里被禁止但它的异步释放行为会影响你对“删除完成”的判断。比如你在Lua脚本里UNLINK一个大key然后立刻判断key是否存在会发现key已经不存在了这没问题。但如果脚本里依赖“删除后内存已释放”这个假设就可能埋下隐患。一般情况下事务和脚本内建议还是用DEL因为它们本身就是原子执行的把阻塞风险控制在外层更好。第五个坑忘记开启lazyfree相关配置。其实UNLINK命令本身不需要任何配置就能用但lazyfree-lazy-expire和lazyfree-lazy-eviction这些配置默认是关闭的。如果只改了业务代码里的DEL为UNLINK而忽视了过期key和淘汰key的异步化配置大key引发的阻塞风险依然存在。我把这几个配置项列成了一张检查清单每次搭建新环境时都会对照检查一遍。5.4 常见问题速查表把上面提到的常见问题统一整理成一张速查表方便日常排查对照问题现象可能原因排查思路解决方法DEL删除大key时主线程卡顿同步释放内存耗时过长通过慢日志、命令耗时统计定位到大key改用UNLINK或分批删除UNLINK后内存不下降后台异步释放未完成或jemalloc缓存查看lazyfree_pending_objects和内存趋势等待后台释放观察指标回落主节点不卡从节点延迟飙升主节点UNLINK传播为DEL给从节点查看从节点慢日志和延迟监控在从节点先行清理大key或低峰期清理大量key集中过期导致延迟尖峰过期key清理走同步路径查看监控中过期key数量和延迟曲线开启lazyfree-lazy-expire设置过期时间随机偏移内存淘汰时写入变慢淘汰大key时同步释放查看淘汰策略和淘汰key大小开启lazyfree-lazy-eviction预留内存缓冲区后台积压对象数持续增长短时间内删除大量大key监控lazyfree_pending_objects分批删除降低删除频率最后再说一个我自己到现在都还在用的习惯。每次批量清理Redis数据前我都会先用SCAN配合TYPE和对应的长度命令扫一遍打算删除的key评估它们的元素量级。如果发现有大key就先UNLINK如果小key居多DEL也无妨。这个习惯让我很少在删除环节翻车。Redis的del和unlink这对命令看起来只是单词不同背后却是两种完全不同的设计哲学。理解了它们的工作原理再遇到大key删除、主从卡顿、内存回收延迟这类问题你就能很快定位到根因不会再被表象迷惑。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。