zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    发布TrajStat 1.4.4
    Dubbo原理解析-监控
    systemctl 命令完全指南
    Spring Boot 性能优化
    试用阿里云RDS的MySQL压缩存储引擎TokuDB
    编译安装 Centos 7 x64 + tengine.2.0.3 (实测+笔记)
    使用ssh公钥实现免密码登录
    Spring Boot Admin Reference Guide
    zookeeper集群搭建设置
    dubbo服务者配置说明
  • 原文地址:https://www.cnblogs.com/cpaulyz/p/14606622.html
Copyright © 2011-2022 走看看