☰
Redis键值对安全批量删除:SCAN与UNLINK的工程化实践
2026/10/2 3:10:05 网站建设 项目流程

做后端的朋友应该都经历过这种场景:线上某个模块出了问题,往Redis里塞了一堆垃圾缓存,比如order:temp:*、cart:tmp_*、verify:code:*,业务一恢复,这些键就成了没人读没人管的历史遗留。我见过不少同事第一次遇到"按模式删键"这个问题时,第一反应就是在命令行敲DEL order:temp:*,结果当然是什么都删不掉——因为DEL命令接受的是具体键名,根本不会帮你做模式匹配。真正等到你需要批量清理一批包含特定模式的键时,几个关键问题就摆上来了:怎么安全地找出这些键?怎么在不影响线上服务的前提下删除?删错了怎么办?

这篇文章我会结合自己这些年实际踩坑和实操的经验,把"Redis键值对批量删除"这件事从头到尾讲透。内容主要围绕如何用SCAN扫描、如何用DEL/UNLINK删除、如何在单机和集群环境下安全操作、误删之后怎么补救这几个核心点展开,适合后端开发、运维、DBA以及刚接触Redis的初学者参考。读完你会发现,批量删键本身不难,难的是做得安全、高效、可回滚。我下面的每一个步骤都是自己在生产环境实际验证过的,按着做,能少走很多弯路。

1. 项目概述与方案选型:为什么不能直接"删删删"

1.1 需求场景拆解:什么情况下需要按模式删键

先说说我在实际工作中遇到过的几类典型场景,你可以对照一下自己是不是也有同样的需求。

第一类是缓存键前缀管理不严格导致的"垃圾键堆积"。比如系统里某个旧接口用了user:info:前缀做缓存,后来接口升级成了account:前缀,新旧两套键在Redis里共存。旧键没人读,但也不会自动消失,除非设置了过期时间。时间一长,上千个user:info:*键就占着内存。这种场景下你没法一个个删,因为键名中间可能带着用户ID,你不可能手写几千条DEL命令。

第二类是批量清理测试数据或者临时数据。联调环境、灰度环境里经常会生成大量临时键,比如session:tmp_20240101、import:batch:*。这些键往往有共同前缀,但过期时间设置得很远,不清理就会把测试环境的内存打满。我见过有人在测试环境里直接用FLUSHDB,这确实简单粗暴,但如果你只想清某个模块的数据,FLUSHDB会把其他业务的数据也一并干掉,风险极大。

第三类是数据迁移或者Key重命名之后的残留。很多团队会写一次性脚本把键从old:key:*迁移到new:key:*,迁移完成后旧键就成了废弃数据。这种场景通常涉及数量很大的键,而且键名模式非常明确,非常适合用模式匹配批量删除。

这几个场景的共同特点是什么呢?键名很长、数量很多、模式非常清晰。你去数一下可能涉及到几千甚至几万个键,手动删不现实,用FLUSHDB又太粗暴。这个时候"按模式匹配键名,再批量删除"就是最合理的手段。

1.2 核心取舍:KEYS 与 SCAN 的差异

聊批量删除,绕不开两个核心命令:KEYS和SCAN。两个命令都能按模式匹配键名,但使用场景完全不同。

KEYS的用法很简单,KEYS user:*就会一次性返回所有匹配的键。看起来很方便,但问题非常致命:Redis是单线程模型,所有命令都在主线程里排队执行。KEYS命令的时间复杂度是O(N),N是Redis里所有的键总数,而不是匹配到的键数量。也就是说,哪怕你的模式是user:*,它也会把整个键空间扫一遍。

我在一个键总量在500万左右的生产Redis实例上做过测试,执行KEYS *大概需要几十秒甚至更久。这期间Redis无法处理其他任何请求,所有读写都会被阻塞。在线上的高流量环境下,这就是一场事故。别说生产环境,就是测试环境键数量比较大的时候,执行KEYS *都能明显感觉到界面卡顿。

SCAN则完全不同。它采用的是增量迭代的方式,每次执行只返回一小批键,配合游标(cursor)不断推进,直到游标回到0才算遍历结束。SCAN每次执行的时间复杂度是O(1),不会阻塞Redis主线程,可以在线上环境安全使用。

打个比方:KEYS就像把整个仓库里的所有东西一次性拖出来清点,期间仓库门口全部堵住,谁也不能进出;SCAN则是拿着一本小小的手账本,每次只翻几页,记录一页就放行一批,仓库还能正常运转。

所以在批量删除这个场景下,正确思路必然是:SCAN遍历键名,DEL删除键。先通过SCAN把匹配的键逐步拿出来,再交给DEL去删,这样既不会阻塞Redis,也能精准地按模式清理。

2. 核心工具与命令细节解析

2.1 用 redis-cli + SCAN + xargs 组合:最经典的做法

redis-cli本身就内置了--scan选项,它的作用就相当于SCAN命令的客户端封装。最朴素的一条删除命令长这样:

redis-cli --scan --pattern "session:*" | xargs -L 100 redis-cli DEL

这条命令的思路很清晰:第一步,redis-cli --scan --pattern "session:*"把匹配到的键名一行一个输出到标准输出,第二步,通过管道交给xargs,每凑齐100个键就执行一次redis-cli DEL。

为什么要用-L 100?因为DEL命令支持一次删除多个键,比如DEL session:1 session:2 session:3。把键分成一批批删除,比一条命令只删一个键要高效得多。一个键一次网络往返和100个键一次网络往返,开销差距很明显的。

如果你的Redis不在本机,或者端口不是默认的6379,需要带上连接参数:

redis-cli -h 192.168.1.10 -p 6379 --scan --pattern "session:*" | xargs -L 100 redis-cli -h 192.168.1.10 -p 6379 DEL

这里有一个我必须提醒的坑:命令行传密码很容易被系统进程列表看到。如果你用-a参数传密码,执行ps aux的时候其他人都能看到你的Redis密码,这在生产环境是非常危险的行为。建议设置环境变量REDISCLI_AUTH:

export REDISCLI_AUTH='your_password' redis-cli --scan --pattern "session:*" | xargs -L 100 redis-cli DEL

这样密码就不会出现在命令行参数里了。

还有一个容易被忽略的问题:如果键名里包含空格或者换行符,用管道加xargs的方式会被空格把键名拆断,导致删除失败或者删错键。虽然Redis键名里带空格的情况很少见,但一旦出现就是灾难。如果你要操作的数据比较重要,更稳妥的方式是先把键名列表导出到文件,再用xargs -a读取文件进行分批删除。

# 第一步:导出键名 redis-cli --scan --pattern "session:*" > /tmp/session_keys.txt # 第二步:统计数量 wc -l /tmp/session_keys.txt # 第三步:确认无误后分批删除 xargs -a /tmp/session_keys.txt -L 100 redis-cli DEL

这样做的另一个好处是:删除前你有机会先看看键的数量,如果和你预期差太多,就说明模式可能写错了,可以及时刹车。

2.2 用 unlink 还是 del:删除大键时的性能差异

很多人在删除Redis键的时候只会用DEL,其实Redis 4.0之后提供了一个更好的选择:UNLINK。

表面上看,UNLINK和DEL作用是一样的,删除键并释放内存,但底层实现有本质区别。DEL是同步删除,它会立即回收这个键的内存。如果这个键对应的值是一个很大的列表、哈希或者集合,比如里面有几十万个元素,那么释放内存本身就是一件耗时的事。前面提过,Redis是单线程的,DEL一个大键的时间可能长达几百毫秒甚至秒级,这段时间Redis同样会被卡住。

UNLINK则是异步删除。它执行时只是把键从键空间中摘除,然后返回结果,真正释放内存的操作交给后台线程慢慢做,主线程可以立刻继续处理其他命令。

我在测试环境里做过一个实验:一个包含100万个元素的Set,执行DEL大约花了1.1秒,期间对应Redis实例上的其他请求全部超时。换成UNLINK之后,命令本身几乎是瞬间返回,后台线程慢慢回收内存,整个过程中Redis响应完全正常。

所以我的建议很直接:如果你不确定要删的键对应的值是大是小,直接用UNLINK。它和DEL的兼容性很好,Redis 4.0以上都支持,而且删除小键时的性能和DEL几乎没有差别,删除大键时却可以避免阻塞事故。

对比项DELUNLINK
阻塞性同步删除,可能阻塞Redis异步删除,后台线程回收内存
适用场景小键、明确知道值很小的键大键、不确定键的大小的场景
版本要求所有版本Redis 4.0及以上
返回效果返回1表示删除成功返回1表示键已从键空间摘除

唯一的注意点是,UNLINK后内存是逐步释放的,如果你想立刻腾出内存给其他业务使用,可能还得等一小会儿。但在绝大多数情况下,用UNLINK都是更安全的选择。

2.3 Lua 脚本批量删除:原子性为什么重要

另一种常见的批量删除方案是写Lua脚本,通过EVAL命令执行。使用Lua的好处是,脚本里所有的Redis操作在同一个执行流里完成,中间不会插入其他客户端发来的命令。也就是说,整个删除过程是原子的。

先看一个最简单的Lua脚本:

local keys = redis.call('KEYS', ARGV[1]) if #keys == 0 then return 0 end return redis.call('DEL', unpack(keys))

调用方式:

redis-cli EVAL "local keys = redis.call('KEYS', ARGV[1]); if #keys == 0 then return 0 end; return redis.call('DEL', unpack(keys))" 0 "session:*"

这个脚本写起来很简洁,但我必须明确告诉你:千万不要在大数据量的Redis实例上用这个脚本。原因我们前面说过,KEYS命令会阻塞Redis,脚本里它依然会阻塞。如果键数量是几百万,这个脚本执行的时候Redis就是全站卡死状态。

真正需要用到Lua脚本的场景,应该是"既要按模式删除,又要保证删除过程是原子的"。比如有一个batch:*前缀的键,每次删除的时候可能要同时检查某个状态键的值,只有状态合法才执行删除,或者需要在删除后更新一个计数键。这种情况下,你可以在Lua脚本里组合多条Redis命令:

-- 按SCAN方式迭代,避免KEYS阻塞 local cursor = '0' local count = 0 repeat local result = redis.call('SCAN', cursor, 'MATCH', ARGV[1], 'COUNT', 100) cursor = result[1] for _, key in ipairs(result[2]) do redis.call('UNLINK', key) count = count + 1 end until cursor == '0' return count

这个脚本在Lua里使用SCAN游标迭代,每次取100个键执行UNLINK,直到遍历完整个键空间。因为SCAN是增量式的,脚本不会一次性把所有键都载入内存,所以比KEYS方案安全得多。

不过我还是要泼一盆冷水:如果生产环境键数量特别大,哪怕是SCAN在Lua里循环,也可能执行很久,导致脚本长时间占用Redis执行线程。Lua脚本执行时间过长,Redis会对后续命令产生明显的延迟。所以我个人更推荐的方式是:用redis-cli --scan在客户端遍历键名,键名先落盘或者直接交给xargs分批删除。这样每一步的操作都是短小的,即使出了问题,影响面也可控。

3. 实操过程与核心环节实现

3.1 基础命令:单机环境安全删除

现在进入具体的实操。单机Redis环境的批量删除是最常见的场景,我以清理user:temp:*这个前缀的键为例,给你梳理一套我认为最稳妥的操作流程。

第一步,确认模式匹配到的键大致数量。

redis-cli --scan --pattern "user:temp:*" | wc -l

先跑一下数量,心里有底。如果你预期只有100个键,结果返回5万个,那模式可能写得太宽了,比如user:*把不该删的user:info也带上了。这时候先不要急着删除,回到业务代码里确认键名的前缀设计到底是怎么样的。

第二步,抽样检查键的类型和内容。

redis-cli --scan --pattern "user:temp:*" | head -n 10 | while read k; do echo "key = $k" redis-cli type "$k" done

抽样看几个键,确认它们确实是你想清理的临时数据。我见过有同事按照前缀删除,结果业务方早就改了键名规范,新老前缀混在一起,一删就把正在使用的数据删了。先抽样可以避免这种惨剧。

第三步,导出键名到文件,形成删除清单。

redis-cli --scan --pattern "user:temp:*" > /tmp/user_temp_keys.txt wc -l /tmp/user_temp_keys.txt

删除清单保留下来,既方便确认数量,也方便后续排查和审计。

第四步,执行删除。这里我推荐使用UNLINK,因为你不知道其中是否隐藏着大键,用UNLINK最稳妥:

xargs -a /tmp/user_temp_keys.txt -L 100 redis-cli UNLINK

如果你用的是Redis 4.0以下的老版本,才需要用DEL替代:

xargs -a /tmp/user_temp_keys.txt -L 100 redis-cli DEL

第五步,删除后验收。

redis-cli --scan --pattern "user:temp:*" | wc -l

数量应该是0,如果有余留,可能是删除过程中业务又生成了新的键。这算正常情况,再跑一遍删除流程即可。

这套流程看起来简单,但每一步都在回答一个关键问题:我到底要删什么?删了多少?删干净了吗?养成这个习惯以后,你在生产环境做任何删除操作都会从容很多。

3.2 进阶实践:集群环境的批量删除方案

到了Redis Cluster环境,批量删除就不能照搬单机命令了,这里面的坑还挺多。

首先,集群环境有16384个哈希槽,键会分散在各个节点上。redis-cli直接执行--scan只能扫到当前连接的节点,扫不到其他节点的键。你要是只扫一个节点,然后拿着部分键去做删除,看起来没有报错,实际上数据没删干净。

要做集群环境的批量删除,正确思路是"分节点扫描、逐节点删除"。

先查一下集群有哪些节点:

redis-cli -h 192.168.1.10 -p 7000 --cluster info

然后把每个主节点的地址端口记下来,逐个扫描。假设你有三个主节点,端口分别是7000、7001、7002:

redis-cli -h 192.168.1.10 -p 7000 --scan --pattern "cart:tmp:*" > /tmp/cart_keys_7000.txt redis-cli -h 192.168.1.10 -p 7001 --scan --pattern "cart:tmp:*" > /tmp/cart_keys_7001.txt redis-cli -h 192.168.1.10 -p 7002 --scan --pattern "cart:tmp:*" > /tmp/cart_keys_7002.txt

删除的时候,有一个特别容易踩的坑:在集群模式下,如果一条DEL命令里带的键不满足同一个哈希槽,服务端会直接报CROSSSLOT错误。比如DEL cart:tmp:a cart:tmp:b,如果两个键被分到了不同的槽,这条命令就没法执行。

有人说用redis-cli -c连集群就能自动转发,但-c模式面对单键读写时确实会自动转发,面对一条包含多个键的DEL命令时,多键事务或跨槽命令的限制依然存在。所以最安全的做法是:把删除命令拆成单键删除,或者每个键单独走一次DEL。

我习惯用一个简单的循环来做:

while read key; do redis-cli -c -h 192.168.1.10 -p 7000 DEL "$key" done < /tmp/cart_keys_7000.txt

这样每个键单独一条DEL,由客户端自动跟踪键对应的槽并转发到正确的节点。缺点是多几次网络往返,但在可接受的范围内。如果你对性能要求比较高,可以按槽位分组后,把属于同一个槽的键拼到一条DEL里,这样能减少命令数量。只是代码复杂度会明显上升。

如果你的项目里用了Redis官方推荐的客户端、带有MGET/MSET等接口的连接池方案,大部分客户端封装的"批量删除"接口实际上也是循环执行单键DEL,并不会真的用多键DEL命令。所以集群环境下,循环单键删除并不可怕,反而更符合集群的运作逻辑。

另外还有一种方案是借助redis-cli --cluster子命令,比如:

redis-cli --cluster call 192.168.1.10:7000 UNLINK "cart:tmp:*"

这个命令会在所有集群节点上执行同一个命令,但它执行的是字面量键名解析,不会自动替换为模式匹配。也就是说,UNLINK "cart:tmp:*"只会去尝试删除一个名字叫cart:tmp:*的键,而不是匹配所有以cart:tmp:开头的键。所以这个方案没法直接满足按模式删除的需求,不要被它误导。

3.3 删除前的备份与演练

我见过很多人在生产环境删Redis键,基本都是"删完再说"。结果真有误删的时候,才发现自己连备份都没有,只能干瞪眼。这里我分享一套我在团队里推行的删除前备份保障流程。

第一步是判断键数据能否重新生成。如果这些键只是缓存数据,删除后业务访问时会重新查询数据库并回填缓存,那备份的意义主要在"能够审计删了哪些键"。这时候你只需要保存键名清单就够了。

redis-cli --scan --pattern "user:temp:*" > /tmp/user_temp_keys_$(date +%Y%m%d%H%M%S).txt

第二步是判断键数据是否必须保留。如果键里存的是用户上传的任务状态、异步任务的中间结果等无法从其他数据源恢复的数据,那你不能只备份键名,还得想办法备份键值。Redis本身不像关系型数据库那样支持细粒度的单键备份,但你可以用DUMP命令把键序列化导出再RESTORE回去。我用过一个土办法,批量循环DUMP:

redis-cli --scan --pattern "task:state:*" | while read key; do redis-cli --raw DUMP "$key" >> /tmp/task_state_dump.bin echo "### $key" >> /tmp/task_state_keys.txt done

这里DUMP会输出键的序列化二进制内容,RESTORE可以把内容恢复到Redis里。注意DUMP输出的内容可能包含特殊字符,直接追加到文件里是可行的,但恢复的时候需要对应解析。更规范的做法是配合Python等脚本逐个DUMP并以二进制文件保存。我这里只是给个思路,实际操作中光一个DUMP循环的次数也值得注意——键很多时最好分批执行,别一口气扫完。

第三步就是演练。我在每次计划大范围删除之前,一定会先在测试环境跑一遍完整流程:扫描、抽样、导出、删除、验证。测试环境的数据结构和生产环境保持一致,只是数据量级小一些。演练能发现很多细节问题,比如模式写错、忘了加密码环境变量、命令版本不兼容等等。

还有个更“保险”的小窍门:真正的删除操作安排在工作负载比较低的时间段,比如凌晨。这样即使出现异常,影响面也小,留给团队的应急时间也更多。

4. 常见问题与排查技巧实录

4.1 SCAN 游标解析与大数据量下的性能表现

很多人第一次看SCAN的返回结果会懵:第一次返回的是游标和一批键,第二次你拿新游标再调用,返回的游标又变了。这个游标到底是怎么设计的?

简单理解,SCAN把Redis键空间看成一张固定大小的哈希表,游标记录的是遍历到的哈希桶位置。每次调用从当前游标位置开始,向后扫描若干个桶,返回这些桶里的键,同时返回下一个要继续扫描的位置。当游标回到0时,整个遍历结束。

COUNT参数很多人以为是"每次返回多少键",实际上它更像是"每次扫描多少个桶"的提示值。桶里键多的话,一次可能返回上千个键,桶里键少的话可能只返回几个。所以COUNT是一个性能调优参数,并非一个严格的数量限制。

我在一个2000万键的Redis实例上测过,用COUNT 1000跑完整轮SCAN,大概需要几十秒。但是注意,这几十秒是分散在很多次命令里的,每次命令都不会阻塞Redis,其他请求始终能正常处理。这就是SCAN和KEYS最大的区别。

另一个现象你可能也会遇到:SCAN过程中如果有键被创建、删除或者过期,SCAN可能会返回一些已经失效的键,也有可能会漏掉少量遍历过程中新添加的键。这是增量遍历的固有特性,Redis官方文档也明确说明了。所以当你统计到的数量和实际删除数量对不上时,不一定是代码有bug,可能是遍历过程中键的状态发生了变动。

实操上,我会用"两次扫描对比数量"的方式来规避这种不确定性。第一次扫描只统计数量,第二次扫描确认数量,如果两次数量一致,说明运行环境比较稳定;如果不一致,就以第二次为准,多跑一轮删除。

4.2 误删数据找回方案

误删数据是每个工程师都不想面对但又确实可能发生的事情。这里我分享几个实际验证过的找回思路,覆盖不同场景。

如果你的Redis开启了AOF持久化,并且删除操作发生之后还没有触发过重写,那么AOF文件里会留有删除之前的写操作记录。你可以把AOF文件拉到其他环境重放,恢复到误删前的状态,再用SCAN把需要的键找出来迁移回生产实例。这个方案理论上可行,但需要AOF文件中确实记录了这些键的写入操作,而且你得在删除后尽快停下来,避免后续操作把AOF记录覆盖掉。

如果你的Redis开启了RDB持久化,RDB快照里保存的是某个时间点之前的数据。把RDB快照恢复到一台临时Redis实例上,然后SCAN找到被误删的键,用DUMP和RESTORE把键搬回生产环境。这种方法在恢复单批键的时候非常管用。前提是你得有删除操作之前的RDB快照,而且RDB本身没有过期。

如果没有备份也没有持久化文件,那就只剩下一条路:从数据源重新生成。比如缓存键可以从数据库读取后重建,临时键可能对应某个异步任务状态,重新触发任务就能恢复。这一步在日常运维中最有用,因为绝大多数Redis键都是可重建的缓存数据。这也是为什么我一直强调,删除前一定要搞清楚键的数据来源。

就我个人的团队实践而言,我们会在误删发生后立刻做三件事:第一,所有相关服务暂停写入Redis;第二,检查备份时间点并评估损失;第三,如果无法快速恢复,立即通知依赖方降级。很多时候误删的键数量不多,业务方只需要重试一遍就能自动重建,最怕的是默默删了一批没有备份的不可再生数据,导致报表错乱、任务丢失,排查起来非常费劲。

所以预防比找回更关键。我的建议是,在生产环境执行危险删除前,至少做到三点:一是用--scan和wc -l核对数量;二是把键名清单保存到本地;三是尽量在低峰期操作。

4.3 模式匹配特殊字符的坑

Redis的键模式匹配用的是glob风格的通配符规则,和正则表达式不完全一样。这里很容易踩坑。

*匹配任意长度的任意字符,?匹配单个任意字符,[...]匹配方括号内的任意一个字符。比如user:[0-9]*可以匹配user:123abc,但不能匹配user:abc。这套规则看着简单,一旦你的键名本身就是带*、?、[、]这些符号,麻烦就来了。

比如键名叫file[1].cache,你执行SCAN MATCH file[*].cache的时候,[和]会被解析成字符集,结果可能匹配file1.cache、fileA.cache等等,而你的真实键名反而匹配不上。这种情况下需要用反斜杠转义:

redis-cli --scan --pattern "file\\[*\\].cache"

这里\\[和\\]转义了方括号,Redis会把它当字面值处理。

还有一个容易忽略的问题:Redis命令里的模式,和Shell命令行的通配符是两个完全不同的体系。你在命令行执行redis-cli --scan --pattern "*.txt",如果Shell做了路径名扩展,它可能在你把参数传给redis-cli之前就已经把模式替换成了当前目录下的文件名列表。我建议你始终用单引号把模式包起来:

redis-cli --scan --pattern '*.txt'

单引号能阻止Shell对*、?、[这些字符做任何解释,确保原样送到Redis。

另外,MATCH模式的作用范围也值得说一下。SCAN的MATCH过滤是在遍历完当前桶之后才做的,并不会因为你的模式很精确就减少哈希桶的扫描范围。比如你执行SCAN MATCH "abc*",Redis依然会扫描整个键空间的桶,然后把不满足匹配条件的键过滤掉再返回。所以模式写得再短,也改变不了扫描全量键的开销。这也是我为什么一直建议,在键名设计阶段就尽量使用固定前缀来隔离业务,扫描匹配时才能更高效。

5. 更工程化的封装:用 Python 脚本给批量删除加上保险

命令行操作适合临时清理,但如果你的团队经常要做类似的批量删除,我建议把流程固化成脚本。用Python封装一个批量删除工具,可以加入dry-run、分批删除、日志记录等能力,安全性会高很多。

下面是一个我自己经常用的脚本模板,功能包括:连接参数配置、模式扫描、预览模式、实际删除模式、支持DEL和UNLINK切换、每删一批打印一条日志。

import redis import sys import time def scan_keys(r, pattern, count=500): """使用scan_iter遍历匹配的键,底层是SCAN,安全不阻塞""" keys = [] for key in r.scan_iter(match=pattern, count=count): keys.append(key) return keys def delete_keys(r, keys, use_unlink=False, dry_run=True, batch_size=100): """按批次删除,支持dry-run""" total = 0 for i in range(0, len(keys), batch_size): batch = keys[i:i+batch_size] if dry_run: for k in batch: print("[dry-run]", k) total += len(batch) else: if use_unlink: r.unlink(*batch) else: r.delete(*batch) total += len(batch) print(f"deleted {total}/{len(keys)} keys") time.sleep(0.01) # 轻微限速,降低对Redis的压力 return total def main(): host = "127.0.0.1" port = 6379 password = None pattern = sys.argv[1] if len(sys.argv) > 1 else "temp:*" dry_run = True use_unlink = True r = redis.Redis(host=host, port=port, password=password, decode_responses=True) print(f"start scanning pattern: {pattern}") keys = scan_keys(r, pattern) print(f"matched keys: {len(keys)}") if not keys: print("no keys matched, exit") return if dry_run: print("dry-run mode, no actual delete") delete_keys(r, keys, use_unlink=use_unlink, dry_run=True) else: confirm = input(f"about to delete {len(keys)} keys, type yes to continue: ") if confirm == "yes": delete_keys(r, keys, use_unlink=use_unlink, dry_run=False) else: print("cancelled") if __name__ == "__main__": main()

这个脚本有几个设计点我想说明一下。

第一个是scan_iter。它封装了SCAN的游标迭代逻辑,每次取一批,不会一次性把全部键拉进内存,但这里会逐步把所有匹配的键收集到Python列表里。键特别多的时候(比如十万以上),可以考虑改成流式处理,边遍历边删除,而不是先收集再删除。只是流式处理时,如果遍历期间业务新增了匹配的键,可能会漏删,需要额外处理。

第二个是dry_run。我一直坚持"默认预览,确认后删除"的原则。脚本默认不会真删,只有你手动改成dry_run=False并输入yes才会执行真正的删除。这条防线能挡住很多手误。

第三个是批次删除和限速。r.delete(*batch)一次删除100个键,配合time.sleep(0.01)给Redis一点喘息时间,避免在业务高峰期产生过大的瞬时压力。

这个脚本在单机环境和集群环境都能用。如果你连的是Redis Cluster,只要把redis.Redis换成redis.cluster.RedisCluster,客户端会自动处理槽位转发和跨节点访问。Python Redis客户端对集群的scan_iter支持得比较好,会帮你轮询所有节点。

我自己在实际使用中,还会把脚本的输出同时重定向到日志文件,这样每次删除操作都有记录,哪一天被业务方质问"是不是你删了我的数据",我能直接甩出日志来自证清白。这个习惯推荐给大家。

最后再分享一点个人经验。批量删除Redis键本身是一项非常机械的工作,真正的风险都在"删错"这件事上。所以我强烈建议你在设计Redis键名的时候就想好前缀规范,让每个业务模块的键都有清晰可辨的前缀和过期时间。如果发现键没有设置TTL,大概率意味着以后你需要人工清理。最理想的状态是:键到了时间自动过期,后台清理只是兜底手段。有了这个意识,你再回来看"按模式批量删除",它就不是一个救火技能,而是一项常规维护操作,安全系数和心理压力完全不是一个级别。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询