文章目录
- 分布式缓存一致性(Redis、MySQL)
- 1. 前言
- 2. 常见方案的问题点
- 2.1 先更新数据库,再更新缓存
- 2.2 先删除缓存,再更新数据库
- 2.3 先更新数据库,再删除缓存
- 3. 维护一致性
- 3.1 设置缓存过期时间
- 3.2 异步延迟删除
- 3.3 利用消息队列来异步处理
- 3.4 利用Canal监控MySQL,来做异步处理
- 4. 维护一致性——拓展思考
- 4.1 思考
- 4.2 分布式架构
- 4.3 分布式架构(优化)
分布式缓存一致性(Redis、MySQL)
1. 前言
- 分布式一致性的问题,既是指“如何保证分布式多个节点的数据一样、没有信息差异”。通常会通过各类算法方案来保证一致性,例如Paxos、Raft、ZAB等。(我的一份简易记录)
- 分布式缓存一致性,通常是谈论一个节点中的缓存与另一个节点的原始数据如何维持一致性(或多个节点)。
- 在这里按照最常用的软件来做分析:Redis + MySQL
- 通常由于多个Service的高并发请求,会导致Redis中缓存的数据与MySQL中的数据不一致,这也就是需要解决的问题。
2. 常见方案的问题点
2.1 先更新数据库,再更新缓存
- 若存在如下逻辑,则会出现不一致的情况
- 示意图
- 逻辑步骤
-
Service 1
需要更新数据,更新MySQL
数据库,设置value = A
-
Service 2
需要更新数据,更新MySQL
数据库,设置value = B
-
Service 2
更新Redis
缓存库,设置value = B
-
Service 1
更新Redis
缓存库,设置value = A
- 最终,
MySQL
中value = B
,Redis
中value = A
,产生不一致的问题
2.2 先删除缓存,再更新数据库
- 若存在如下逻辑,则会出现不一致的情况
- 示意图
- 逻辑步骤
-
Service 1
需要更新数据,删除Redis
的缓存值 -
Service 2
需要查询数据,查询Redis
的缓存值,无值 -
Service 2
查询MySQL
数据库,得到旧值A
-
Service 1
更新MySQL
数据库,设置value = B
-
Service 2
更新Redis
缓存库,设置value = A
- 最终,
MySQL
中value = B
,Redis
中value = A
,产生不一致的问题
2.3 先更新数据库,再删除缓存
- 若存在如下逻辑,则会出现不一致的情况
- 示意图
- 逻辑步骤
-
Redis
库中的缓存失效,过期
或被更前一个Service删除
-
Service 2
需要查询数据,查询Redis
的缓存值,无值 -
Service 2
查询MySQL
数据库,得到旧值A
-
Service 1
需要更新数据,更新MySQL
数据库,设置value = B
-
Service 1
删除Redis
的缓存值 -
Service 2
更新Redis
缓存库,设置value = A
- 最终,
MySQL
中value = B
,Redis
中value = A
,产生不一致的问题
- 注意
- 国外的“Cache-Aside pattern”,也是支持该方案的(先更新数据库,再删除缓存)。其原因在于,通常情况下,数据库的更新会比查询慢,因此"查询数据库后更新缓存"的逻辑会在“更新数据库后删除缓存”的逻辑之前执行完,最终缓存会被删除。
- 只是可能出现如上所述的小概率事件
3. 维护一致性
3.1 设置缓存过期时间
- 该方式较为简单,只要数据会过期,最终还是会保持两边的一致性
- 仍然存在两个问题点
- 过长的过期时间,会导致较长时间存在不一致性问题
- 过短的过期时间,会导致频繁查询MySQL数据库
3.2 异步延迟删除
- 针对导致“先更新数据库,再删除缓存”方案出现不一致的小概率事件(
更新Redis
在删除Redis
之后),我们还可以进行延迟删除,也就是说更新MySQL数据库后,我们可以等几秒(异步)再删除Redis的缓存。 - 这样,就能保证
删除Redis
在更新Redis
之后。
3.3 利用消息队列来异步处理
- 在前面所说的方案中,“先更新数据库,再删除缓存”属于最优方案。但除开并发导致的顺序问题外,其实还有存在删除缓存失败的可能(例如Redis挂了,在恢复中)。
- 如果删除缓存失败,就会导致数据不一致,那么你可以
- 删除失败,那就不管了(导致数据不一致)
- 删除失败,那就一直重试(阻塞,影响业务)
- 删除失败,那就多次重试(较小的影响业务,超过次数后依然失败的话,还是会导致不一致)
- 删除失败,那就异步重试删除(不影响当前业务)
- 显然“异步重试删除”更好,其通常的方案如下
- 在本节点构建一个消息队列,负责异步重试删除缓存
- 在其他节点构建一个缓存删除服务,重试删除缓存
- 将消息发往消息中间件(RocketMQ、RabbitMQ等),使用其他程序接收中间件的数据,进行异步删除
- 其实这几方案的逻辑都比较相似,其主要逻辑图如下
3.4 利用Canal监控MySQL,来做异步处理
- 流程图如下
4. 维护一致性——拓展思考
4.1 思考
- 前面部分针对现网络上常见的方案,进行了描述与解析,基本上已经能解决缓存一致性问题。
- 但是,我们可以做一些拓展思考
- 能否进行进一步解耦呢?业务
Service
不直接负责Redis
缓存的更新。 - 能否做一个一致性维护的服务呢?有一个服务来专门维护MySQL与Redis的一致性,保证顺序性。
4.2 分布式架构
- 示意图
- 描述
-
一致性服务
,负责缓存的更新、删除,保证执行的顺序性,如图中的蓝色部分 - 任何业务
Service
查询数据,都只从Redis
缓存库中获取,如图中黄色部分
-
Service 1
开始查询数据,查询Redis
的缓存值,无值 -
Service 1
发送更新缓存的消息
到一致性服务中,本节点继续轮询Redis
缓存库(或监听一致性服务
) -
一致性服务
获得消息,查询MySQL
数据库中的数据 -
一致性服务
利用查到的数据,更新Redis
库中的缓存 -
Service 1
最终从Redis
查得数据
- 任何业务
Service
更新数据,都从MySQL
数据库中更新,其后不负责删除数据,如图中橙色部分
-
Service 2
需要更新数据,更新MySQL
数据库 -
Service 2
发送缓存失效的消息
到一致性服务中,本节点继续执行其他代码 -
一致性服务
获得消息,删除Redis
库中的缓存
- 注意
- 因为
一致性服务
中队列的顺序性,因此一条消息执行完成后,才会执行下一条
- 情况 1
- 队列顺序:“缓存失效”、“更新缓存”
- 执行顺序:删除缓存、查询MySQL数据库、更新缓存
- 情况 2
- 队列顺序:“更新缓存”、“缓存失效”
- 执行顺序:查询MySQL数据库、更新缓存、删除缓存
- 因此不会存在不一致的问题
4.3 分布式架构(优化)
- 问题点 1
- 仔细看上面
一致性服务
的执行逻辑就会发现:所有消息都是有顺序的,不相关的缓存之间也会进行阻塞。 - 其实,我们只需要保证同一个
key
对应的缓存的一致性即可。因此,我们可以多分几个队列,只要保证同一个key
的所有消息进入同一个队列即可(利用hash取模)。
- 问题点 2
- 另外,在分布式并发请求的情况下,可能队列中会同时收到多个
缓存失效
、更新缓存
的消息,部分步骤是没必要重复做的。例如,连续多条针对同一个key
的更新缓存
的消息,更新一次了后,没必要重新再做“从MySQL查询,并更新Redis”的操作,除非该key
在MySQl库中的值变了。 - 因此,我们可以为每个
key
维护一个布尔值的flag
- 处理
缓存失效
的消息时,检查flag
- 如果为 true,那么表示该
key
已存在的缓存,进行删除缓存操作,最后设置flag
为 false - 如果为 flase,那么表示
key
的缓存不存在,可以不进行缓存删除操作,但是还是建议执行删除缓存操作(确保一定失效)
- 处理
更新缓存
的消息时,检查flag
- 如果为 true,那么表示前面已有消息更新了该
key
的缓存,直接不做处理 - 如果为 flase,那么表示
key
的缓存不存在,需要“从MySQL查询,并更新Redis”,最后将该flag
设置为 true
- 示意图
- 描述
- 由于
Service
是并发的,因此会发送各种消息到一致性服务
- 根据对
key
的hashcode取模,模以队列个数,可以知道该key
会进入哪个队列,从而保证同一个key
进入同一个队列,保证了该key
消息的顺序性 - 消息进入队列后,会被按顺序处理,处理时根据
key
对应的flag
来决定后续逻辑(见问题点2)
- 注意
- 显然该
一致性服务
可以是单个节点,同时还可以做成HA架构 - 为了保证健壮性、处理量,我们还可以直接利用分布式的消息队列来实现,
一致性服务
中的每个队列即对应分布式消息队列的一个分区(单个分区内是有序的)!例如 RocketMQ/Kafka + Flink。