03.08 如何解决Redis缓存和MySQL数据一致性的问题?

1029417316


在高并发的业务场景下,数据库的性能瓶颈往往都是用户并发访问过大。所以,一般都使用redis做一个缓冲操作,让请求先访问到redis,而不是直接去访问MySQL等数据库。从而减少网络请求的延迟响应

数据为什么会不一致

这样的问题主要是在并发读写访问的时候,缓存和数据相互交叉执行。

一、单库情况下

同一时刻发生了并发读写请求,例如为A(写) B (读)2个请求

  1. A请求发送一个写操作到服务端,第一步会淘汰cache,然后因为各种原因卡主了,不在执行后面业务(例:大量的业务操作、调用其他服务处理消耗了1s)。

  2. B请求发送一个读操作,读cache,因为cache淘汰,所以为空

  3. B请求继续读DB,读出一个脏数据,并写入cache

  4. A请求终于执行完全,在写入数据到DB

    总结:因最后才把写操作数据入DB,并没同步。cache里面一直保持脏数据

    脏数据是指源系统中的数据不在给定的范围内或对于实际业务毫无意义,或是数据格式非法,以及在源系统中存在不规范的编码和含糊的业务逻辑。

二、主从同步,读写分离的情况下,读从库而产生脏数据

  1. A请求发送一个写操作到服务端,第一步会淘汰cache

  2. A请求写主数据库,写了最新的数据。

  3. B请求发送一个读操作,读cache,因为cache淘汰,所以为空

  4. B请求继续读DB,读的是从库,此时主从同步还没同步成功。读出脏数据,然后脏数据入cache

  5. 最后数据库主从同步完成

    总结:这种情况下请求A和请求B操作时序没问题,是主从同步的时延问题(假设1s),导致读请求读取从库读到脏数据导致的不一致

根本原因:

单库下,逻辑处理中消耗1s。可能读到旧数据入缓存

主从+读写分离,在1s的主从同步时延中。读到从库的旧数据入缓存


数据优化方案

一、缓存双淘汰法

  1. 先淘汰缓存

  2. 再写数据库

  3. 往消息总线esb发送一个淘汰消息,发送立即返回。写请求的处理时间几乎没有增加,这个方法淘汰了缓存两次。因此被称为“缓存双淘汰法“,而在消息总线下游,有一个异步淘汰缓存的消费者,在拿到淘汰消息在1s后淘汰缓存,这样,即使在一秒内有脏数据入缓存,也能够被淘汰掉。

二、异步淘汰缓存

上述的步骤,都是在业务线里面执行,新增一个线下的读取binlog异步淘汰缓存模块,读取binlog总的数据,然后进行异步淘汰。

1.思路:

MySQL binlog增量发布订阅消费+消息队列+增量数据更新到redis

1)读请求走Redis:热数据基本都在Redis

2)写请求走MySQL: 增删改都操作MySQL

3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis

2.Redis更新

1)数据操作主要分为两块:

  • 一个是全量(将全部数据一次写入到redis)

  • 一个是增量(实时更新)

这里说的是增量,指的是mysql的update、insert、delate变更数据。

这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。就无需在从业务线去操作缓存内容



PHP智慧与能力


Redis缓存和MySQL数据一致性的问题,相信大家在大量使用redis缓存进行业务开发的场景下是一定要考虑的问题。

总有运营、产品、测试人员过来问你为什么我刚刚更新了一条数据,APP上还是原来的数据呢?你总是一句话:“加了缓存,等会儿就好”。有可能是1分钟,有可能是半小时。问你的都属于关系不错,不问你直接给你提bug你也没办法。

下面就分享一下我个人在工作中对如何解决redis缓存和MySQL数据一致性的一些心得:

简单粗暴

大家看了这个图是不是就知道什么意思了?一个请求过来查询数据,我先看看redis有没有,有直接返回,没有就去数据库查出来,顺便同步到redis,设置一下过期时间。下次同样的数据查询redis缓存就可以直接返回了。是不是很简单很粗暴?在实时性要求不高的场景下,这种方式我估计是大家最常用的一种方式。但是他有几个问题:

  1. 无法保证一致性:数据库更改了数据,redis里的数据就和数据库不一致了,产生脏读;

  2. 缓存雪崩:这种方案要求redis里的缓存必须设置有效期,如果在同一时间大面积过期,所有请求压力都指向数据库,这个时候数据库顶不住压力就会宕机,然后整个世界都安静了;

  3. 缓存穿透:查询一个数据,缓存没有,去查询数据库,数据库也没有,怎么办?这样的请求多了对数据库也是压力,没有数据的时候也需要在redis缓存一个空值。

缓存同步

针对第一种方案的问题,那么大多数时候我们会继续做一个job,去定时同步数据库里的数据都redis缓存中,我们的业务请求直接查询redis缓存,无论有或者没有数据都直接返回结果。这样可以避免缓存穿透、缓存雪崩等问题,也能缓解redis缓存和数据库不一致的情况,但是还无法彻底解决一致性的问题。在job的间隔期内对数据的修改必须要等到下一次job的运行。

缓存同步加强版

针对上面的2中方案的问题,我们又搞了第三种方案,简单概括就是:“实时刷新、定时同步”八字方案。具体如下:

  1. 增加一个cache中间件,专门处理各类缓存同步事件,采用消息队列机制保证每一次数据的操作都能够刷新对应的缓存数据;

  2. 采用elastic-job做定时的增量/全量数据同步;

  3. 服务请求大部分走redis,高并发API只走缓存,不允许流量进入数据库层面;

以上三种方案我们都有采用,即使现在更多采用第三种方案,但是前两种我们依旧在用,不同的场景采用不同的方案。三种方案的结合基本上能满足我们的业务在数据一致性的需求。

大家觉得第三种方案在解决redis缓存和MySQL数据一致性方面有没有什么问题?还有其他更好的办法吗?欢迎评论区交流讨论~


java架构设计


对软件开发同学来说,这个场景太常见了。基本的思路是:mysql数据发生变更的时候,要及时清除redis缓存。那究竟要怎样清除呢?

分两种情况来说吧。

单集群。在redis集群和mysql集群都是单集群部署的情况下,需要确保如果redis查不到数据读取mysql主库来填充数据,此时只需要在mysql变更生效后,直接调用redis清缓存即可。

多集群。在redis集群和mysql集群都是多地多机房的部署情况下。采用单集群清缓存策略清除本机房redis集群缓存后,延迟消息通知其他机房redis集群(确保其他机房mysql集群已收到本机房mysql集群的binlog变更消息并将变更生效)。

希望能解答您的问题,喜欢的话可以关注下我哈


分享到:


相關文章: