不能只用Redis做热点缓存,因其集中式架构在TTL到期瞬间遭遇高并发时易引发锁竞争、网络延迟与重复穿透;本地缓存需设短TTL、禁用expireAfterAccess、读失败时本地限流加载,并通过MQ事件驱动各节点一致失效。

热点数据单靠 Redis 一层缓存扛不住击穿,必须加本地缓存做第一道防线;但直接套用多级缓存容易引发一致性问题和内存泄漏。
为什么不能只用 Redis 做热点缓存
Redis 是集中式缓存,所有请求都打到同一组实例。当某个 user:10086 这类 key 在 TTL 到期瞬间遭遇 5000+ QPS,即使加了互斥锁,锁竞争、网络延迟、序列化开销仍会让重建耗时拉长,部分请求超时或降级。更关键的是:Redis 本身没有“本地感知”能力,无法规避网络抖动带来的重复穿透。
常见错误现象:
- 加了
SETNX锁,但应用节点数一多,锁获取成功率骤降,大量线程陷入自旋等待 - 定时刷新任务没覆盖全部节点,部分机器缓存长期 stale
- 未做本地缓存容量控制,
ConcurrentHashMap持续 put 导致 OOM
本地缓存 + Redis 的两级结构怎么搭才稳
核心是“读优先走本地,失效后协同刷新”,不是简单地把 Redis 结果再塞进 GuavaCache 或 Caffeine。
实操建议:
- 本地缓存仅存热点 key 的「只读副本」,TTL 设为 Redis TTL 的 1/3(例如 Redis 是 30min,本地设 10min),避免本地过久 stale
- 写操作一律先更新数据库,再主动
DELRedis 中对应 key,**不更新本地缓存**——由下次读触发重建,防止写扩散 - 本地缓存启用
maximumSize+expireAfterWrite,禁用expireAfterAccess,防止冷 key 占满内存 - 在本地缓存 get 失败时,不立即查 Redis,而是先尝试用
tryLock("local:reload:" + key)控制本机最多一个线程去加载,其余线程 sleep(50ms) 后重试本地
如何让两级缓存的数据真正一致
一致性难点不在“谁先更新”,而在“谁来通知失效”。不要依赖被动轮询或固定间隔 reload。
推荐做法:
- 所有业务写操作完成后,发一条 MQ 消息(如
cache-invalidate:user:10086),各节点监听后调用localCache.invalidate("user:10086") - Redis 层对热点 key 使用逻辑过期(value 封装为
{"data":{...},"expireAt":1743990000}),本地缓存读到该结构后,自行比对expireAt决定是否触发 reload - 禁止跨服务共享本地缓存实例;每个 JVM 独立维护,靠事件驱动同步,而非状态同步
容易被忽略的点:MQ 消息必须保证至少一次投递,且本地 invalidate 要幂等;否则一次消息丢失或重复,就会导致某台机器本地缓存永久错乱。

















