如何保证Redis与MySQL双写一致性
什么是双写一致性问题?
双写一致性主要指在一个数据同时存在于缓存(如Redis)和持久化存储(如MySQL)的情况下,任何一方的数据更新都必须确保另一方数据的同步更新,以保持双方数据的一致状态。这一问题的核心在于如何在高并发环境下正确处理缓存与数据库的读写交互,防止数据出现不一致的情况。
一致性通常可以分为以下几个类别:
强一致性:
所有节点在任何时间都看到相同的数据。任何更新操作都会立即对所有节点可见,保证了数据的强一致性。这意味着,如果一个节点完成了写操作,那么所有其他节点读取相同的数据之后,都将看到最新的结果。强一致性通常需要付出更高的代价,例如增加通信开销和降低系统的可用性。
弱一致性:
系统中的数据在某些情况下可能会出现不一致的状态,但最终会收敛到一致状态。弱一致性下的系统允许在一段时间内,不同节点之间看到不同的数据状态。弱一致性通常用于需要在性能和一致性之间进行权衡的场景,例如缓存系统等。
最终一致性:
是弱一致性的一种特例,它保证了在经过一段时间后,系统中的所有节点最终都会达到一致状态。尽管在数据更新时可能会出现一段时间的不一致,但最终数据会收敛到一致状态。最终一致性通常通过一些技术手段来实现,例如基于版本向量或时间戳的数据复制和同步机制。
1、缓存常见读取数据、写数据用法

2、缓存不一致产生的原因
如果数据一直没有变更,那么就不会出现Redis和MySQL数据不一致性的问题。
两者之间数据不一致是因为一者发生了数据的变更,另一者如何在短时间内同步数据的问题。因为每次数据变更需要同时操作数据库和缓存,而他们又属于不同的系统,无法做到同时操作成功或失败,总会有一个时间差。在并发读写的时候可能就会出现缓存不一致的问题。
保证数据一致性通常涉及5种策略
1、先更新数据库,再更新缓存
@Transactional
public void updateUser(User user) {// 1. 更新数据库userMapper.updateUser(user);// 2. 更新Redis缓存// 方式1:更新缓存redisTemplate.opsForValue().set("user:" + user.getId(), user);// 方式2:删除缓存(推荐)redisTemplate.delete("user:" + user.getId());
}

如上图所示,其可能执行的流程顺序为:
1.客户端1 触发更新数据A的逻辑
2.客户端2 触发查询数据A的逻辑
3.客户端3 触发查询数据A的逻辑
4.客户端1 更新数据库中数据A
5.客户端2 查询缓存中数据A,命中返回(旧数据)
6.客户端1 让缓存中数据A失效
7.客户端3 查询缓存中数据A,未命中
8.客户端3 查询数据库中数据A,并更新到缓存中
可见,最后缓存中的数据A和数据库中的数据A是一致的,理论上可能会出现一小段时间数据不一致,不过这种概率也比较低,大部分的业务也不会有太大的问题。
为什么操作缓存的时候是删除旧缓存而不是直接更新缓存?
举个例子:
线程A先发起一个写操作,第一步先更新数据库,然后更新缓存
线程B再发起一个写操作,第二步更新了数据库,然后更新缓存
当以上两个线程的执行,如果严格先后顺序执行,那么对于更新缓存还是删除缓存去操作缓存都可以,但是如果两个线程同时执行时,由于网络或者其他原因,导致线程B先执行完更新缓存,然后线程A才会更新缓存。这时候缓存中保存的就是线程A的数据,而数据库中保存的是线程B的数据。这时候如果读取到的缓存就是脏数据。但是如果使用删除缓存取代更新缓存,那么就不会出现这个脏数据。
2、先更新缓存,再更新数据库
@Transactional
public void updateUser(User user) {// 1. 删除Redis缓存redisTemplate.delete("user:" + user.getId());// 2. 更新MySQLuserMapper.updateUser(user);
}

如上图所示,其可能执行的流程顺序为:
1.客户端1,发起一个写操作,第一步删除缓存
2.客户端2,发起一个读操作,缓存中没有,则继续读数据库,读出来一个老数据,然后客户端2把老数据放入缓存中
3.客户端1更新数据库数据
这样就会出现缓存中存储的是旧数据,而数据库中存储的是新数据,这样就出现脏数据,所以我们一般都采取先操作数据库,再操作缓存。这样后续的读请求从数据库获取最新数据并重新填充缓存。这样的设计降低了数据不一致的风险,提升了系统的可靠性。同时,这也符合CAP定理中对于一致性(Consistency)和可用性(Availability)权衡的要求,在很多场景下,数据一致性被优先考虑。因此一般不建议使用这种方式。
3、延时双删策略
@Transactional
public void updateUser(User user) {// 1. 删除Redis缓存redisTemplate.delete("user:" + user.getId());// 2. 更新MySQLuserMapper.updateUser(user);// 3. 延迟一段时间后再次删除缓存CompletableFuture.runAsync(() -> {try {Thread.sleep(500); // 延迟500毫秒redisTemplate.delete("user:" + user.getId());} catch (InterruptedException e) {// 处理异常}});
}
延时双删策略主要用于解决在高并发场景下,由于网络延迟、并发控制等原因造成的数据库与缓存数据不一致的问题。
当更新数据库时,首先删除对应的缓存项,以确保后续的读请求会从数据库加载最新数据。
但是由于网络延迟或其他不确定性因素,删除缓存与数据库更新之间可能存在时间窗口,导致在这段时间内的读请求从数据库读取数据后写回缓存,新写入的缓存数据可能还未反映出数据库的最新变更。
所以为了解决这个问题,延时双删策略在第一次删除缓存后,设定一段短暂的延迟时间,如几百毫秒,然后在这段延迟时间结束后再次尝试删除缓存。这样做的目的是确保在数据库更新传播到所有节点,并且在缓存中的旧数据彻底过期失效之前,第二次删除操作可以消除缓存中可能存在的旧数据,从而提高数据一致性。
4、使用消息队列
@Transactional
public void updateUser(User user) {// 1. 更新MySQLuserMapper.updateUser(user);// 2. 发送消息到消息队列kafkaTemplate.send("user-update-topic", JSON.toJSONString(user));
}// 3. 在消费者服务中更新缓存
@KafkaListener(topics = "user-update-topic")
public void consumeUserUpdate(String message) {User user = JSON.parseObject(message, User.class);// 更新Redis缓存redisTemplate.opsForValue().set("user:" + user.getId(), user);
}
在高并发的业务场景中,消息队列是必不可少的技术之一。它不仅可以异步解耦,还能削峰填谷。对保证系统的稳定性是非常有意义的。
1.更新数据库
2.通过指定的topic发送到消息队列服务
3.然后消费者订阅该topic的消息,读取消息数据之后,再更新redis缓存。
5、使用 Canal 进行 MySQL binlog 同步
@Component
public class CanalClient {@Autowiredprivate RedisTemplate<String, String> redisTemplate;@PostConstructpublic void init() {CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("127.0.0.1", 11111), "example", "", "");try {connector.connect();connector.subscribe(".*\\..*");while (true) {Message message = connector.getWithoutAck(100);long batchId = message.getId();List<CanalEntry.Entry> entries = message.getEntries();if (batchId != -1 && entries.size() > 0) {for (CanalEntry.Entry entry : entries) {if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());if (rowChange.getEventType() == CanalEntry.EventType.UPDATE) {for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {// 处理更新操作,更新Redis缓存updateRedisCache(rowData);}}}}}connector.ack(batchId);}} finally {connector.disconnect();}}private void updateRedisCache(CanalEntry.RowData rowData) {// 根据rowData更新Redis缓存// 这里需要根据具体的数据结构来实现}
}
在数据库发生写操作时,将变更记录在binlog或类似的事务日志中,然后使用一个专门的异步服务或者监听器订阅binlog的变化(比如Canal),一旦检测到有数据更新,便根据binlog中的操作信息定位到受影响的缓存项,删除或更新缓存中的对应数据,确保缓存与数据库保持一致。
相关文章:
如何保证Redis与MySQL双写一致性
什么是双写一致性问题? 双写一致性主要指在一个数据同时存在于缓存(如Redis)和持久化存储(如MySQL)的情况下,任何一方的数据更新都必须确保另一方数据的同步更新,以保持双方数据的一致状态。这一…...
【IC每日一题:IC验证面试--UVM验证-2】
IC每日一题:IC验证面试--UVM验证-2 2.9 get_next_iterm()和try_next_item()的区别?2.10 一个典型的UVM验证平台,谈一下UVM验证环境结构,各个组件之间的关系?2.11 uvm组件之间通信的方式? analysis_port和其…...
SPIRE: Semantic Prompt-Driven Image Restoration 论文阅读笔记
这是一篇港科大学生在google research 实习期间发在ECCV2024的语义引导生成式修复的文章,港科大陈启峰也挂了名字。从首页图看效果确实很惊艳,尤其是第三行能用文本调控修复结果牌上的字。不过看起来更倾向于生成,对原图内容并不是很复原&…...
#揭秘万维网:从静态页面到智能互联网
揭秘万维网:从静态页面到智能互联网 今天刚上了学校开设的课程,于是便有了下文的思考内容。 在当今数字化时代,Web(万维网)扮演着重要的角色,成为人们获取信息、沟通交流和进行商业活动的主要平台。 1. …...
【计算机基础——数据结构——红黑树】
1. 红黑树(RBTree) 为什么HashMap不直接使用AVL树,而是选择了红黑树呢? 由于AVL树必须保证左右子树平衡,Max(最大树高-最小树高) < 1,所以在插入的时候很容易出现不平衡的情况,一旦这样&…...
Sentinel — 微服务保护
微服务架构将大型应用程序拆分为多个小而独立的服务,每个服务可以独立部署和扩展。然而,微服务系统需要面对的挑战也随之增加,例如服务之间的依赖、分布式环境下的故障传播和安全问题。因此,微服务保护措施是确保系统在高并发、资…...
Cynet:全方位一体化安全防护工具
前言 1999年,布鲁斯施奈尔曾说过:“复杂性是安全最大的敌人。”彼时还是19年前,而现在,网络安全已然变得更加繁杂。 近日我在网上冲浪过程中发现了这么一个平台性质的软件,看似具有相当强的防护能力。 根据Cynet的描…...
python中常见的8种数据结构之一数组的应用
在Python中,数组是一种常见的数据结构,用于存储一系列相同类型的元素。在实际应用中,数组可以用于解决各种问题。 以下是数组在Python中的一些常见应用: 1. 存储和访问数据:数组可以用于存储和访问一组数据。可以通过…...
安装多个低版本谷歌Chrome浏览器用于测试,适配Vue3+vite项目
安装多个低版本谷歌Chrome浏览器用于测试,适配Vue3vite项目 问题:使用vue3tsvite搭建了一个项目,在chrome新版本浏览器上无问题,但是部署到现场页面直接空白,且控制台报错: Uncaugnt SyntaxError: Unexpe…...
UI组件---如何设置el-pagination分页组件的背景色
1. 要替换 el-pagination 组件的背景色,您可以通过自定义CSS来实现。 具体的CSS规则取决于您想要更改的是哪个部分的背景色,例如普通页码、活跃页码、上下导航箭头等。以下是一些示例CSS规则,您可以根据自己的需求进行调整: /* …...
LabVIEW编程过程中为什么会出现bug?
在LabVIEW编程过程中,Bug的产生往往源自多方面原因。以下从具体的案例角度分析一些常见的Bug成因和调试方法,以便更好地理解和预防这些问题。 1. 数据流错误 案例:在一个LabVIEW程序中,多个计算节点依赖相同的输入数据&#…...
论文阅读《Structure-from-Motion Revisited》
摘要 增量式地运动结构恢复是从无序图像集合中进行三维重建的一个普遍策略。虽然增量式地重建系统在各个方面上都取得了巨大的进步,但鲁棒性、准确性、完整度和尺度仍然是构建真正通用管道的关键问题。我们提出了一种新的运动结构恢复技术,它改进了目前…...
RK android14 第三方app获取su权限
需要修改的地方如下 frameworks/base/core/jni/com_android_internal_os_Zygote.cpp kernel-6.1/security/commoncap.c system/core/init/selinux.cpp system/core/libcutils/fs_config.cpp system/extras/su/su.cpp device/rockchip/common/BoardConfig.mk device/rockchip…...
线程与进程的区别(面试)
一.进程 进程:一个程序启动起来,就会对应一个进程,进程就是系统分配资源的基本单位。 上面一部分进程是我们自己去执行应用的可执行文件, 而另一部分是操作系统自动启动的进程. 二.线程 线程:线程是进程中的一个执行单元ÿ…...
OpenDroneMap Webodm
OpenDroneMap & Webodm OpenDroneMap Webodm 开源无人机航拍系列图像及其它系列图像三维重建软件。很棒的开源无人机测绘软件OpenDroneMap,从航拍图像生成精确的地图、高程模型、3D 模型和点云。 应用领域 Mapping & Surveying 测绘和测量 从图像测量获得高精度的可…...
Could not create task ‘:shared_preferences_android:generateDebugUnitTestConfig‘
flutter项目使用shared_preferences库的时候,打开flutter项目中的android项目运行,会出现如下错误信息: A build operation failed. Could not create task :shared_preferences_android:generateDebugUnitTestConfig. Could not create…...
CSS教程(四)- 字体
1、尺寸单位 px 像素单位% 百分比,参照父元素对应属性的值进行计算em 字体尺寸单位,参照父元素的字体大小计算,1em16pxrem字体尺寸单位,参照根元素的字体大小计算,1rem16px 2、字体属性 介绍 CSS Fonts (字体)属性用于定义字体…...
深入理解Java中的Lambda表达式
在Java 8中,Lambda表达式的引入无疑是一个重大的里程碑。 Lambda表达式以其简洁的语法和强大的功能,极大地改变了Java开发者编写代码的方式。本文将深入探讨Lambda表达式的概念、语法、使用场景以及其在函数式编程中的意义。 一、Lambda表达式的基本概…...
C#里怎么样判断一个数是偶数还是奇数
一般是采用取余的做法。 程序如下: /** C# Program to Check whether the Entered Number is Even or Odd*/ using System; using System.Collections.Generic; using System.Linq; using System.Text;namespace check1 {class Program{static void Main(string[]…...
【论文笔记】Prefix-Tuning: Optimizing Continuous Prompts for Generation
🍎个人主页:小嗷犬的个人主页 🍊个人网站:小嗷犬的技术小站 🥭个人信条:为天地立心,为生民立命,为往圣继绝学,为万世开太平。 基本信息 标题: Prefix-Tuning: Optimizin…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...
生成 Git SSH 证书
🔑 1. 生成 SSH 密钥对 在终端(Windows 使用 Git Bash,Mac/Linux 使用 Terminal)执行命令: ssh-keygen -t rsa -b 4096 -C "your_emailexample.com" 参数说明: -t rsa&#x…...
【决胜公务员考试】求职OMG——见面课测验1
2025最新版!!!6.8截至答题,大家注意呀! 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:( B ) A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...
大学生职业发展与就业创业指导教学评价
这里是引用 作为软工2203/2204班的学生,我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要,而您认真负责的教学态度,让课程的每一部分都充满了实用价值。 尤其让我…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...
QT3D学习笔记——圆台、圆锥
类名作用Qt3DWindow3D渲染窗口容器QEntity场景中的实体(对象或容器)QCamera控制观察视角QPointLight点光源QConeMesh圆锥几何网格QTransform控制实体的位置/旋转/缩放QPhongMaterialPhong光照材质(定义颜色、反光等)QFirstPersonC…...
CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝
目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为:一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...
uniapp 字符包含的相关方法
在uniapp中,如果你想检查一个字符串是否包含另一个子字符串,你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的,但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...
Bean 作用域有哪些?如何答出技术深度?
导语: Spring 面试绕不开 Bean 的作用域问题,这是面试官考察候选人对 Spring 框架理解深度的常见方式。本文将围绕“Spring 中的 Bean 作用域”展开,结合典型面试题及实战场景,帮你厘清重点,打破模板式回答,…...
从面试角度回答Android中ContentProvider启动原理
Android中ContentProvider原理的面试角度解析,分为已启动和未启动两种场景: 一、ContentProvider已启动的情况 1. 核心流程 触发条件:当其他组件(如Activity、Service)通过ContentR…...
