MySQL数据库与Redis缓存双写一致性

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: MySQL数据库与Redis缓存双写一致性

问题

你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?


分析

先做一个说明,从理论上来说,有两种处理思维,一种需保证数据强一致性,这样性能肯定大打折扣;另外我们可以采用最终一致性,保证性能的基础上,允许一定时间内的数据不一致,但最终数据是一致的。


一致性问题是如何产生的?

对于读取过程:

  • 首先,读缓存;
  • 如果缓存里没有值,那就读取数据库的值;
  • 同时把这个值写进缓存中。


双更新模式:操作不合理,导致数据一致性问题

我们来看下常见的一个错误编码方式:


public void putValue(key,value){
    // 保存到redis
    putToRedis(key,value);
    // 保存到MySQL
    putToDB(key,value);//操作失败了
}


比如我要更新一个值,首先刷了缓存,然后把数据库也更新了。但过程中,更新数据库可能会失败,发生了回滚。所以,最后“缓存里的数据”和“数据库的数据”就不一样了,也就是出现了数据一致性问题。


8.png


你或许会说:我先更新数据库,再更新缓存不就行了?


public void putValue(key,value){
    // 保存到MySQL
    putToDB(key,value);
    // 保存到redis
    putToRedis(key,value);
}


这依然会有问题。

考虑到下面的场景:操作 A 更新 a 的值为 1,操作 B 更新 a 的值为 2。由于数据库和 Redis 的操作,并不是原子的,它们的执行时长也不是可控制的。当两个请求的时序发生了错乱,就会发生缓存不一致的情况。


9.png


放到实操中,就如上图所示:A 操作在更新数据库成功后,再更新 Redis;但在更新 Redis 之前,另外一个更新操作 B 执行完毕。那么操作 A 的这个 Redis 更新动作,就和数据库里面的值不一样了。

那么怎么办呢?其实,我们把“缓存更新”改成“删除”就好了


不再更新缓存,直接删除,为什么?

  • 业务角度考虑


原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。


性价比角度考虑

更新缓存的代价有时候是很高的。如果频繁更新缓存,需要考虑这个缓存到底会不会被频繁访问?


举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。


“后删缓存”能解决多数不一致


因为每次读取时,如果判断 Redis 里没有值,就会重新读取数据库,这个逻辑是没问题的。

唯一的问题是:我们是先删除缓存?还是后删除缓存?

答案是后删除缓存。


1.如果先删缓存

我们来看一下先删除缓存会有什么问题:


public void putValue(key,value){
    // 删除redis数据
    deleteFromRedis(key);
    // 保存到数据库
    putToDB(key,value);
}


10.png


就和上面的图一样。操作 B 删除了某个 key 的值,这时候有另外一个请求 A 到来,那么它就会击穿到数据库,读取到旧的值, 然后写入redis,无论操作 B 更新数据库的操作持续多长时间,都会产生不一致的情况。


2.如果后删缓存

而把删除的动作放在后面,就能够保证每次读到的值都是最新的。


public void putValue(key,value){
    // 保存到数据库
    putToDB(key,value);
    // 删除redis数据
    deleteFromRedis(key);
}


这就是我们通常说的Cache-Aside Pattern,也是我们平常使用最多的模式。我们看一下它的具体方式。


先看一下数据的读取过程,规则是“先读 cache,再读 db”,详细步骤如下:


每次读取数据,都从 cache 里读;

如果读到了,则直接返回,称作 cache hit;

如果读不到 cache 的数据,则从 db 里面捞一份,称作 cache miss;

将读取到的数据塞入到缓存中,下次读取时,就可以直接命中。

再来看一下写请求,规则是“先更新 db,再删除缓存”,详细步骤如下:


将变更写入到数据库中;

删除缓存里对应的数据。

大厂高并发,“后删缓存”依旧不一致


这种情况不存在并发问题么?不是的。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生


缓存刚好失效

请求A查询数据库,得一个旧值

请求B将新值写入数据库

请求B删除缓存

请求A将查到的旧值写入缓存

如果发生上述情况,确实是会发生脏数据。


然而,发生这种情况的概率又有多少呢?

发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的,因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。


一般情况下,读取操作都是比写入操作快的,但我们要考虑两种极端情况:


- 一种是这个读取操作 A,发生在更新操作 B 的尾部。(比如写操作执行1s,读操作耗时100ms,读操作在写操作执行到800ms的时候开始执行,在写操作执行到900ms的时候结束,所以实际上读操作仅仅比写操作快了100ms而已)


一种是操作 A 的这个 Redis 的操作时长,耗费了非常多的时间。比如,这个节点正好发生了 STW。(Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾回收器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起)

那么很容易地,读操作 A 的结束时间就超过了操作 B 删除的动作。


这种场景的出现,不仅需要缓存失效且读写并发执行,而且还需要读请求查询数据库的执行早于写请求更新数据库,同时读请求的执行完成晚于写请求。这种不一致场景产生的条件非常严格,一般业务是达不到这个量级的,所以一般公司不去处理这种情况,但高并发业务就非常常见了。


11.png


那如果是读写分离的场景下呢?如果按照如下所述的执行序列,一样会出问题:


请求A更新主库

请求A删除缓存

请求B查询缓存,没有命中,查询从库得到旧值

从库同步完毕

请求B将旧值写入缓存

如果数据库主从同步比较慢的话,同样会出现数据不一致的问题。事实上就是如此,毕竟我们操作的是两个系统,在高并发的场景下,我们很难去保证多个请求之间的执行顺序,或者就算做到了,也可能会在性能上付出极大的代价。


加锁?


可以采用加锁在写请求中保证“更新数据库&删除缓存”的串行执行为原子性操作(同理也可对读请求中缓存的更新加锁)。加锁势必会导致吞吐量的下降,故采取加锁的方案应该对性能的损耗有所预期.

12.png


如何解决高并发的不一致问题?

大家看上面这种不一致情况发生的场景,归根结底还是“删除操作”发生在“更新操作”之前了。


延时双删


假如我有一种机制,能够确保删除动作一定被执行,那就可以解决问题,起码能缩小数据不一致的时间窗口。

常用的方法就是延时双删,依然是先更新再删除,唯一不同的是:我们把这个删除动作,在不久之后再执行一次,比如 5 秒之后。

public void putValue(key,value){
    putToDB(key,value);
    deleteFromRedis(key);
    // 数秒后重新执行删除操作
    deleteFromRedis(key,5);
}


这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。


这种方案还算可以,只有休眠那一会,可能有脏数据,一般业务也会接受的。


其实在讨论最后一个方案时,我们没有考虑操作数据库或者操作缓存可能失败的情况,而这种情况也是客观存在的。


那么在这里我们简单讨论下,首先是如果更新数据库失败了,其实没有太大关系,因为此时数据库和缓存中都还是老数据,不存在不一致的问题。假设删除缓存失败了呢?此时确实会存在数据不一致的情况。除了设置缓存过期时间这种兜底方案之外,如果我们希望尽可能保证缓存可以被及时删除,那么我们必须要考虑对删除操作进行重试。


删除缓存重试机制


你当然可以直接在代码中对删除操作进行重试,但是要知道如果是网络原因导致的失败,立刻进行重试操作很可能也是失败的,因此在每次重试之间你可能需要等待一段时间,比如几百毫秒甚至是秒级等待。为了不影响主流程的正常运行,你可能会将这个事情交给一个异步线程来执行。


而删除动作也有多种选择:


如果开线程去执行,会有随着 JVM 进程的死亡,丢失更新的风险;

如果放在 MQ 中,会增加编码的复杂性。

所以到了这个时候,并没有一个能够行走天下的解决方案。我们得综合评价很多因素去做设计,比如团队的水平、工期、不一致的忍受程度等。


异步优化方式:消息队列


  1. 写请求更新数据库
  2. 缓存因为某些原因,删除失败
  3. 把删除失败的key放到消息队列
  4. 消费消息队列的消息,获取要删除的key
  5. 重试删除缓存操作



13.png


异步优化方式:基于订阅binlog的同步机制


那如果是读写分离场景呢?我们知道数据库(以Mysql为例)主从之间的数据同步是通过binlog同步来实现的,因此这里可以考虑订阅binlog(可以使用canal之类的中间件实现),提取出要删除的缓存项,然后作为消息写入消息队列,然后再由消费端进行慢慢的消费和重试。


14.png


  1. 更新数据库数据
  2. 数据库会将操作信息写入binlog日志当中
  3. 订阅程序提取出所需要的数据以及key
  4. 另起一段非业务代码,获得该信息
  5. 尝试删除缓存操作,发现删除失败
  6. 将这些信息发送至消息队列
  7. 重新从消息队列中获得该数据,重试操作。



小结


针对 Redis 的缓存一致性问题,我们聊了很多。可以看到,无论你怎么做,一致性问题总是存在,只是几率慢慢变小了。


随着对不一致问题的忍受程度越来越低、并发量越来越高,我们所采用的方案也越来越极端。一般情况下,到了延时双删这一步,就证明你的并发量已经够大了;再往下走,无不是对高可用、成本、一致性的权衡,进入到了特事特办的场景,甚至要考虑基础设施,关于这些每个公司的策略都是不一样的。


除了 Cache-Aside Pattern,一致性常见的还有 Read-Through、Write-Through、Write-Behind 等模式,它们都有自己的应用场景,你可以再深入了解一下。


题的忍受程度越来越低、并发量越来越高,我们所采用的方案也越来越极端。一般情况下,到了延时双删这一步,就证明你的并发量已经够大了;再往下走,无不是对高可用、成本、一致性的权衡,进入到了特事特办的场景,甚至要考虑基础设施,关于这些每个公司的策略都是不一样的。


除了 Cache-Aside Pattern,一致性常见的还有 Read-Through、Write-Through、Write-Behind 等模式,它们都有自己的应用场景,你可以再深入了解一下。


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
2月前
|
存储 缓存 数据库
解决缓存与数据库的数据一致性问题的终极指南
解决缓存与数据库的数据一致性问题的终极指南
203 63
|
3月前
|
消息中间件 canal 缓存
项目实战:一步步实现高效缓存与数据库的数据一致性方案
Hello,大家好!我是热爱分享技术的小米。今天探讨在个人项目中如何保证数据一致性,尤其是在缓存与数据库同步时面临的挑战。文中介绍了常见的CacheAside模式,以及结合消息队列和请求串行化的方法,确保数据一致性。通过不同方案的分析,希望能给大家带来启发。如果你对这些技术感兴趣,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!
213 6
项目实战:一步步实现高效缓存与数据库的数据一致性方案
|
3月前
|
canal 缓存 NoSQL
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
根据对一致性的要求程度,提出多种解决方案:同步删除、同步删除+可靠消息、延时双删、异步监听+可靠消息、多重保障方案
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
|
21天前
|
缓存 物联网 数据库
InfluxDB vs TDengine :2025 年了,谁家用的数据库还不能高效读缓存?
在工业互联网和物联网的大数据应用场景中,实时数据的写入和查询性能至关重要。如何快速获取最新设备状态并实时处理数据,直接影响到业务的高效运转。本文将深入分析 TDengine 和 InfluxDB 在缓存机制上的差异,帮助读者更好地理解这两款主流时序数据库在性能优化方面的优劣。
50 1
|
1月前
|
SQL 缓存 关系型数据库
美团面试:Mysql 有几级缓存? 每一级缓存,具体是什么?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴因未能系统梳理MySQL缓存机制而在美团面试中失利。为此,尼恩对MySQL的缓存机制进行了系统化梳理,包括一级缓存(InnoDB缓存)和二级缓存(查询缓存)。同时,他还将这些知识点整理进《尼恩Java面试宝典PDF》V175版本,帮助大家提升技术水平,顺利通过面试。更多技术资料请关注公号【技术自由圈】。
美团面试:Mysql 有几级缓存? 每一级缓存,具体是什么?
|
1月前
|
缓存 NoSQL 数据库
运用云数据库 Tair 构建缓存为应用提速,完成任务得苹果音响、充电套装等好礼!
本活动将带大家了解云数据库 Tair(兼容 Redis),通过体验构建缓存以提速应用,完成任务,即可领取罗马仕安卓充电套装,限量1000个,先到先得。邀请好友共同参与活动,还可赢取苹果 HomePod mini、小米蓝牙耳机等精美好礼!
|
1月前
|
缓存 NoSQL 关系型数据库
mysql和缓存一致性问题
本文介绍了五种常见的MySQL与Redis数据同步方法:1. 双写一致性,2. 延迟双删策略,3. 订阅发布模式(使用消息队列),4. 基于事件的缓存更新,5. 缓存预热。每种方法的实现步骤、优缺点均有详细说明。
|
2月前
|
缓存 弹性计算 NoSQL
新一期陪跑班开课啦!阿里云专家手把手带你体验高并发下利用云数据库缓存实现极速响应
新一期陪跑班开课啦!阿里云专家手把手带你体验高并发下利用云数据库缓存实现极速响应
|
3月前
|
存储 SQL 关系型数据库
一篇文章搞懂MySQL的分库分表,从拆分场景、目标评估、拆分方案、不停机迁移、一致性补偿等方面详细阐述MySQL数据库的分库分表方案
MySQL如何进行分库分表、数据迁移?从相关概念、使用场景、拆分方式、分表字段选择、数据一致性校验等角度阐述MySQL数据库的分库分表方案。
528 15
一篇文章搞懂MySQL的分库分表,从拆分场景、目标评估、拆分方案、不停机迁移、一致性补偿等方面详细阐述MySQL数据库的分库分表方案
|
3月前
|
消息中间件 缓存 NoSQL
15)如何保证缓存和数据库之间的数据一致性
15)如何保证缓存和数据库之间的数据一致性
71 1