缓存更新的套路深入理解

作为一个刚刚入行两年的小菜鸟,刚刚推出业务研发序列。面对新环境、新项目中无处不在的NoSQL,参考大佬的文章自己总结一下。大家有什么想法欢迎积极交流.
发现好多人只是把大佬的文章原封不动的抄到自己的博客,我感觉这些人应该都没有看懂吧,没有一点自己的想法,本文主要以想法为主,个人理解可能稍有偏差,还请大家指教
本文转自:http://coolshell.cn/articles/17416.html 。

1. 经典方法 – Cache Aside Pattern

  • 读操作:Consume --> Redis --> SQL 先从Redis获取,没有命中则从MySQL中获取,最后更新到Redis中
  • 写操作:Consumer --> SQL --> Redis 先写MySQL,成功后删除Redis中的数据

读写图例

1.1 为什么这样的方案会是经典方法呢?

  • 因为简单,有效,错误少! 然而这并不代表没有错误,而是一种拼命较少错误的方式。
    我们完全可以选择2PC或是Paxos协议保证一致性。但是2PC太慢,而Paxos太复杂,那么拼命的降低并发时脏数据的概率就成了我们最好的选择

1.2 当前方案存在问题和场景

  1. 读操作在Redis中没有命中时,从MySQL中获取了数据,在更新Redis数据之前,有一个写操作完成,此时MySQL的数据已经变了,而读操作会把之前的数据写入Redis中,产生脏数据
    解释: 这样的场景发生的几率非常非常小,由于MySQL锁的限制,只能发生在读操作读取数据之后读操作更新Redis数据之前,并且这段时间有一个写操作完成,同样因为锁的存在写操作一般都比读操作会耗时,并且给Redis数据增加过期时间进一步减小脏数据的产生几率。

1.3 其他方法和场景

方案1–写操作先删除Redis再操作数据库

问题场景 :写操作和读操作并发执行

  • 1.写操作: 删除了Redis中的数据,未数据更新到MySQL。
    2. 读操作:在Redis中读取到数据未命中,读取数据库。
    3. 读操作:将数据库的数据更新到Redis。
    4. 写操作: 更新数据库

结果:虽然MySQL中的数据是正确的,虽然缓存有超时时间,但是在超时时间范围内每次读取的数据都是脏数据。

方案2–写操作后增加更新Redis数据

问题场景 :双写并发执行

  • 1.写操作A更新数据库,进行加1操作。 完成数据1 --> 2的更新。
    2. 写操作B更新数据库,进行加1操作 。完成数据2 --> 3的操作。
    3. 写操作B首先更新Redis的操作 redisKey = 3 。
    4. 写操作A更新Redis的操作 redisKey = 2。
    读操作期待数据:3 获得数据:2

结果:这样读和写的操作都会更新Redis数据看似更稳健,其实不然,更新操作变多了脏数据也会变多。大家可能觉得,首先操作数据库的应该会首先操作Redis吗?当然不是!!实际情况是在两步操作中会有各种各样的业务逻辑操作,这些业务逻辑会受到机器性能,网络优劣等各方面的影响,最终一般不会按照操作数据库的顺序访问Redis,所以这种方案脏数据会很多很多

1.4 业务场景选择

  • 经典案例:先更新数据库,再删除缓存,当出现并发问题概率很小(假设概率为R1),会造成脏数据。当出现网络等问题导致删除缓存失败(假设概率为R2),会导致之后的请求一直是脏数据。
  • 方案1:先删除缓存,再更新数据库,当出现并发问题概率较大(假设概率为R3),会导致之后的请求一直是脏数据,当出现网络问题,删除缓存成功,更新数据库失败,只会引发一次cache miss,在业务上基本没啥影响。当然为了弥补,我们一般都会设置缓存的过期时间,来缩短出现脏数据的时间。
    现在问题的关键就是R1+R2和R3的大小问题。如果大厂,网络基础设施啥的比较牛,当然R1+R2<R3经典案例比较合适;对于占比更多的中小厂来说,真有可能R1+R2>R3,那怎么选择场景1未必不是更好的选择。

综上所述,虽然经典方法推荐方案虽然稍有瑕疵,但依然经典可用,因为其简单,有效,错误少

2. Read/Write Through Pattern

  • Read Through
    Read Through 套路就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出), Cache Aside是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载, 从而对应用方是透明的。读取应用方只需要关注Cache。
  • Write Through
    Write Through 套路和Read Through相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作)
    在这里插入图片描述

参考文章有个大佬提问,我想大家看到这里一定多多少少也会有这方面的疑惑,顺便讨论下
问题:为什么Write Through时,在Cache未命中,不直接更新Cache,由Cache直接同步到SQL。
我的小想法:设计者想尽可能的让缓存存放热点数据,当更新数据时,为了保证数据的快速响应,直接存放到缓存,由同步机制同步到数据库,而发现数据不在缓存后,数据为非热点数据,会直接写库。

2.1、设计思路

  • Cache Aside设计思路固然很优秀,但是应用代码需要自己维护缓存(Cache)和数据库(Repository),让人有些苦恼。试想一下,有个很简单的查询业务: 获取商品数据,正常情况只需要连接缓存(Cache),然后在这种模式的情况下,不仅需要连接Cache,还需要连接Repository并且在未命中的时候操作Cache,我要是应用开发我会很苦难。
  • Read/Write Through套路是把更新数据库(Repository)的操作由缓存自己代理了,所以,对于应用层来说,后端就是一个单一的存储,而存储自己维护自己的Cache
  • 连接关系:
    查询 – Cache
    操作-Repository

Write Behind Caching Pattern

解读:文章中缓存都是分块的,命名为Cache block。

  • 读操作:Redis命中直接返回;没有命中,同步MySQL数据到Cache,从Cache中获取数据返回
    1、命中缓存退回数据,未命中缓存执行。
    2、定位缓存存放的缓存块。
    3、缓存是否被更新(is it dirty ?)。
    (Yes)则认为是dirty(未同步数据库),这个时候就首先进行一次同步,将当前缓存中的数据同步到数据库,然后同步数据库数据到缓存Cache。
    (No)同步数据库数据到缓存Cache。
    4、并且将缓存块标记为no dirty(已同步数据库)。
    5、读取数据

问题来了,为什么要设置标记位点,判断是否被同步过呢?
解答: 此种方案存放大量的数据,当数据没有在缓存中,需要去定位缓存空间,这个缓存空间可能已经被占用,所以需要确实是否同步(惰性删除,只有获取到才会判断是否过期),没同步需要先同步到数据库,才不会完成更新丢失的问题,然后再从数据库更新到当前key的数据,然后读取数据从库到缓存。
同步机制不是实时同步,所以才会有这样的判断

  • 写操作:无论Redis命中与否都先更新Redis,接着异步更新MySQL中的数据

  • 在这里插入图片描述

这种模式写操作只针对于一个数据库(MySQL或者Redis),读数据时在Redis中没有命中会从MySQL中读取到Redis中,长时间运行后大部分数据都会在Redis中命中,写操作也会针对于Redis,相当于是强依赖于Redis,弱依赖甚至不依赖于MySQL,这么看来这种方式是最高效的,但是有一个致命的缺点,数据可能会丢失,由于强依赖于Redis中的数据,同样强依赖于Redis的高可用性,当Redis的数据丢失没有很好的灾备的话,数据就没了,类似于Redis生成RDB文件,同步时间过短影响性能,过长出现宕机等问题就会丢失数据。
总结一下,这种模式有利有弊,具体使用场景需要针对于具体的业务场景,我认为如果有比较好的Redis高可用和灾备的策略,这种模式还是非常好的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值