推荐先更新数据库再删缓存,并辅以异步延迟双删和占位符防穿透;必须在事务提交后删除缓存,确保key可推导、命名规范、清理可控。

缓存更新时先删缓存还是先改数据库
多数人卡在这一步——不是技术不会,是逻辑没理清。实际生产中,先删缓存再更新数据库 和 先更新数据库再删缓存 都会出问题,但后者更可控,前提是加一层重试或延迟双删。
典型错误现象:缓存击穿 或 短暂脏数据(比如用户刚下单,查订单页还显示“未支付”)。这是因为并发读写下,DB 写完、缓存还没删,另一个请求把旧值又刷进去了。
- 推荐做法:更新 DB 后,立刻执行
DEL key;再起一个异步任务(如用消息队列或延迟任务),100–500ms 后再次DEL key - 不依赖
EXPIRE自动过期,它不可控,且无法解决“刚写完就查到旧值”的窗口期 - 如果业务对一致性要求极高(如金融类),删缓存后加个
SET key "" EX 2占位,避免缓存穿透
用什么方式触发缓存清理才可靠
靠应用层代码手动删最直接,但容易漏删、错删;监听 binlog 做自动清理更健壮,但引入额外组件(如 canal、debezium)成本高。
常见错误现象:新增字段没同步删缓存、批量更新只删了单条缓存、事务回滚后缓存已删,导致下次读空。
- 必须在 DB 事务提交成功后,再执行缓存删除——不能放在 try 块里,也不能在事务内删(Redis 连接和事务不绑定)
- 批量操作(如
UPDATE users SET status = ? WHERE type = ?)要主动构造所有可能命中缓存的key,不能只删当前主键 - 避免用正则或 scan 批量删(
KEYS pattern在线上禁用;SCAN+DEL有性能风险),宁可维护一份 key 映射表
缓存 key 设计不当导致删不干净
key 里带动态参数(比如时间戳、随机数、用户 session ID)会让清理变成盲猜。删缓存不是靠“记得”,而是靠“能推导”。
使用场景举例:用户订单列表接口返回 orders:uid:123:page:1,但更新某订单状态时,你根本不知道该删哪几个分页 key。
- 关键原则:读写 key 必须有确定性映射关系。例如订单详情用
order:id:456,列表页统一用orders:uid:123(不带分页),查的时候自己做分页裁剪 - 避免在 key 中拼接非业务主键字段(如
user:email:xxx@x.com),邮箱改了,老 key 就永远残留 - 给 key 加命名空间(如
v2:order:id:456),升级缓存结构时,直接按前缀批量清理,不用改业务逻辑
为什么用 Redis 的发布订阅做清理很危险
很多人想用 PUBLISH cache:invalidation order:456 让多个服务监听并删本地缓存,但这套机制在 Redis 里不保证送达,也不保证顺序,更没法重试。
错误现象:部分实例没收到消息、消息堆积导致延迟删缓存、重启后消息丢失。
-
PUB/SUB是 fire-and-forget 模型,没有 ACK,不适合做关键路径的缓存清理 - 如果真要用消息驱动,走独立 MQ(如 Kafka、RabbitMQ),让每个消费者自己决定删哪些 key,并支持失败重试
- 哪怕只是单机多进程,也优先用本地事件总线(如 Python 的
asyncio.Queue或 Go 的 channel),别跨网络依赖 Redis pub/sub
最常被忽略的一点:缓存清理不是“删一次就完事”,而是“删得准、删得稳、删得可追溯”。很多问题表面是 Redis 操作不对,其实是 key 命名没抽象好,或者 DB 更新和缓存动作没对齐事务边界。
















