如何保证缓存与数据库的双写一致性?

如何保证缓存与数据库的双写一致性?

  • 概述
  • 同步策略
    • 更新缓存还是删除缓存:
    • 先操作数据库还是缓存:
      • 案例一、先删除缓存,在更新数据库
      • 案例二 先操作数据库,再删除缓存
    • 延时双删策略(不推荐)
    • 使用分布式锁实现双写一致性
    • 使用读写锁实现双写一致性
  • 使用消息队列异步通知
  • 订阅Mysql的Binlog文件(可借助Canal来进行)
  • 总结

概述

MySQL 和 Redis 都是常见的数据存储方案,MySQL 用于存储结构化数据,Redis 用于存储非结构化数据。在一些高并发场景下,为了提升系统的性能,我们通常会将数据存储在 Redis 缓存中,并通过 Redis 缓存来提高系统的读取速度。但是,Redis 缓存中的数据是不稳定的,可能会随时被删除或者被更新,因此需要和 MySQL 中的数据进行同步,保证数据的一致性。

但是使用过缓存的人都应该知道,在实际应用场景中,要想实时刻保证缓存和数据库中的数据一样,很难做到。 基本上都是尽可能让他们的数据在绝大部分时间内保持一致,并保证最终是一致的。

同步策略

首先介绍一下双写一致性·当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库

四种同步策略:
想要保证缓存与数据库的双写一致,一共有4种方式,即4种同步策略:
1. 先更新缓存,再更新数据库;
2. 先更新数据库,再更新缓存;
3. 先删除缓存,再更新数据库;
4. 先更新数据库,再删除缓存。

从这4种同步策略中,我们需要作出比较的是:

  • 更新缓存与删除缓存哪种方式更合适?
  • 应该先操作数据库还是先操作缓存?

更新缓存还是删除缓存:

下面,我们来分析一下,应该采用更新缓存还是删除缓存的方式。

  1. 更新缓存
  • 优点:每次数据变化都及时更新缓存,所以查询时不容易出现未命中的情况。
  • 缺点:更新缓存的消耗比较大。如果数据需要经过复杂的计算再写入缓存,那么频繁的更新缓存,就会影响服务器的性能。如果是写入数据频繁的业务场景,那么可能频繁的更新缓存时,却没有业务读取该数据。
  1. 删除缓存
  • 优点:操作简单,无论更新操作是否复杂,都是将缓存中的数据直接删除。
  • 缺点:删除缓存后,下一次查询缓存会出现未命中,这时需要重新读取一次数据库。

从上面的比较来看,一般情况下,删除缓存是更优的方案。

先操作数据库还是缓存:

下面,我们再来分析一下,应该先操作数据库还是先操作缓存。

案例一、先删除缓存,在更新数据库

初始时,缓存和数据库均为10。
在这里插入图片描述
如上图,先删除缓存,再更新数据库,可能会出现的问题:

  1. 线程1删除缓存
  2. 线程2查询缓存未命中,查询数据库
  3. 写入缓存的值为10,
  4. 线程1再进行更新数据库,值为20

此时数据库为更新过的值20,而缓存还是旧值10,此时出现了数据库和缓存数据不一致情况。

案例二 先操作数据库,再删除缓存

在这里插入图片描述
如上图,先删除缓存,再更新数据库,可能会出现的问题:

  1. 线程1查询缓存未命中,查询数据库
  2. 线程2更新数据库为20,
  3. 线程2删除缓存
  4. 线程1写入缓存值为10

此时数据库为更新过的值20,而缓存还是旧值10,此时出现了数据库和缓存数据不一致情况。

经过案例一和案例二的比较,先删除缓存和先更新数据库都会出现问题。

延时双删策略(不推荐)

在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。
在这里插入图片描述

伪代码如下:

public void write( String key, Object data ){redis.delKey(key);db.updateData(data); Thread.sleep(500);redis.delKey(key);
}

问题:这个500毫秒怎么确定的,具体该休眠多久时间呢?

  1. 需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
  2. 当然这种策略还要考虑redis和数据库主从同步的耗时。
  3. 另外这种策略也会可能会有脏数据的风险,而且还会消耗不必要的性能。

在实际场景中,并不推荐延时双删策略,一方面可能会有脏数据的风险,而且还会消耗不必要的性能。

虽然先更新数据库,再删除缓存也是会出现数据不一致性的问题,但是在实际中,这个问题出现的概率并不高。因为缓存的写入通常要远远快于数据库的写入,所以在实际中很难出现请求 B 已经更新了数据库并且删除了缓存,请求 A 才更新完缓存的情况。所以,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。

但是,为了确保万无一失,在更新完缓存时,给缓存加上较短的过期时间,这样即时出现缓存不一致的情况,缓存的数据也会很快过期,对业务还是能接受的。另外在更新缓存中加入过期时间,这样就算出现了缓存和数据库不一致问题,但最终是一致的。

使用分布式锁实现双写一致性

分别在写数据和读数据加分布式锁,保证同一时间只运行一个请求更新缓存(保证读写串行化),就会不会产生并发问题了,这样就能保证redis和mysql的数据强一致性。
在这里插入图片描述
但是这样的话读操作和写操作都需要加锁,效率就会大大降低。其实在真实场景中放入缓存中的数据一般是读多写少,如果是读少写多,那完全可以不用缓存,直接操作数据库了。

使用读写锁实现双写一致性

在读多写少的场景下,可以使用读锁和写锁的机制。

  1. 共享锁:读锁readLock,加锁之后,其他线程可以共享读操作,写互斥
  2. 排他锁:独占锁writeLock也加写锁,加锁之后,堵塞其他线程读写操作。
    在这里插入图片描述

使用redisson中的读写锁实现双写一致性

想要拿到共享锁或者排他锁,都需要先拿到读写锁。通过固定代码可以拿到读写锁。

RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("READ_WRITE_LOCK");

随后分别拿到共享锁和排他锁。(注意两个锁需要是同一把读写锁)

RLock readLock = readWriteLock.readLock();
RLock writeLock = readWriteLock.writeLock();

读操作加入读锁(共享锁)

public void getById(Integer id){RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("READ_WRITE_LOCK");RLock readLock = readWriteLock.readLock();try{readLock.lock();System.out.println("readLock...");Item item = (Item) redisTemplate.opsForValue().get("item"+id);if(item != null){return item;}item = new Item(id, "手机", "手机", 60.00);redisTemplate.opsForValue().set("item"+id, item);return item;}finally{readLock.unlock();}
}

写操作加入写锁(排他锁)

public void updateById(Integer id){RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("READ_WRITE_LOCK");RLock writeLock = readWriteLock.writeLock();try{writeLock.lock();System.out.println("writeLock...");Item item = new Item(id, "手机", "手机", 100.00);try{Thread.sleep(2000);}catch(InterruptedException e){e.printStackTrace();}redisTemplate.delete("item"+id);}finally{writeLock.unlock();}
}

可以实现强一致性方案,虽然比分布式锁好一点,但是在高并发场景下性能也比较低。

使用消息队列异步通知

如果允许缓存中的数据在短时间内可以跟数据库数据不一致的情况下,可以使用异步通知的方案,可以保证最终一致性。

为了解决双写一致性的问题,我们可以引入消息队列,比如RabbitMQ,来异步更新Redis。将操作同一资源的请求,打到同一个队列中。

当有数据变动时,我们先操作数据库,然后通过消息队列发送消息到一个缓存更新的队列中,异步更新缓存。这种方式能够让写操作变得更加高效,并且避免了高并发下的缓存与数据库数据不一致的问题。
在这里插入图片描述

订阅Mysql的Binlog文件(可借助Canal来进行)

另一种更为可靠的方法是使用MySQL的binlog。我们可以使用Maxwell或者Canal等工具,实时解析binlog,然后更新Redis。
在这里插入图片描述
这种方案的好处是即使应用程序崩溃,也不会丢失binlog,因此能够保证最终的数据一致性。但是,这种方案的实现比较复杂,需要对MySQL的内部机制有深入的理解。

总结

允许延时一致的业务,采用异步通知

  1. 使用MQ中间件,更新数据之后,通知缓存更新,将操作同一资源的请求,打到同一个队列中。
  2. 利用canal中间件,不需要修改业务代码,伪装为mysql的一个从节点,canal通过读取binlog数据更新缓存

强一致性,采用Redisson提供的读写过

在读多写少的场景下,可以使用读锁和写锁的机制。

  1. 共享锁:读锁readLock,加锁之后,其他线程可以共享读操作,写互斥
  2. 排他锁:独占锁writeLock也加写锁,加锁之后,堵塞其他线程读写操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/286577.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《数据安全技术 数据分类分级规则》及典型行业标准指南要点提炼

数据分类分级发布新国标 千呼万唤,国家标准GB/T 43697-2024《数据安全技术 数据分类分级规则》于3月21日正式发布。作为全国网络安全标准化技术委员会更名后,发布的第一部以“数据安全技术”命名的国家标准,《数据安全技术 数据分类分级规则…

K8s+Nacos实现应用的优雅上下线【生产实践】

文章目录 前言一、环境描述二、模拟请求报错三、配置优雅上下线1.修改nacos配置2.修改depolyment配置3.重新apply deployment后测试4.整体(下单)测试流程验证是否生效 四、期间遇到的问题 前言 我们在使用k8s部署应用的时候,虽然k8s是使用滚动升级的,先…

【CXL协议-事务层之CXL.cache (3)】

3.2 CXL.cache 3.2.1 概述 CXL.cache 协议将设备和主机之间的交互定义为许多请求,每个请求至少有一个关联的响应消息,有时还有数据传输。 该接口由每个方向的三个通道组成: 请求、响应和数据。 这些通道根据其方向命名,D2H&…

【笔记】深入理解JVM机制

🎥 个人主页:Dikz12📕格言:吾愚多不敏,而愿加学欢迎大家👍点赞✍评论⭐收藏 目录 JVM 运⾏流程图 JVM 中内存区域划分 方法区 / 元数据区 堆 栈 程序计数器 本地方法栈 内存区域总结 JVM 中类加载过程 …

Go第三方框架--gin框架(一)

序言 Gin框架作为go语言使用最多的web框架,以其快速的响应速度和对复杂http路由配置的支持受到程序员和媛们的喜爱,几乎统治了web市场。但作为一名合格的程序员,要知其然更要知其所以然,不然八股文背的也没有啥意思。本着这个原则…

【Java程序设计】【C00368】基于(JavaWeb)Springboot的箱包存储系统(有论文)

TOC 博主介绍:java高级开发,从事互联网行业六年,已经做了六年的毕业设计程序开发,开发过上千套毕业设计程序,博客中有上百套程序可供参考,欢迎共同交流学习。 项目简介 项目获取 🍅文末点击卡片…

【MySQL数据库】数据类型和简单的增删改查

目录 数据库 MySQL的常用数据类型 1.数值类型: 2.字符串类型 3.日期类型 MySQL简单的增删改查 1.插入数据: 2.查询数据: 3.修改语句: 4.删除语句: 数据库 平时我们使用的操作系统都把数据存储在文件中&#…

3.3 数据定义 数据库与系统概论

目录 3.3.1 模式的定义与删除 1. 定义模式 2. 删除模式 CASCADE(级联) RESTRICT(限制) 3.3.2 基本表的定义、删除与修改 表的定义 2.数据类型 3. 模式与表 4. 修改基本表 5. 删除基本表 3.3.3 索引的建立与删除 1. …

如何备考2024年AMC10:吃透2000-2023年1250道真题(限时免费送)

我们今天继续来随机看5道AMC10真题,以及详细解析,这些题目来自1250道完整的官方历年AMC10真题库。通过系统研究和吃透AMC10的历年真题,参加AMC10的竞赛就能拿到好名次。即使不参加AMC10竞赛,掌握了这些知识和解题思路后初中和高中…

2015年认证杯SPSSPRO杯数学建模C题(第一阶段)荒漠区动植物关系的研究全过程文档及程序

2015年认证杯SPSSPRO杯数学建模 C题 荒漠区动植物关系的研究 原题再现: 环境与发展是当今世界所普遍关注的重大问题, 随着全球与区域经济的迅猛发展, 人类也正以前所未有的规模和强度影响着环境、改变着环境, 使全球的生命支持系统受到了严重创伤, 出现了全球变暖…

Flutter 旋转动画 线性变化的旋转动画

直接上代码 图片自己添加一张就好了 import dart:math;import package:flutter/material.dart;import package:flutter/animation.dart;void main() > runApp(MyApp()); //旋转动画 class MyApp extends StatelessWidget {overrideWidget build(BuildContext context) {re…

RMAN 备份恢复、删除归档

RMAN冷备全库 rman target / list backup shutdown immediate startup mount #不要自动备份control file set nocfau; #注意要先备份数据库,然后备份控制文件,因为数据库的备份位置记录在控制文件中。 #备份数据库 backup database format /mnt/disk01/r…

vue 中实现下载后端返回的流式数据

验证是否是blob /*** Event 验证是否为blob格式* */export async function blobValidate(data) {try {const text await data.text();JSON.parse(text);return false;} catch (error) {return true;}}get请求 /*** Event: get请求下载后端返回的数据流* description: url[Stri…

Redis-指定配置启动

基础篇Redis 3.3.5.指定配置启动 如果要让Redis以后台方式启动,则必须修改Redis配置文件,就在我们之前解压的redis安装包下(/usr/local/src/redis-6.2.6),名字叫redis.conf: 我们先将这个配置文件备份一份…

利用 Scapy 库编写 ARP 缓存中毒攻击脚本

一、ARP 协议基础 参考下篇文章学习 二、ARP 缓存中毒原理 ARP(Address Resolution Protocol)缓存中毒是一种网络攻击,它利用了ARP协议中的漏洞,通过欺骗或篡改网络中的ARP缓存来实施攻击。ARP协议是用于将IP地址映射到物理MAC…

警务数据仓库的实现

目录 一、SQL Server 2008 R2(一)SQL Server 的服务功能(二)SQL Server Management Studio(三)Microsoft Visual Studio 二、创建集成服务项目三、配置“旅馆_ETL”数据流任务四、配置“人员_ETL”数据流任…

OM6626低功耗M4内核低睡眠电流BLE5.3 SoC国产ESL蓝牙方案芯片

目录 OM6626简介OM6626主要特性射频特性PUM特性安全性SDK代码微信号:dnsj5343OM6626最小系统Demo板 OM6626简介 OM6626是功能强大、性能稳定、超低功耗的蓝牙SoC芯片,适用于各种低功耗蓝牙和专有的2.4GHz应用场景。OM6626还集成了电源管理单元 (PMU)&am…

机器视觉检测设备的组成要素

机器视觉检测设备是一种先进的自动化检测技术工具,它利用光学、图像处理和计算机硬件及软件技术模拟并扩展人类的视觉功能,以实现对产品或目标物体进行自动化的尺寸测量、缺陷检测、表面质量评估、颜色识别、形状匹配以及位置判断等功能。这种设备通常包…

PyCharm环境下Git与Gitee联动:本地与远程仓库操作实战及常见问题解决方案

写在前面:本博客仅作记录学习之用,部分图片来自网络,如需引用请注明出处,同时如有侵犯您的权益,请联系删除! 文章目录 前言下载及安装GitGit的使用设置用户签名设置用户安全目录Git基本操作Git实操操作 Pyc…

Python高阶函数库之functools使用详解

概要 functools是Python标准库中的一个模块,它提供了一系列用于高阶函数:即那些作用于或返回其他函数的函数。这些工具主要用于函数式编程风格,其中包括用于创建函数包装器的装饰器。 functools简介 functools库的目的是为了高阶函数,特别是那些涉及到函数转换的操作提供…