Redis、MySQL 如何保证数据一致性?
一般情况下,Redis 是用来实现应用和数据库之间的一个读操作的缓存层。主要目的是去减少数据库的 IO ,还可以提升数据库的 IO 性能。
整体架构:
当应用程序需要去读取某个数据的时候,首先会尝试 Redis 里面去加载,如果命中了就直接去加载,直接返回数据,如果没有命中,就查询数据库,然后再将查询到的数据写入到 redis 缓存中。
在这个架构里面呢,会出现数据一致性的问题
。
一份数据同时被保存在 redis 和 mysql 里面,当某个数据需要被更新的时候,由于更新数据是具有前后顺序的,它并不像 mysql 中的多表事务操作,可以满足 ACID 的特性。
常规解决办法只有两种:
先更新数据库,再更新缓存。
该种方案,若出现缓存失败的话,会出现数据不一致的问题。
先删除缓存,再更新数据库。
该种方案,是借助再次访问该数据的时候,发现 redis 里的数据为空,然后查询数据库再次加载。但是这两个操作并不是原子操作,所以在这个过程中,如果出现其他线程来访问,还是会存在数据不一致的问题。
那么,需要在极端情况下仍要保证 redis 和 mysql 数据一致性,就需要采用 最终一致性
的方案。
例如 基于 MQ 的可靠性消息通信
来实现数据最终的一致性。
或者,直接通过 Canel 组件,监控 mysql 中的 bingo 的日志,将更新后的数据同步到 Redis 里面。
接下来,就聊一下各种缓存方案…
一致性(consistency)
一致性就是缓存和数据库存储数据的两份数据保持一致性
- 强一致性要求:
所谓强一致性,就是对于 app 来说,缓存和数据库存储的数据读写是符合原子性的,要求读写的一致性,实现起来时对系统的影响大。
- 弱一致性:
这部分对于缓存层和数据库层数据一致性要求较低,不要求在更新数据时,缓存和数据库立即同步更新的情况,也不会要求缓存和数据库多久达到一致性,但是会尽可能保证
到某个时间级别后,数据能够达到一致性状态。 - 最终一致性:
最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。
三种经典的缓存模式
- Cache-Aside Pattern
- Read-Through/Write through
- Write behind
Cache-Aside 读流程
旁路缓存模式,它的提出是为了尽可能地解决缓存与数据库的数据不一致问题
Cache-Aside 读流程
- 读取数据的时候,先读缓存,缓存命中时,直接返回数据。
- 缓存未命中时,读取数据库,取数据的时候同时更新数据到缓存中,返回响应
Cache-Aside 写流程
更新的时候,先更新数据库,然后再删除缓存
Read-Through
Read/Write Through 模式中,服务端把
缓存
作为主要数据存储
。应用程序跟数据库缓存交互,都是通过抽象缓存层
完成的
Read-Through 的思想是采用的将缓存作为主要的存储结构,是从性能的角度出发。
Write-Through
这种模式下,当发生请求时,是由缓存抽象层完成数据源和缓存数据的更新
Write behind (异步缓存写入)
Write behind 和 Read-Through/Write-Through 相似,都是由
Cache Provider
来负责缓存和数据库看的读写。
但是 前者是同步更新缓存和数据的,Write Behind 则只是更新缓存,不直接更新数据库,通过批量异步
的方式来更新数据库。
三种模式的比较
Cache Aside 更新模式实现起来比较简单,但是需要维护两个数据存储:
- 缓存(Cache)
- 一个是数据库(Repository)
Read/Write Through 的写模式需要维护一个数据存储(Cache Provider),实现起来较为复杂一些。
Write Behind Caching 更新模式和 Read/Write Through 更新模式类似,区别是 Write Behind Caching 更新模式的数据持久化操作是异步
的,但是`Read/Write Through 更新模式的数据持久化操作是同步的
Write Behind Caching 的优点是直接操作内存速度快
,多次操作可以合并持久化到数据库。缺点是数据可能会丢失,例如系统断电等等。
Cache-Aside 问题
我们在更新数据的时候,Cache-Aside 是删除缓存呢,还是应该更新缓存?
我们在操作缓存的时候,到底是应该删除缓存还是说更新缓存呢?我们先来看个例子:
多线程情况:
以上情况就可以看出来问题:
线程 A 先发起一个写操作,先更新数据库。
线程 B 再发起写操作,更新数据库。
现在由于网络原因,线程 B 的更新缓存的操作却在 A 之后发生。
此时缓存中的数据就是与数据库中不一致了。
更新缓存相对于删除缓存还有两点劣势:
- 若写入缓存的值,是经过复杂计算才得到,更新频率较高的情况下,十分浪费性能。
- 在写多读少的情况下,也会十分浪费性能。
双写的情况下,先操作数据库还是先操作缓存?
如果遇到多线程情况,也会有类似情况,出现缓存中存储的是脏数据。因此 Cache-Aside 缓存模式,选择了先操作数据库而不是先操作缓存
两种方案保证数据库与缓存的一致性
- 删除缓存重试机制
- 读取 binlog 异步删除缓存
删除缓存重试机制
多次删除确保 cache 中存储的是正确的数据
删除缓存重试机制的大致步骤:
- 写请求更新数据库
- 缓存因为某些原因,删除失败
- 把删除失败的 key 放到消息队列
- 消费消息队列的消息,获取要删除的 key
- 重试删除缓存操作
同步 binlog 异步删除缓存
重试删除缓存机制还可以,就是会造成好多业务代码入侵。
其实,还可以通过数据库的 binlog 来异步淘汰 key
以 mysql 为例,可以使用阿里的 canal 将 binlog 日志采集发送到 MQ 队列里面,然后编写一个简单的缓存删除
消息者订阅 binlog 日志,根据更新 log 删除缓存,并且通过 ACK 机制确认处理这条更新 log,保证数据缓存一致性。
总结:
综上所述,在分布式系统中,缓存和数据库同时存在时,如果有写操作的时候,「 先操作数据库,再操作缓存 」
参考:「 https://www.cnblogs.com/crazymakercircle/p/14853622.html 」