大列表直接用lrange会卡住redis,因其时间复杂度o(n),几十万条时单次耗时上百毫秒,阻塞单线程导致其他命令排队,且lrange 0 -1全取更危险;应改用lindex分批取值或scan分片删除。

为什么大列表直接用 LRANGE 会卡住 Redis?
因为 LRANGE 时间复杂度是 O(N),当列表有几十万条时,单次执行可能耗时上百毫秒。Redis 是单线程的,这期间其他所有命令都得排队——你看到的 ERR BUSY Redis is busy running a script 或监控里 used_cpu_sys 突增,基本就是它在扛着。
更麻烦的是,很多人写 LRANGE key 0 -1 想“全取”,结果不管列表多长都硬扛,等于主动给自己埋阻塞点。
- 别信“只是读操作就没事”——读也是 CPU 密集型
-
LTRIM key 100 -1是 O(1),但前提是前面那步LRANGE别先炸掉 - 客户端分页拉 + 逐条
LPOP,网络往返 + 锁持有时间翻倍,实际更慢
EVALSHA 必须配 fallback,否则重启后脚本失效
生产环境不能只用 EVAL 提交脚本,每次都要解析编译,高频调用下开销明显。正确做法是:首次用 EVAL 注册,拿到 SHA(比如 "4e251b7a876c17b4f1a773818e843d2a2a9c8d9e"),后续全走 EVALSHA。
但 Redis 重启后 SHA 缓存清空,EVALSHA 会返回 nil,不是报错——业务代码若没判空 fallback 到 EVAL,就会静默失败。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- fallback 逻辑必须存在,且要幂等(重复注册 SHA 不影响)
- 别把脚本字符串拼在业务代码里,启动时预加载或从配置中心拉取更稳
-
EVALSHA的参数顺序是:EVALSHA <sha><numkeys><key1> [<key2>...] [<arg1><arg2>...]</arg2></arg1></key2></key1></numkeys></sha>
分批消费大列表:用 lindex 替代全量 LRANGE
如果只需要取前 N 条并删掉,LRANGE key 0 N-1 + LTRIM key N -1 是安全的,但前提是 N 小(比如 ≤ 100)。一旦 N 动态变大(如按页码算偏移),风险立刻上升。
更可控的方式是改用 lindex 分次取值,避免一次性加载整段内存:
local items = {}
for i = 0, 99 do
local v = redis.call('lindex', KEYS[1], i)
if not v then break end
table.insert(items, v)
end
redis.call('ltrim', KEYS[1], #items, -1)
return items
-
lindex单次是 O(1) 查找,但循环 100 次仍是 O(100),比LRANGE 0 99略慢但更可控 - 遇到空列表或 key 不存在时,
lindex返回nil,需显式break,否则脚本会卡在死循环 - 这个模式适合“小批量稳定消费”,不适合“一次取几万条”
清空海量 key:别用 KEYS,改用 SCAN + 分片删除
用 KEYS pattern 扫描再删,在百万级 key 下会阻塞数秒——它是一次性遍历全部 db,根本不可控。
必须换成 SCAN 游标式分页,配合 COUNT 控制单次扫描量:
local cursor = 0
local deleted = 0
repeat
local res = redis.call('scan', cursor, 'match', ARGV[1], 'count', ARGV[2])
cursor = tonumber(res[1])
local keys = res[2]
for i, k in ipairs(keys) do
redis.call('del', k)
deleted = deleted + 1
end
until cursor == 0
return deleted
-
ARGV[2]建议设为 100~500,太小扫太多轮,太大仍可能抖动 - 脚本里没加
replicate_commands()?那在 AOF/RDB 或主从同步时可能丢数据 - 空 key 或 pattern 无匹配时,
res[2]是空表{},#keys为 0,不会报错
redis.call('lrange', 'missing', 0, 99) 返回 {} 没问题,但 redis.call('lindex', 'missing', 0) 返回 nil —— 这个差异会让不加判断的循环停不下来。










