文章目录
分布式缓存一致性(Redis、MySQL)
1. 前言
- 分布式一致性的问题,既是指“如何保证分布式多个节点的数据一样、没有信息差异”。通常会通过各类算法方案来保证一致性,例如Paxos、Raft、ZAB等。(我的一份简易记录)
- 分布式缓存一致性,通常是谈论一个节点中的缓存与另一个节点的原始数据如何维持一致性(或多个节点)。
- 在这里按照最常用的软件来做分析:Redis + MySQL

- 通常由于多个Service的高并发请求,会导致Redis中缓存的数据与MySQL中的数据不一致,这也就是需要解决的问题。
2. 常见方案的问题点
2.1 先更新数据库,再更新缓存
- 若存在如下逻辑,则会出现不一致的情况
- 示意图

- 逻辑步骤
Service 1需要更新数据,更新MySQL数据库,设置value = AService 2需要更新数据,更新MySQL数据库,设置value = BService 2更新Redis缓存库,设置value = BService 1更新Redis缓存库,设置value = A- 最终,
MySQL中value = B,Redis中value = A,产生不一致的问题
2.2 先删除缓存,再更新数据库
- 若存在如下逻辑,则会出现不一致的情况
- 示意图

- 逻辑步骤
Service 1需要更新数据,删除Redis的缓存值Service 2需要查询数据,查询Redis的缓存值,无值Service 2查询MySQL数据库,得到旧值AService 1更新MySQL数据库,设置value = BService 2更新Redis缓存库,设置value = A- 最终,
MySQL中value = B,Redis中value = A,产生不一致的问题
2.3 先更新数据库,再删除缓存
-
若存在如下逻辑,则会出现不一致的情况
-
示意图

-
逻辑步骤
Redis库中的缓存失效,过期或被更前一个Service删除Service 2需要查询数据,查询Redis的缓存值,无值Service 2查询MySQL数据库,得到旧值AService 1需要更新数据,更新MySQL数据库,设置value = BService 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数据库、更新缓存、删除缓存
- 情况 1
- 因此不会存在不一致的问题
- 因为
4.3 分布式架构(优化)
-
问题点 1
- 仔细看上面
一致性服务的执行逻辑就会发现:所有消息都是有顺序的,不相关的缓存之间也会进行阻塞。 - 其实,我们只需要保证同一个
key对应的缓存的一致性即可。因此,我们可以多分几个队列,只要保证同一个key的所有消息进入同一个队列即可(利用hash取模)。
- 仔细看上面
-
问题点 2
- 另外,在分布式并发请求的情况下,可能队列中会同时收到多个
缓存失效、更新缓存的消息,部分步骤是没必要重复做的。例如,连续多条针对同一个key的更新缓存的消息,更新一次了后,没必要重新再做“从MySQL查询,并更新Redis”的操作,除非该key在MySQl库中的值变了。 - 因此,我们可以为每个
key维护一个布尔值的flag- 处理
缓存失效的消息时,检查flag- 如果为 true,那么表示该
key已存在的缓存,进行删除缓存操作,最后设置flag为 false - 如果为 flase,那么表示
key的缓存不存在,可以不进行缓存删除操作,但是还是建议执行删除缓存操作(确保一定失效)
- 如果为 true,那么表示该
- 处理
更新缓存的消息时,检查flag- 如果为 true,那么表示前面已有消息更新了该
key的缓存,直接不做处理 - 如果为 flase,那么表示
key的缓存不存在,需要“从MySQL查询,并更新Redis”,最后将该flag设置为 true
- 如果为 true,那么表示前面已有消息更新了该
- 处理
- 另外,在分布式并发请求的情况下,可能队列中会同时收到多个
-
示意图

-
描述
- 由于
Service是并发的,因此会发送各种消息到一致性服务 - 根据对
key的hashcode取模,模以队列个数,可以知道该key会进入哪个队列,从而保证同一个key进入同一个队列,保证了该key消息的顺序性 - 消息进入队列后,会被按顺序处理,处理时根据
key对应的flag来决定后续逻辑(见问题点2)
- 由于
-
注意
- 显然该
一致性服务可以是单个节点,同时还可以做成HA架构 - 为了保证健壮性、处理量,我们还可以直接利用分布式的消息队列来实现,
一致性服务中的每个队列即对应分布式消息队列的一个分区(单个分区内是有序的)!例如 RocketMQ/Kafka + Flink。
- 显然该

本文深入探讨了在高并发场景下,Redis与MySQL间缓存一致性问题的常见解决方案及其潜在问题。通过对不同方案的分析,包括先更新数据库再更新缓存、先删除缓存再更新数据库等,提出利用缓存过期时间、异步延迟删除、消息队列及分布式架构等策略,以实现高效且稳定的一致性保障。
&spm=1001.2101.3001.5002&articleId=107451485&d=1&t=3&u=7f3582cbc66a408ea527fff3d5f167b4)
1291

被折叠的 条评论
为什么被折叠?



