缓存一致性问题就是指:当数据发生变更时,缓存中的数据和数据库中的数据一致性的问题。
主要有两种策略:超时剔除策略和主动更新策略。
其中超时剔除策略适用于低一致性需求,通过设置 Redis 的键值对过期时间实现。主动更新策略适用于高一致性需求。
超时剔除策略
适用于低一致性需求(很少发生变更的数据)。
通过设置 Redis 的过期时间实现。
主动更新策略
适用于高一致性需求(常发生变更的数据)。
通过程序员编码来实现。
更新缓存
如果是先更新数据库再更新缓存,这种情况下相比于删除缓存,会大大提高缓存的命中率。
但如果在写多读少的情况下,可能还没读到缓存,缓存就又被更新了,缓存性能消耗比较大。
在读多写少的情况下,推荐使用删除缓存,因为写少,删缓存的次数也会减少。
先删除缓存,后更新数据库
若先删除缓存,再更新数据库,会出现读写并发问题,可以使用延时双删解决。
延时双删: 先删除缓存,在更新数据库后,线程睡眠一段时间,第二次再次删除缓存。
为什么要延时删除?
如果第二次删的过快,在第二次删除过后,新的线程查询旧数据后才把旧数据写入到缓存了,此时仍存在数据不一致的问题。
为什么要删除两次缓存?
当线程1第一次删除缓存,没来得及更新数据库时,线程2查询缓存没命中,就去查询数据库中的数据,并写入缓存,此时线程1将数据库的值更新,此时会出现缓存和数据库不一致的情况。
延时双删存在的问题:
-
延时双删需要把控第二次删除延迟的时间,不然可能会出现第二次删完了,另一个线程才开始查询缓存写入旧值。
-
在删除缓存后,此时再来别的请求查不到缓存,会去数据库中找,如果大量的请求同时进来,会出现缓存击穿的问题。
先更新数据库,后删除缓存
如上图,可能会存在脏数据的情况,但几率很小,因为update语句涉及 MVCC,加锁,所以很慢。而select不涉及加锁,速度很快。
例如 Spring Cache 就是使用的这种方式。
但这种情况下,如果删缓存失败,就会造成数据库和缓存不一致的问题。
如何选择
一般情况下,因为业务中对于缓存一般不要求强一致性,所以选择先更新数据库再删缓存,因为一个服务中接口的 RT 是不好确定的,每次响应时间也不一样,所以延迟双删等待第二次删除的时间不好确定。
上述无论哪种方法,都不能保证强一致性,如果要保证强一致性,应该使用锁,但这会影响系统的性能。
解决删除缓存失败问题
删除缓存这一步操作是有可能失败的,可以通过消息队列 + Canal 订阅数据库变更日志 binlog解决,利用了消息队列的异步重试机制。
使用 Canal 中间件,用于监听数据库的 binlog,一旦数据库发生变更,binlog 日志就会发生变更,Canal 会自动投递消息到 mq,然后消费者收到消息后删除缓存,借助消息队列的重试机制来实现。