搞懂 Redis 与数据库的数据一致性,看这一篇就够了

搞懂 Redis 与数据库的数据一致性,看这一篇就够了 在日常开发中只要系统并发量稍微上来一点我们都会不自觉地想到引入 Redis 来做缓存。这本来是件好事读写速度直接起飞数据库的压力也降下来了。但是引入缓存就像是一把双刃剑它带来了一个让无数开发者头疼、也是面试官最爱问的终极问题数据库和缓存的数据不一致怎么办今天咱们就抛开那些晦涩难懂的学术名词用大白话把这个“老生常谈”的问题扒得干干净净。第一步更新缓存还是删除缓存当数据库里的数据发生变化时我们要怎么处理 Redis 里的数据拍脑袋一想无非就两种操作更新缓存数据库改了我顺手把 Redis 里的数据也改了。删除缓存数据库改了我直接把 Redis 里的这条数据删掉。等下次有人来查的时候发现缓存没有再去查数据库然后把新数据放进 Redis。结论先行在绝大多数业务场景下请直接选择“删除缓存”这也是经典的 Cache Aside Pattern 旁路缓存模式的做法。为什么不推荐“更新缓存”性能浪费有些缓存数据不是直接从数据库原封不动拿出来的而是经过了一系列复杂的计算。如果这条数据一天被修改了 100 次但没人来查你不仅白白计算了 100 次还跟数据库交互了 100 次。并发安全问题假设线程 A 和 B 同时在更新同一条数据。A 先更新数据库B 后更新数据库但在网络抖动的情况下B 可能先更新了缓存A 后更新了缓存。这时候数据库里是 B 的新数据缓存里却是 A 的老数据直接脏数据了。所以“删就完事儿了”简单粗暴且安全。等谁真正需要用到这条数据时让他自己去查数据库并把结果写回缓存也就是延迟加载的思想。第二步先动数据库还是先动缓存既然确定了策略是“删除缓存”那随之而来的就是顺序问题。方案一先删除缓存再更新数据库这个方案听起来很合理先把旧缓存干掉再慢慢去更新数据库。但如果是高并发场景极容易踩坑线程 A 请求更新数据先把缓存删了。还没等线程 A 去更新数据库线程 B 跑来查询数据。线程 B 发现缓存没了就去查数据库此时查到的是老数据。线程 B 把老数据又塞回了 Redis。线程 A 终于把数据库更新完了。结果数据库是最新的缓存里永远是老数据。凉凉。方案二先更新数据库再删除缓存这是目前业界公认的最佳实践。我们来看看这种顺序下的执行过程线程 A 更新了数据库。线程 A 删除了缓存。线程 B 来查询发现缓存没有去查数据库此时拿到的是新数据写入缓存。完美那这个方案有没有破绽呢有但概率极小。只有在读写并发极其凑巧的瞬间缓存刚好过期失效 - 线程 A 查库拿到老数据 - 线程 B 更新数据库 - 线程 B 删除缓存 - 线程 A 把老数据写回缓存。这种情况发生的条件是写数据库的操作比读数据库的操作还要快。但在实际中写库加锁、写日志等通常比读库慢得多所以这种极端情况极少发生。如果是为了应对这种极小概率事件可以考虑给缓存加上过期时间兜底方案。第三步万一第二步失败了怎么办“先更新数据库再删除缓存”看似完美但不要忘了现在的系统都是分布式的网络随时会抖动。如果数据库更新成功了但是删除缓存失败了怎么办缓存里依然是老数据不一致的问题又出现了。为了解决这个“失败”的问题我们通常有以下两种工业级解法解法 1消息队列重试机制简单有效既然删缓存失败了那我就多试几次直到成功为止。但如果用死循环去重试会阻塞当前的业务线程。所以我们可以引入消息队列比如 RabbitMQ、Kafka、RocketMQ。更新数据库。尝试删除缓存。如果删除失败把这个“删除 Redis 某某 key”的任务扔进消息队列。自己写个消费者监听这个队列不断尝试删除缓存直到成功。优点实现简单能保证最终一致性。缺点业务代码里揉进去了消息队列的逻辑对代码有侵入性。解法 2订阅 MySQL Binlog优雅解耦如果你不想在业务代码里写一堆删除缓存、发消息的逻辑可以使用阿里的开源组件Canal。Canal 的原理是把自己伪装成 MySQL 的从库Slave。当 MySQL 的数据发生变化时会生成 Binlog二进制日志。Canal 监听到 Binlog 的变化后再去触发删除 Redis 缓存的操作。业务代码只管更新数据库别的啥也不用管。Canal 监听 MySQL Binlog 发现某张表的数据变了。Canal 把变动信息丢给消息队列或者直接自己起一段逻辑去删 Redis 里的 key。如果删失败了依然利用消息队列的机制进行重试。优点对业务代码零侵入解耦得非常干净。缺点系统架构变复杂了需要额外维护 Canal 组件。补充彩蛋面试最爱问的“延迟双删”虽然刚才我们说了“先更新数据库再删除缓存”是主流但在一些历史遗留系统或者特殊场景下你可能会听到“延迟双删”这个词。这其实是为了解决**“先删缓存再更新数据库”**带来的并发问题而打的补丁。流程大概是这样的先删除缓存。更新数据库。休眠一小会儿比如 500ms。再删除一次缓存。为什么要休眠是为了等那些正在查老数据的读请求执行完然后再补一刀把他们可能写回 Redis 的老数据彻底删掉。虽然这也是个办法但在实际开发中评估“休眠多长时间”纯粹是门玄学且降低了吞吐量。所以现在的系统除非万不得已很少用这个方案了。知道这个概念用来对付面试官就行。最后总结一下保证缓存和数据库一致性记住以下三条核心准则兜底策略永远不能忘给所有的 Redis 缓存都加上过期时间TTL。就算发生了一致性问题等过期时间一到缓存自动失效数据自然就强行一致了。日常业务首推先更新数据库再删除缓存。这种方案能应对 99% 的场景。高要求场景兜底如果遇到删除缓存失败的情况使用消息队列重试或订阅 Binlog的方式保证缓存最终一定会被删除。说到底分布式系统里没有绝对的“实时强一致性”我们做的所有努力都是在追求**“最终一致性”**。理解了这一点你在处理缓存问题时就会游刃有余了。