这几天瞎逛,不晓得在哪里瞟到了缓存的双写,就忽然想起来这块尽管简略,然而细节上还是有足够多咱们能够去关注的点。这篇文章就来具体聊聊 双写一致性。
首先咱们晓得,当初将高速缓存利用于业务当中曾经非常常见了,甚至可能跟数据库的频率并驾齐驱。你的用户量如果下来了,间接将一个裸的 MySQL 去扛住所有压力显著是不合理的。
这里的高速缓存,目前业界支流的就是 Redis 了,对于 Redis 相干的文章,之前也有聊过,在此就不赘述,感兴趣的能够看看:
- Redis 根底数据结构和用法
- Redis 数据长久化
- Redis 主从同步
- Redis Sentinel 高可用
- Redis Cluster 集群详解
额,不列出来我都没感觉对于 Redis 我竟然写了这么多 … 言归正传。
在咱们的业务中,广泛都会须要将一部分罕用的热点数据(或者说不常常变然而又比拟多的数据)放入 Redis 中缓存起来。下次业务来申请查问时,就能够间接将 Redis 中的数据返回,以此来缩小业务零碎和 数据库 的交互。
这样有两个益处,一个是可能升高数据库的压力,另一个自不必说,对雷同数据来说可能无效的升高 API 的 RT(Response Time)。
后者其实还好,升高数据库的压力显得尤为重要,因为咱们的业务服务尽管可能以较低的老本做到横向扩大,但数据库不能。
这里的不能,其实不是指数据库不能扩大。MySQL 在主从架构下,通过扩大 Slave 节点的数量能够无效的横向扩大 读申请。而 Master 节点因为不是无状态的,所以扩大起来很麻烦。
对,是很麻烦,也不是不能横向扩大。然而在那种架构下,我举个例子,主 - 主架构下,会带来很多动向不到的数据同步问题,并且对整个的架构引入了新的复杂性。
就像我在之前写的 MySQL 主从原理中提到过的一样,双主架构更多的意义在于 HA,而不是做负载平衡。
所以,雷同的数据会同时存在 Redis 和 MySQL 中,如果该数据并不会扭转,那就完满的一匹。可事实很骨感,这个数据 99.9999% 的概率是肯定会变的。
为了保护 Redis 和 MySQL 中数据的一致性,双写的问题的就诞生了。
Cache Aside Pattern
其中最经典的计划就是 Cache Aside Pattern,这套定义了一套缓存和数据库的读写计划,以此来保障缓存和数据库中的数据一致性。
具体计划
Cache Aside Pattern 具体又分为两种 Case,别离是读和写。
对于读申请,会先去 Redis 中查问数据,如果命中了就会间接返回数据。而如果没有从缓存中获取到,就会去 DB 中查问,将查问到的数据写回 Redis,而后返回响应。
而更新则绝对简略,然而也是最具备争议。当收到写申请时,会先更新 DB 中的数据,胜利之后再将缓存中的数据 删除。
留神这里是删除,而不是更新。因为理论生产中,缓存中寄存的可能不仅仅是繁多的像 true
、false
或者 1
、19
这种值。
为什么是删除
还有可能在缓存中寄存一整个构造体,其中蕴含了十分多的字段。那么是不是每次有一个字段更新就都须要去把数据从缓存中读取进去,解析成对应的构造体,而后更新对应字段的值,再写回缓存呢?又或者你是间接将原缓存删除,而后又将最新的数据写入缓存?
其实乍一看,如同没有故障。我更新难道不应该这么更新吗?在这里,咱们的关注点更多的放在了 更新的形式 上,而把 更多的必要性 给疏忽到了。咱们更新了这个值之后,在接下来的一段时间内,它会被频繁拜访到吗?可能会,但也可能基本不会被拜访到了。
那既然有可能不会被拜访到,那咱们为啥还要去更新它?而且,更新缓存所带来的开销有时侯会十分大。
然而这还只是缓存数据源繁多的状况,如果缓存中缓存的是某个读模型,其数据是通过多张表的数据计算得出的,其开销会更大。
读模型,简略了解就是用现有数据,计算、统计进去的一些数据。
这个思路就相似于 懒加载 的形式,只在须要的时候去计算它。
争议在哪儿?
后面提到过,更新时程序为先更新 DB 中的数据,胜利之后再删除缓存。然而也有人认为应该 先删除缓存,再去更新 DB 中的数据。
乍一看,可能并不能发现问题。甚至感觉还有那么一丝丝正当。因为如果先删除缓存,如果删除操作失败,DB 中的数据也不会更新,这样缓存和 DB 中数据也能保障一致性。而且,如果删除缓存胜利,但更新 DB 失败了,大不了下次获取时,再将数据写回缓存即可,能够说非常的正当。
但,这只是单线程的状况下,如果在多线程下,会间接造成致命的数据不统一。
下面的流程图具体的形容了状况,更新申请 1 刚刚把缓存中的数据删除,查问申请 2 就过去了,查问申请 2 会发现缓存中是空的,所以依照 Cache Aside Pattern 的读申请规范,会从 DB 中加载最新的数据并将其写入缓存。而此时更新申请 1 还没有对 DB 进行更新操作,所以查问申请 2 写入到缓存中的数据依然是旧数据。
这样一来,查问申请 3 在下一次更新之前,读取到的就都会是老数据。而后,更新申请 1 将最新的数据更新至 DB,缓存和 DB 的数据就 不统一 了。
其实 Cache Aside Pattern 中的模式,依然会在某些 case 下造成数据不统一。然而这个概率十分的低,因为触发这个不统一的状况的条件太刻薄了。
首先是缓存要生效,而后读申请、写申请并发的执行,并且读申请要比写申请后执行完。为啥说概率不大呢,首先在理论生产中,读申请个别都要比写申请快得多。除此之外,读申请去 DB 申请数据的工夫肯定要早于写申请,并且写缓存的工夫还要肯定晚于写申请,比起最开始的那种状况来说,条件曾经是十分的严格了。
如果齐全不能容忍,能够通过 2PC 的模式去保证数据的一致性,也能够通过将申请串行化的形式来解决,但这样的代价就是会就义并发量。
End
其实还有其余的几种计划,比方 Read Throught Pattern
、Write Through Pattern
、Write Around
、Write Behind Caching Pattern
等等。然而这些绝对于 Cache Aside Pattern
来说比较简单,能够本人去理解一下就好。
本篇文章已放到我的 Github github.com/sh-blog 中,欢送 Star。微信搜寻关注【SH 的全栈笔记 】,回复【 队列】获取 MQ 学习材料,蕴含根底概念解析和 RocketMQ 具体的源码解析,继续更新中。
如果你感觉这篇文章对你有帮忙,还麻烦 点个赞 , 关个注 , 分个享 , 留个言。