引入缓存,我们的很大原因是为了让经常访问而不常修改的数据快速响应,提高系统性能。除此之外还有一些对及时性、数据一致性不高的场景。
使用缓存我们还有一个问题就是,缓存的数据一致性问题,即保证数据库的数据与我们缓存的数据一致,如何解决,我们常用的解决方式有以下两种。
双写就是,写入数据库的时候,也更新缓存中的数据。如果细分析下来这两个步骤不同顺序执行也会不同效果。
数据一致性考虑主要两点:在不考虑并发问情况的异常问题,在并发情况下的不安全问题。
在不考虑并发问情况下我们考虑:
这都可能会出现业务问题。比如:更新了缓存,但更新数据库出错了,导致不一致。那如何解决?
解决的办法就是重试,详细在后面讲到。
我们还能思考一层就是更新缓存和数据库的操作容易出错吗?是否需要保证这一层的高可用?我们引入缓存是为了性能,强一致性的场景是否需要缓存呢?
而双写在并发情况下会出现以下问题

如果我们设置了缓存过期,即时出现上图的脏数据问题,数据不一致,等缓存过期,重新更新缓存,最终还是得到正确的数据,叫做最终一致性。这个过程时间不是立即的,所以适用于时效性不要求严格的场景。
当然我们也可以双写的时候加锁,避免脏数据的问题。这里的锁保证数据一致。写写互斥,读和写也要互斥关系。
缓存利用率的角度来评估这个方案,也是不太推荐的。这是因为每次数据发生变更,都更新缓存,但是缓存中的数据不一定会被马上读取,这就会导致缓存中可能存放了很多不常访问的数据,浪费缓存资源。
指的是更新数据时,删除缓存。这样访问的时候先从数据库查再更新缓存。
这和双写模式一样,解决的办法还是重试,详细在后面讲到。

出现不一致的情况,推荐先更新数据库后删除缓存因为数据库数据保证最新,那么缓存过了有效期也会最终一致。

这个问题也是一个脏数据问题,更新到第一个线程的数据,缓存数据不是最新的。这个问题如何解决呢?还是能够使用设置缓存过期,然后保证最终一致性。除此之外使用读写锁,把读和写锁住就能解决这个问题。
设置缓存的过期时间。缓存中不经常访问的数据,随着时间的推移,都会逐渐「过期」淘汰掉,最终缓存中保留的,都是经常被访问的「热数据」,缓存利用率得以最大化。
不同顺序的解决方法都一样,如下:
不考虑并发问情况出现异常,如何保证两部都成功?
使用重试,但重试需要思考下面问题
更好的是异步重试,直接把缓存的操作放消息队列上通知操作,MQ保证消息可靠,异步释放当前线程。或者订阅数据库变更日志,再操作缓存。
使用canal从MySQL的binlog获取数据,更新到缓存。

无论是双写模式还是失效模式,都会导致缓存的不一致问题(脏数据)。即多个实例同时更新会出事。怎么办?
双写模式和失效模式都一样,加上锁就能保证强一致,比如加上读写锁,通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。由于写操作是排他锁,所以会损耗一定性能,写时读就会降低并发量。我们要考虑加上了锁之后代价是否大于加上缓存的性能提升。