Redis和数据库缓存一致性问题之我见

Redis和数据库缓存一致性问题之我见

一个经典的问题,redis经常被用来当作缓存,那么redis缓存一致性怎么解决?翻阅了网上很多资料,答案不一,这里简单整理一下我的看法。

1 先操作缓存,后操作数据库

1.1 先删缓存,再更新数据库

问题 脏写

在并发的情况下,可能出现以下情况的问题

image-20210316155844132

解决方案

延时双删

思路是,为了避免更新数据库的时候,其他线程从缓存中读取不到数据,就在更新完数据库之后,再 Sleep 一段时间,然后再次删除缓存。

  1. 线程1删除缓存,然后去更新数据库。
  2. 线程2来读缓存,发现缓存已经被删除,所以直接从数据库中读取,这时候由于线程1还没有更新完成,所以读到的是旧值,然后把旧值写入缓存。
  3. 线程1,根据估算的时间,Sleep,由于 Sleep 的时间大于线程2读数据+写缓存的时间,所以缓存被再次删除。
  4. 如果还有其他线程来读取缓存的话,就会再次从数据库中读取到最新值。

image-20210322170011627

一定程度下可以保证一致性了,但很很离谱的一点在于要“估算时间”,因为有这种不确定的因素在,可能还是会出问题。

比如估算的时间太短了,还是先删除缓存,再更新旧数据了。

在比如多个线程一起延时双删,还是会有问题。

2 先操作数据库,后操作缓存

简单来说有两种方式:

  • 先更新数据库,再删缓存
  • 先更新数据库,再更新缓存

不管会不会出问题,先说说哪种比较好,答案当然是——看情况

  • 情况一:删除好

    举个例子:如果数据库 1 小时内更新了 1000 次,那么缓存也要更新 1000 次,但是这个缓存可能在1小时内只被读取了 1 次,那么这 1000 次的更新有必要吗?这种情况下,如果是删除的话,就算数据库更新了 1000 次,那么也只是做了 1 次缓存删除,只有当缓存真正被读取的时候才去数据库加载

  • 情况二:更新好

    举个例子:读请求真的很多,远远大于写请求,我们假设不从缓存拿数据的话,会消耗长达n分钟的时间。如果这时候用的是删除策略,那么必定会出现很多读请求一起打到数据库中,负载非常大。而更新的话,至少能保证缓存里有东西吧,不管缓存里的东西对不对,至少用户不会面对“白屏”。

接下来分析以下这两种方式可能出现的问题

2.1 先更新数据库,再更新缓存

问题1 弱一致性

由于磁盘I/O速度慢,在更新数据库、更新缓存这段操作之前,其他线程读取到的都是原本缓存中的旧值。

解决方案

如果不要求强一致性,那没啥问题

如果要求这么强的一致性,那在更新DB前更新一下缓存

问题2 更新失败

更新数据库成功,如果更新缓存失败或者还没有来得及更新,那么,其他线程从缓存中读取到的就是旧值,还是会发生不一致

可以引入消息队列,并进行重试,保证成功

问题3 脏写

可能会出现缓存“脏写”造成的脏数据

image-20210316160008939

解决方案:

  • 方法一:

    干脆不要用更新的方式,直接删除,这样两个删除的前后顺序就不重要了

  • 方法二:

    使用消息队列

    先更新数据库,成功后往消息队列发消息,消费到消息后再删除缓存,借助消息队列的重试机制来实现,达到最终一致性的效果

    也可以引入消息中间件,监听 binlog 消息,因为数据库的binlog是严格按照顺序的,这样就可以做到将更新顺序串行化

image-20210322172340948

image-20210322172332293

2.2 先更新数据库,再删缓存

问题1 删除失败

其实和先更新数据库,再更新缓存的问题2一样

更新数据库成功,如果更新删除失败或者还没有来得及删除,那么,其他线程从缓存中读取到的就是旧值,还是会发生不一致

可以引入消息队列,并进行重试,保证成功

问题2 脏写

写操作先更新数据库,更新成功后使缓存失效

image-20210316155932689

解决方法

这种问题其实有点无解,因为读数据库实际上是不会产生binlog的,所以我们即使引入了消息队列,也无法保证最后的那个“写缓存”操作什么时候到来。

值得一提的是,这种问题其实在以上的任何情况,都有可能出现。

个人认为一个比较好的解决方案就是加锁,比如读数据库的A的时候,我们使用for update加锁,将“读数据库、写缓存”看作一个完整的事务,“更新数据库、删缓存”看作一个完整的事务,通过加锁来保证原子性。

其实这样加锁以后,“先删缓存、后改数据库的并发问题也可以迎刃而解了

image-20210316155844132

3 个人总结

个人认为一个比较好的方法是先更新后删除,配合读数据库加锁,如果有必要的话可以引入消息队列

4 番外:从借鉴操作系统的一些方法

write through

CPU向cache写入数据时,同时向memory(后端存储)也写一份,使cache和memory的数据保持一致。

其实就是上面提到的这些操作

write back

cpu更新cache时,只是把更新的cache区标记一下(脏位),并不同步更新memory(后端存储)。只是在cache区要被新进入的数据取代时,才更新memory(后端存储)

其实是一个挺好的方法,但问题是redis在淘汰的时候我们并不能感知到,而OS的话替换是也是OS操作的,是可以感知到的

另外一个问题就是,要引入额外字段

参考

面试官:Redis 缓存一致性问题怎么解决,这样回答简直完美https://www.cnblogs.com/cmt/p/14553189.html

Redis 缓存常见问题:缓存一致性的解决方案https://blog.csdn.net/qq_35423154/article/details/112431225

原文地址:https://www.cnblogs.com/cpaulyz/p/14606622.html