讲 Mysql 和 Redis 如何保证数据一致性
1,问题背景
当我们数据库性能有瓶颈时,一般我们使用缓存对热点数据进行分离,减轻数据库压力。当我们引入缓存机制(Redis)的时候,我们业务逻辑就是这样子的:
对于读请求来讲,先从缓存查询,查得到直接返回,查不到则去数据库查,然后再更新缓存。(注意这里查不到指的是缓存过期。对于非热点数据我们一般直接查库。)
对于写请求来讲,我们是先写数据库还是先写缓存呢?下面我们详细的分析一下这种情况:
2,问题分析
1,先更新数据库再更新缓存
首先说结论,不行。比如写写并发时会出现问题,原因在于事务的数据库写和缓存写无法保证原子性。比如 A 写此时 B 写 B 更新缓存 A 更新缓存,最后的状态是 A 的缓存和 B 的数据库。缓存和数据库不一致。
2,先更新缓存再更新数据库:
不行。写写并发时也会出问题,原因同上。
究其原因,此种方案在并发情况下会出现问题,因为对数据库和缓存的操作不是原子性的。怎么办?这种情况,我们只能使用分布式锁,写写串行化的方式,保证双写一致性。
我们来看另外一种方案,删除缓存而非更新缓存,更新缓存是比较耗时的操作,不如删掉缓存由下次读取的时候再触发更新缓存。这也是一种懒加载的思想。但是,删除缓存又有了问题,是先删除缓存再更新数据库,还是先更新数据库再删除缓存?
3,先删除缓存再更新数据库
读写并发情况下,A 删缓存 B 读取 B 更新缓存 A 更新数据,此时存的是 B 的缓存 A 的数据。
这时我们可以使用延时双删策略,先删除缓存再更新数据,过一段时间再删除缓存。这个时间点很重要,我们要确保在这个时间点内完成读请求对缓存的脏数据更新操作,然后删除它。但也正是因为我们要预估好读操作的时间,才能控制好延时的策略。
但是,这种情况下,如果第二次删除失败,仍然会导致问题,我们需要使用重试机制,比如基于消息队列的重试机制,直至成功,否则在业务上报错。
4,先更新数据库再删除缓存
读写并发情况下,A 读取 B 更新 B 删除 A 写缓存,此时的情况是,缓存存的是 A 的数据库是 B 的。但此种情况较少,因为写缓存要快于写数据库的。不存在 A 先写缓存 B 后更新还先完成的情况。
但是,这种情况下,如果删除缓存失败,也会有情况 3 的问题,我们一样要使用类似的重试机制,保证删除操作成功。
另外,这种方案每次写入数据要删除缓存,会影响缓存命中率。甚至造成缓存击穿情况。
3,总结
理论上来讲,只要我们设置了缓存过期时间,就能一定程度上避免不一致性问题。需要强调的是,这里的一致性应是最终一致性。强一致性不可取,弱一致性可取,最终一致性是折中。
-
优先推荐【先更新数据库再删除缓存+设置过期时间+重试机制】的方案; -
延时双删+重试机制 -
对于并发写的情况下,我们在【更新数据库,再更新缓存】的基础上使用分布式锁机制,保证一个请求执行的串行化。要么设置较短的过期时间。 -
基于 binlog 的订阅机制。数据和缓存的一致性?这不就是 mysql 主从复制中的一致性要求吗?所以我们可以采用订阅 binlog 的形式,比如基于消息队列异步订阅并 log,然后由 redis 消费去重做 binglog,进而保证数据库和缓存的一致性。为此而生的工具如阿里开源的 canal。
|