数据库和缓存不一致性的问题,如何解决?
回答
-
为了保证 Redis 和 数据库 的一致性,肯定要缓存和数据库双写。
-
业内常见的 3 种方案:
-
先更新数据库,再删除缓存
-
延迟双删
先删除缓存,
再更新数据库,
再删除一次缓存
-
cache-aside
更新数据库,基于 bin log 监听进行缓存删除
-
扩展
1、为什么要删缓存,而不是更新?
-
优先选择删除缓存,而不是更新缓存。
-
更新缓存的动作,相比于直接删除缓存。操作过程比较的复杂,而且,也容易出错。
更新缓存的例子:当我们需要通过缓存进行扣减库存的时候。
-
你可能需要从缓存中查出整个订单模型数据,
-
把他进行反序列化之后,再解析出其中的库存字段,
-
把他修改掉,然后再序列化,
-
最后,再更新到缓存中可以看到
-
-
缓存删除 带来的一个小问题
缓存删除会带来一次 cache miss。
这种 cache miss 在某种程度上,可能会导致缓存击穿,
通过加锁的方式,比较方便的解决缓存击穿的问题的。
2、先写数据库,还是先删缓存?
-
先删缓存,再写数据库
-
优点:先删除缓存成功了,但是,第二步更新数据库失败了,这种情况是可以接受的。
-
缺点:这种方式,会无形中放大 "读写并发",导致的数据不一致的问题。
对于一个读线程来说,虽然,不会写数据库,但是,会更新缓存的。
数据不一致的问题,示例:
假如一个读线程,在读缓存的时候没查到值,他就会去数据库中查询。
如果,在查询到结果之后,更新缓存之前,数据库被更新了。
但是,这个读线程是完全不知道的。
那么,就导致最终缓存会被重新用一个"旧值"覆盖掉。
这样,就导致了缓存和数据库的不一致的现象
-
-
先写数据库,再删缓存
- 好处1:缓存删除失败的概率还是比较低的。
- 好处2:数据库是作为持久层存储的。先更新数据库,就可以保证数据的可靠性和一致性。
- 问题:先写数据库,后删除缓存,如果,第二步失败了。会导致数据库中的数据已经更新,缓存还是旧数据。最终,导致数据不一致。
-
延迟双删
- 参考:延迟双删?
3、如何选择?
-
业务量不大,并发不高的情况,可以选择**"先更新数据库,后删除缓存"**的方式,因为,这种方案更加简单。
先操作数据库,后操作缓存。是一种比较典型的设计模式一 Cache Aside Pattern
这种模式的主要方案就是,先写数据库,后删缓存。而且,缓存的删除是可以在旁路异步执行的。
这种模式的优点,他可以解决 "写写并发"导致的数据不一致问题 ,并且,可以大大降低"读写并发"的问题。
所以,这也是Facebook比较推崇的一种模式。
-
业务量比较大,并发度很高的话 ,那么建议选择 "先删除缓存,再更数据库" 。因为,这种方式在引入延退双删、分布式锁 等机制,会使得整个方案会更加趋近于完美,带来的并发问题更少 。当然,也会更复杂。
4、方案再优化
- Cache Aside Pattern 这种模式中,我们可以异步的在旁路处理缓存。
- 实现方式:借助数据库的 bin log 或者 基于异步消息订阅的方式。
- 先操作数据库,数据库操作完,发一个异步消息出来。
- 然后,再由一个监听者在接到消息之后,异步的把缓存中的数据删除掉。
- 或者,借助数据库的 binlog,订阅到数据库变更之后,异步的清除缓存。
- 再完美一点(按实际情况评估,是否需要此方案)
- 先删缓存
- 再更新数据
- 再监听binlog删除缓存
5、缓存更新的设计模式
-
Read/Write Through Pattern
-
Read Through 模式
是由缓存配置一个读模块,它知道如何将数据库中的数据写入缓存。
在数据被请求的时候,如果未命中,则将数据从数据库载入缓存。
-
Write Through 模式
缓存配置一个写模块,它知道如何将数据写入数据库。
当应用要写入数据时,缓存会先存储数据,并调用写模块将数据写入数据库
-
这两种模式下,不需要应用自己去操作数据库,缓存自己就把活干完了。
-
-
Write Behind Caching Pattern
- 在更新数据的时候,只更新缓存,而不更新数据库。然后,再异步的定时的,把缓存中的数据持久化到数据库中。
- 优点/缺点:读写速度都很快,但是,会造成一定的数据丢失。
- 适合场景:可以用在比如:统计文章的访问量、点赞等场景中。允许数据少量丢失,但是,速度要快。
6、没有银弹
- 任何的技术方案,都是一个权衡的过程。
- 没有一个"完美"的方案,只有"合适"的方案。