redis阻塞命令大全:说说 哪些的命令 会导致 Redis 阻塞 ,如何规避? 减少 故障的发生?(图解+秒懂)
本文 的 原文 地址
原始的内容,请参考 本文 的 原文 地址
尼恩说在前面
在45岁老架构师 尼恩的读者交流群(50+)中,帮助很多小伙伴拿到了一线企业如 字节、得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团、蚂蚁、得物的面试资格。
前段时间,辅导一个 3年经验的小伙伴 进了 一个大厂,逆天改命。
但是不小心干了一件错事。
用错一个redis命令,一波请求高峰把redis打挂了,导致 月绩效降级了。
如何避免再犯?
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这个问题作为面试题,也会收入咱们的 《尼恩Java面试宝典》V175版本PDF集群,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,后台回复:领电子书
说说 哪些的命令 会导致 Redis秒级 阻塞 ,如何规避,从而减少 故障的发生?
在 Redis 单线程模型中,命令的执行效率直接决定了服务的响应能力。
当数据量较大时,时间复杂度为 O(n) 及以上的Redis 命令会一次性处理大量数据,导致线程长时间占用,引发服务阻塞,甚至影响整个业务系统的稳定性。
接下来将结合真实场景案例,逐一解析常见阻塞命令的风险,并提供可落地的优化方案。
一、 理论上 可能导致 Redis 阻塞的命令
可能导致 Redis 阻塞的命令,主要是一些时间复杂度为 O(n) 或更高的命令。
在数据量较大时,这些命令 会一次性加载大量数据,导致 Redis 单线程模型下的阻塞。
以下是常见的类似命令:
1、通用命令
KEYS *:获取所有键(或匹配模式的所有键)。
2、哈希(Hash)相关
- HGETALL:获取哈希中所有字段和值。
- HKEYS:获取哈希中所有字段。
- HVALS:获取哈希中所有值。
2、列表(List)相关
- LRANGE key 0 -1:获取列表中所有元素。
3、集合(Set)相关
- SMEMBERS:获取集合中所有成员。
- SUNION:计算多个集合的并集。
- SINTER:计算多个集合的交集。
- SDIFF:计算多个集合的差集。
4、有序集合(Sorted Set)相关
- ZRANGE key 0 -1:获取有序集合中所有成员。
- ZREVRANGE key 0 -1:逆序获取有序集合中所有成员。
5、其他可能导致阻塞的命令
- DEL:删除包含大量元素的键(如大哈希、大列表、大集合等),时间复杂度为 O(n) 。
6、如何避免阻塞?
- 渐进式遍历:使用 SCAN、HSCAN、SSCAN、ZSCAN 等命令,逐步迭代数据,避免一次性加载大量数据 。
- 拆分大键:将大哈希、大列表等拆分为多个小键,避免单个键数据量过大
二、哈希(Hash)相关阻塞命令
Hash 是 Redis 中常用的结构,用于存储键值对集合(如用户信息、商品属性等)。
当 Hash 中字段数量(field)达到数万甚至数十万时,HGETALL、HKEYS、HVALS 会一次性返回所有数据,触发阻塞。
1. HGETALL:获取哈希中所有字段和值
命令作用:
返回指定 Hash 中所有 field-value 键值对,时间复杂度 O(n)(n 为 Hash 字段数)。
阻塞风险:
当 Hash 包含大量字段(如 10 万 +)时,命令执行时间会从毫秒级飙升至秒级,期间 Redis 无法处理其他请求。
案例场景:
电商平台用 Hash 存储 “商品库存表”,键为 product:stock,field 为商品 ID(如 prod_1001),value 为库存数量。
随着商品数量增长到 50 万,运营人员执行 HGETALL product:stock 导出全量库存时,Redis 阻塞 3 秒,导致下单接口大面积超时。
Demo 演示
# 1. 模拟创建包含 10 万字段的 Hash(通过 Redis 客户端脚本)
redis-cli EVAL "for i=1,100000 do redis.call('HSET', 'product:stock', 'prod_'..i, math.random(1, 1000)) end" 0
# 2. 执行 HGETALL,观察阻塞现象(数据量过大时,客户端会卡顿数秒)
redis-cli HGETALL product:stock
# 3. 查看命令执行时间(通过 INFO stats 或慢查询日志)
redis-cli INFO stats | grep "latest_fork_usec" # 或查看 slowlog get
2. HKEYS:获取哈希中所有字段
命令作用:
仅返回 Hash 中的所有 field,不包含 value,时间复杂度 O(n)。
阻塞风险:
虽不返回值,但仍需遍历所有字段,数据量大时阻塞时间与 HGETALL 接近(仅少了 value 传输时间)。
案例场景:
某社交平台用 Hash 存储 “用户关注列表”,键为 user:follow:1001(1001 为用户 ID),field 为被关注用户的 ID。
当某大 V 用户关注数达 20 万时,后台服务执行 HKEYS user:follow:1001 统计关注总数,导致 Redis 阻塞 1.5 秒,私信、评论功能暂时不可用。
Demo 演示
# 1. 模拟创建包含 20 万字段的用户关注 Hash
redis-cli EVAL "for i=1,200000 do redis.call('HSET', 'user:follow:1001', 'follow_'..i, 1) end" 0
# 2. 执行 HKEYS,观察阻塞(客户端会卡顿 1-2 秒)
redis-cli HKEYS user:follow:1001
优化方案:用 HSCAN 渐进式遍历
# 3. 对比:用 HSCAN 渐进式遍历(无阻塞,分批次返回)
redis-cli HSCAN user:follow:1001 0 COUNT 1000 # 每次返回 1000 个 field,可循环执行
3. HVALS:获取哈希中所有值
命令作用:
仅返回 Hash 中的所有 value,不包含 field,时间复杂度 O(n)。
阻塞风险:
与 HKEYS 类似,需遍历所有字段并提取值,数据量大时仍会阻塞。
案例场景:
某游戏用 Hash 存储 “玩家装备评分”,键为 game:equip:score,field 为玩家 ID,value 为装备总评分。
当玩家数量达 8 万时,运营执行 HVALS game:equip:score 计算平均评分,Redis 阻塞 1 秒,导致游戏登录、战斗结算超时。
Demo演示
# 1. 模拟创建包含 8 万字段的装备评分 Hash
redis-cli EVAL "for i=1,80000 do redis.call('HSET', 'game:equip:score', 'player_'..i, math.random(5000, 20000)) end" 0
# 2. 执行 HVALS,观察阻塞
redis-cli HVALS game:equip:score
优化方案:用 HSCAN 遍历并提取 value(无阻塞)
# 优化方案:用 HSCAN 遍历并提取 value(无阻塞)
redis-cli EVAL "local cursor=0; repeat local res=redis.call('HSCAN', 'game:equip:score', cursor, 'COUNT', 1000); cursor=tonumber(res[1]); for _,v in ipairs(res[2]) do if _%2==0 then print(v) end end until cursor==0" 0
三、列表(List)相关阻塞命令
List 基于双向链表实现,适合存储有序数据(如消息队列、用户动态)。
LRANGE key 0 -1 会从列表头部(0)到尾部(-1)返回所有元素,是典型的阻塞命令。
LRANGE key 0 -1:获取列表中所有元素
命令作用:
返回列表中所有元素,时间复杂度 O(n)(n 为列表长度)。
阻塞风险:
List 长度超过 1 万时,命令执行时间明显增加;
若作为消息队列且未及时消费,长度达 10 万 +,执行 LRANGE 会直接导致 Redis 长时间阻塞。
案例场景:
某平台用 List 实现 “订单消息队列”,键为 queue:order,每个元素是订单 JSON 字符串。
因消费端故障,队列积压 30 万条消息,运维人员执行 LRANGE queue:order 0 -1 排查消息时,Redis 阻塞 5 秒,所有依赖 Redis 的订单接口、支付接口均报错。
Demo 案例
# 1. 模拟向列表中添加 30 万条订单消息
redis-cli EVAL "for i=1,300000 do redis.call('LPUSH', 'queue:order', '{"order_id":"'..i..'","amount":'..math.random(100, 10000)..'}"') end" 0
# 2. 执行 LRANGE 获取所有元素,观察严重阻塞(客户端可能超时断开)
redis-cli LRANGE queue:order 0 -1
优化方案:分批次获取(避免一次性加载)
# 3. 优化方案:分批次获取(避免一次性加载)
# 先获取列表长度,再循环用 LRANGE 分批次取(如每次取 1000 条)
redis-cli LLEN queue:order # 查看长度
redis-cli LRANGE queue:order 0 999 # 第 1 批
redis-cli LRANGE queue:order 1000 1999 # 第 2 批(依此类推)
四、集合(Set)相关阻塞命令
Set 用于存储无序、唯一的数据(如用户标签、商品分类),SMEMBERS、SUNION、SINTER、SDIFF 均需遍历集合元素,数据量大时会阻塞。
1. SMEMBERS:获取集合中所有成员
命令作用:
返回 Set 中所有成员,时间复杂度 O(n)。
阻塞风险:
Set 底层用哈希表实现,遍历所有成员的耗时随元素数量线性增长,10 万 + 元素时阻塞明显。
案例场景:
某教育平台用 Set 存储 “课程报名用户”,键为 course:users:101(101 为课程 ID),成员为用户 ID。
当报名人数达 15 万时,后台执行 SMEMBERS course:users:101 导出报名名单,Redis 阻塞 2 秒,导致课程购买、学习进度保存接口超时。
Demo 案例
# 1. 模拟创建包含 15 万用户的课程报名 Set
redis-cli EVAL "for i=1,150000 do redis.call('SADD', 'course:users:101', 'user_'..i) end" 0
# 2. 执行 SMEMBERS,观察阻塞
redis-cli SMEMBERS course:users:101
优化方案:用 SCAN 渐进式遍历
# 3. 优化方案:用 SSCAN 渐进式遍历
redis-cli SSCAN course:users:101 0 COUNT 1000 # 每次返回 1000 个用户,循环执行至 cursor=0
2. SUNION:计算多个集合的并集
命令作用:
返回多个 Set 的并集(合并所有成员,去重),时间复杂度 O(n)(n 为所有集合的总元素数)。
阻塞风险:
若参与计算的多个 Set 均为大集合(如每个 5 万元素),总元素数达 10 万 +,SUNION 会因大量计算和数据返回导致阻塞。
案例场景:
某电商用 Set 存储 “用户浏览商品”(键 user:view:1001)和 “用户收藏商品”(键 user:collect:1001),运营需求是计算 “用户感兴趣商品”(浏览 + 收藏的并集)。当两个 Set 各有 8 万元素时,执行 SUNION user:view:1001 user:collect:1001 导致 Redis 阻塞 1.8 秒,推荐系统无法响应。
Demo 案例
# 1. 模拟创建两个各 8 万元素的 Set
redis-cli EVAL "for i=1,80000 do redis.call('SADD', 'user:view:1001', 'prod_'..i) end" 0
redis-cli EVAL "for i=40001,120000 do redis.call('SADD', 'user:collect:1001', 'prod_'..i) end" 0
# 2. 执行 SUNION,观察阻塞(返回约 12 万元素,耗时 1-2 秒)
redis-cli SUNION user:view:1001 user:collect:1001
优化方案:用 SSCAN 渐进式遍历 + 业务层计算并集
用 SSCAN 分别遍历两个 Set,在业务层计算并集(避免 Redis 端计算)
3. SINTER:计算多个集合的交集
命令作用:
返回多个 Set 的交集(仅保留同时存在于所有集合的成员),时间复杂度 O(n)(n 为最小集合的元素数,但需遍历所有集合)。
阻塞风险:
即使最小集合较小,若其他集合为大集合,仍需遍历所有集合元素,导致阻塞。
案例场景:
某社交平台用 Set 存储 “用户 A 的好友”(user:friends:A)和 “用户 B 的好友”(user:friends:B),需求是计算 “A 和 B 的共同好友”。
当 A 有 10 万好友、B 有 8 万好友时,执行 SINTER user:friends:A user:friends:B 导致 Redis 阻塞 2.5 秒,好友推荐功能不可用。
Demo 案例
# 1. 模拟创建两个 Set:A 有 10 万好友,B 有 8 万好友(交集约 2 万)
redis-cli EVAL "for i=1,100000 do redis.call('SADD', 'user:friends:A', 'friend_'..i) end" 0
redis-cli EVAL "for i=80001,160000 do redis.call('SADD', 'user:friends:B', 'friend_'..i) end" 0
# 2. 执行 SINTER,观察阻塞(计算交集需遍历大量元素,耗时 2-3 秒)
redis-cli SINTER user:friends:A user:friends:B
# 3. 优化方案:用 SSCAN 遍历较小的 Set,再逐个判断元素是否存在于其他 Set(如 SISMEMBER)
4. SDIFF:计算多个集合的差集
命令作用:
返回第一个 Set 与其他 Set 的差集(仅保留存在于第一个 Set、不存在于其他 Set 的成员),时间复杂度 O(n)(n 为第一个 Set 的元素数)。
阻塞风险:
若第一个 Set 为大集合(如 10 万元素),即使其他 Set 较小,仍需遍历第一个 Set 的所有元素,导致阻塞。
案例场景:
某会员系统用 Set 存储 “所有会员”(member:all)和 “已过期会员”(member:expired),需求是计算 “有效会员”(所有会员 - 已过期会员)。
当 member:all 有 20 万元素、member:expired 有 3 万元素时,执行 SDIFF member:all member:expired 导致 Redis 阻塞 2 秒,会员登录、权益查询接口超时。
Demo 案例
# 1. 模拟创建两个 Set:所有会员 20 万,已过期会员 3 万
redis-cli EVAL "for i=1,200000 do redis.call('SADD', 'member:all', 'mem_'..i) end" 0
redis-cli EVAL "for i=1,30000 do redis.call('SADD', 'member:expired', 'mem_'..i) end" 0
# 2. 执行 SDIFF,观察阻塞(遍历 20 万元素,耗时约 2 秒)
redis-cli SDIFF member:all member:expired
# 3. 优化方案:用 SSCAN 遍历 `member:expired`,在业务层从 `member:all` 中排除(减少 Redis 计算压力)
五、有序集合(Sorted Set)相关阻塞命令
由于平台篇幅限制, 剩下的内容(5000字+),请参参见原文地址
浙公网安备 33010602011771号