“排队”比“重试”更可控,因重试会放大洪峰导致并发查库,而排队将请求序列化、按系统吞吐节拍处理,可使数据库QPS下降70%以上。

缓存击穿时,为什么“排队”比“重试”更可控
因为重试会放大洪峰:几十个线程同时发现缓存失效,各自去抢锁、查库、写缓存,哪怕只有一人成功,其余线程的等待+超时+重试逻辑仍会持续制造压力。而排队机制本质是把并发请求序列化,让系统按吞吐能力节拍处理。
- 典型错误现象:
GET product:1001返回空后,50个请求在200ms内全部执行SELECT * FROM products WHERE id = 1001 - 关键区别:重试是“我等100ms再试一次”,排队是“我进队列,等前面49人走完才轮到我”
- 适用场景:热点商品详情、登录态校验、活动页配置等读多写少、容忍毫秒级延迟的接口
- 性能影响:单次响应可能增加 5–50ms(取决于队列长度),但数据库 QPS 可下降 70% 以上(参考真实案例从 12,000 → 3,360)
用 Redis List + Lua 实现轻量级请求排队
不用引入 Kafka 或 RabbitMQ,仅靠 Redis 原生命令就能搭出可靠排队层——关键是用 Lua 保证“入队+判长”原子性,避免队列爆满后继续塞入。
- 入队脚本(
queue_push.lua):local key = KEYS[1] local max_len = tonumber(ARGV[1]) local current_len = redis.call('LLEN', key) if current_len < max_len then return redis.call('LPUSH', key, ARGV[2]) else return -1 -- 拒绝入队 end - 消费端轮询示例(Python):
r.brpop('queue:product', timeout=1),配合timeout=1避免长阻塞 - 参数差异:
max_len不是越大越好;设为 200~500 较稳妥,超过则说明后端处理能力已严重不足,该触发降级而非硬扛 - 注意陷阱:别用
LLEN+LPUSH两步判断,中间可能被其他客户端插入,必须合并在 Lua 中执行
排队期间如何不卡死用户?返回旧数据 or 空响应
用户刷新页面,不能让他盯着 loading 转圈 3 秒。排队机制必须配套“兜底响应策略”,否则体验反而更差。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 常见错误:排队成功就直接
return {"status":"queued"},前端无感知,反复刷新造成二次排队 - 推荐做法:缓存中保留一份
stale data(例如 Caffeine 本地缓存 + Redis 逻辑过期字段),排队时立即返回它 - 结构示例:
{"data":{"name":"iPhone","price":6999},"expire_time":1743456789000},检查expire_time < now()即触发异步更新 - 兼容性提醒:若业务强依赖实时性(如库存数字),则必须返回
{"code":425,"msg":"数据更新中,请稍候"},并由前端做倒计时重拉
Redis 排队和消息队列(Kafka/RabbitMQ)怎么选
不是“谁更好”,而是“谁更合适当前瓶颈”。Redis 排队解决的是瞬时毛刺型洪峰(秒级爆发),消息队列解决的是持续高吞吐型负载(分钟级稳定高压)。
- 选 Redis 的信号:
QPS 波动剧烈(比如从 200 突增到 8000)、单次处理耗时短(< 200ms)、不需要消息追溯或重放 - 选 Kafka 的信号:
订单/日志类需持久化、消费者处理慢且不可丢、要支持多订阅者(如风控+统计+推送) - 容易踩的坑:用 Redis List 当 MQ 用——没 ACK 机制,消费者崩溃就丢任务;用 Kafka 承担缓存击穿——过度设计,延迟从毫秒升到百毫秒
- 真实折中方案:Redis 排队做第一道闸口,过滤掉 90% 毛刺;真正需要落库的任务,再投递到 Kafka 异步处理
实际落地时,最常被忽略的是“排队超时后的清理动作”:用户等了 2 秒还没排到,前端取消请求,但 Redis 里那个排队项还在。得有定时任务或 Lua 脚本定期 LTRIM 过期等待项,否则队列会缓慢膨胀。

















