报表查询易触发缓存击穿,因强时效性与高生成成本导致短过期时间,过期瞬间并发请求压垮报表服务;Redis Cluster下需用带slot标签的锁key、客户端自动重试及分层过期策略,并结合轮询排队与降级兜底来防护。

为什么报表查询容易触发缓存击穿
报表类接口通常有强时效性,但数据生成成本高(比如聚合计算、跨表 Join、读取历史归档库),所以常设 EXPIRE 为 5–15 分钟。一旦多个请求在 key 过期瞬间同时到达,就会全部穿透到后端服务——不是数据库被打满,而是报表生成服务被并发压垮,出现 CPU 100%、线程池耗尽、超时堆积等现象。
Redis Cluster 下不能直接用 SETNX 做互斥锁
Redis 集群对 KEY 做了 slot 分片,而 SETNX 是单 key 命令,如果锁 key 和业务 key 落在不同节点,原子性就失效了。更危险的是:有人写 SET key value NX EX 30 看似安全,但在集群模式下,若客户端没开启 ASK/MOVED 自动重定向(如某些老版本 Jedis),会静默失败或报 MOVED 错误却未处理,导致锁根本没生效。
- 必须确保锁 key 和报表数据 key 落在同一个 slot:对锁 key 使用
{report:20261019:summary}这种带花括号的 tag 写法,让 Redis 计算 slot 时只取花括号内部分 - 客户端必须支持集群自动重试,推荐用 Lettuce(默认开启)或升级到 Jedis 4.x 并显式配置
ClusterClientConfig - 避免用 Lua 脚本封装锁逻辑——集群模式下 Lua 脚本只能操作单个 key,跨 slot 执行会报错
ERR MOVED
用 RedLock + 本地等待实现排队效果
单纯加锁只是“谁抢到谁干”,但报表查询场景需要的是“先到先服务”的排队语义。不能让 100 个请求都去争一把锁,而应让它们按顺序串行触发重建。
- 第一步:用
SET {report:20261019:summary} building NX EX 120尝试获取“构建中”标记;成功者进入第二步,失败者执行GET {report:20261019:summary}检查当前状态 - 第二步:若返回
building,说明已有任务在跑,此时不做重试,而是 sleep 1–3 秒后重查缓存;不建议用BLPOP或队列,因为报表重建是长耗时任务,阻塞式等待会浪费连接资源 - 第三步:重建完成后,不直接
SET新值,而是用SET {report:20261019:summary} ready EX 3600标记就绪,并用PUBLISH report:ready {report:20261019:summary}推送事件(需提前订阅)
这个模式把“互斥”和“排队”拆开:互斥靠分布式锁保证唯一构建者,排队靠客户端轮询 + 状态机推进,规避了集群下锁粒度与等待机制的耦合风险。
关键细节:过期时间必须分层设置
报表缓存的生命周期要拆成两段,否则锁释放和缓存写入之间存在空窗,仍可能引发击穿。
- 锁 key 的过期时间(如 120 秒)必须显著长于报表实际生成耗时(比如实测最长 85 秒),留出安全余量
- 数据 key 的过期时间(如 3600 秒)要远大于锁时间,且不能跟锁 key 同时失效;否则刚写入缓存就过期,下一个请求又会触发重建
- 绝不使用
PEXPIRE动态延长锁时间——集群环境下无法保证该命令落到正确节点,易造成锁残留或丢失
最易被忽略的一点:报表接口的降级逻辑必须独立于缓存层。当检测到锁已存在且等待超时(比如轮询 5 次共 15 秒仍未就绪),应直接返回上一版缓存数据 + X-Cache-Status: stale 响应头,而不是抛异常或无限等。这层兜底决定了故障时用户体验是“稍慢一点”,还是“直接不可用”。











