当前位置: 首页 > news >正文

问题: redis-高并发场景下如何保证缓存数据与数据库的最终一致性

在高并发场景下,Redis 通常用作缓存层,与数据库结合使用以提高系统的性能。为了保证缓存数据与数据库的最终一致性,通常采用的有双写机制、缓存失效机制,基于双写机制、缓存失效机制又衍生出来了消息队列、事件驱动架构等

常见机制

常见的机制如下,个人理解无非是先后或各种手段操作数据库、redis,代码ai给写的示列只需看懂即可。

  1. 双写机制
    在更新数据库的同时,同步更新缓存。
    适用于写操作较少的场景
 public class CacheService {private final JdbcTemplate jdbcTemplate;private final RedisTemplate<String, Object> redisTemplate;public void updateData(String key, String value) {// 更新数据库jdbcTemplate.update("UPDATE table SET value = ? WHERE key = ?", value, key);// 更新缓存redisTemplate.opsForValue().set(key, value);}
  1. 缓存失效机制
    在更新数据库后,删除缓存中的旧数据,读取数据时候时写入缓存
    适用于写操作频繁的场景。
 public class CacheService {private final JdbcTemplate jdbcTemplate;private final RedisTemplate<String, Object> redisTemplate;public void updateData(String key, String value) {// 更新数据库jdbcTemplate.update("UPDATE table SET value = ? WHERE key = ?", value, key);// 删除缓存redisTemplate.delete(key);}public String getData(String key) {// 从缓存中获取数据String value = (String) redisTemplate.opsForValue().get(key);if (value == null) {// 缓存未命中,从数据库中获取数据value = jdbcTemplate.queryForObject("SELECT value FROM table WHERE key = ?", new Object[]{key}, String.class);if (value != null) {// 将数据写入缓存redisTemplate.opsForValue().set(key, value);}}return value;}}
  1. 消息队列机制
    使用消息队列异步更新redis,确保数据的一致性。
    适用于高并发写操作的场景。
  import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.rabbitmq.client.ConnectionFactory;public class CacheService {private final JdbcTemplate jdbcTemplate;private final RedisTemplate<String, Object> redisTemplate;public void updateData(String key, String value) {// 更新数据库jdbcTemplate.update("UPDATE table SET value = ? WHERE key = ?", value, key);// 发送消息到消息队列sendUpdateMessage(key, value);}private void sendUpdateMessage(String key, String value) {ConnectionFactory factory = new ConnectionFactory();factory.setHost("localhost");try (Connection connection = factory.newConnection();Channel channel = connection.createChannel()) {channel.queueDeclare("cache_update_queue", true, false, false, null);channel.basicPublish("", "cache_update_queue", null, (key + ":" + value).getBytes());} catch (Exception e) {e.printStackTrace();}}public void consumeUpdateMessages() {ConnectionFactory factory = new ConnectionFactory();factory.setHost("localhost");try (Connection connection = factory.newConnection();Channel channel = connection.createChannel()) {channel.queueDeclare("cache_update_queue", true, false, false, null);DeliverCallback deliverCallback = (consumerTag, delivery) -> {String message = new String(delivery.getBody(), "UTF-8");String[] parts = message.split(":");String key = parts[0];String value = parts[1];// 更新缓存redisTemplate.opsForValue().set(key, value);};channel.basicConsume("cache_update_queue", true, deliverCallback, consumerTag -> {});} catch (Exception e) {e.printStackTrace();}}}
  1. 事件驱动机制
    使用事件驱动架构,当数据库数据发生变化时,触发事件,事件处理器负责更新缓存。
    适用于复杂的数据更新逻辑。
   import org.springframework.context.ApplicationEventPublisher;import org.springframework.context.ApplicationEventPublisherAware;import org.springframework.stereotype.Service;@Servicepublic class CacheService implements ApplicationEventPublisherAware {private final JdbcTemplate jdbcTemplate;private final RedisTemplate<String, Object> redisTemplate;private ApplicationEventPublisher eventPublisher;public void updateData(String key, String value) {// 更新数据库jdbcTemplate.update("UPDATE table SET value = ? WHERE key = ?", value, key);// 发布事件eventPublisher.publishEvent(new DataUpdatedEvent(this, key, value));}@Overridepublic void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {this.eventPublisher = applicationEventPublisher;}@Servicepublic class EventListener {private final RedisTemplate<String, Object> redisTemplate;@org.springframework.context.event.EventListenerpublic void handleDataUpdatedEvent(DataUpdatedEvent event) {// 更新缓存redisTemplate.opsForValue().set(event.getKey(), event.getValue());}}}public class DataUpdatedEvent extends ApplicationEvent {private final String key;private final String value;public DataUpdatedEvent(Object source, String key, String value) {super(source);this.key = key;this.value = value;}public String getKey() {return key;}public String getValue() {return value;}}
  1. 定期补偿机制
    定期对缓存和数据库的数据进行校验,发现不一致时进行补偿操作。
    适用于对数据一致性要求较高的场景。
 import java.util.concurrent.Executors;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.TimeUnit;public class DataConsistencyChecker {private final JdbcTemplate jdbcTemplate;private final RedisTemplate<String, Object> redisTemplate;private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);public void startChecking() {scheduler.scheduleAtFixedRate(() -> {// 从数据库中获取所有数据List<Map<String, Object>> dataFromDB = jdbcTemplate.queryForList("SELECT key, value FROM table");for (Map<String, Object> row : dataFromDB) {String key = (String) row.get("key");String value = (String) row.get("value");// 从缓存中获取数据String cacheValue = (String) redisTemplate.opsForValue().get(key);if (!value.equals(cacheValue)) {// 数据不一致,更新缓存redisTemplate.opsForValue().set(key, value);}}}, 0, 1, TimeUnit.HOURS);}}

废弃缓存与更新缓存的取舍

由上面代码可看出 1和2 最大的区别在于更新数据库时到底是更新缓存还是删除缓存。
【废弃缓存】
优点:
操作简单,只需在更新数据库后删除缓存,下次读取时重新从数据库加载数据,减少了写的操作日数
缺点:
可能短暂不一致:在缓存删除后和新数据写入缓存前,可能会出现短暂的缓存不一致

【更新缓存】
优点:
数据强一致性:更新数据库和缓存同时进行,确保数据的一致性。
减少数据库读压力:缓存始终是最新的,减少了对数据库的读操作。
缺点:
复杂性增加:需要处理缓存更新失败的情况,可能需要回滚操作。
性能影响:每次更新操作都需要同时更新数据库和缓存,增加了操作的复杂性和时间

  • 写操作较少的场景:
    推荐使用更新缓存:因为写操作较少,更新缓存的额外开销相对较小,且可以确保数据的一致性。
  • 写操作频繁的场景:
    推荐使用废弃缓存:因为写操作频繁,更新缓存会增加系统的复杂性和开销,而废弃缓存可以减少缓存的写操作,降低系统负担。
  • 对数据一致性要求极高的场景:
    推荐使用更新缓存:尽管复杂性增加,但可以确保数据的强一致性。
  • 对性能要求较高且可以容忍短暂不一致的场景:
    推荐使用废弃缓存:可以减少数据库的读压力,提高系统的整体性能

淘汰缓存的顺序

https://blog.csdn.net/qq_39033181/article/details/119276120

【 方案一 】先淘汰缓存,再更新数据库
在并发量较大的情况下,会导致数据的不一致。
  1. A线程进行写操作,先成功淘汰缓存,但由于网络或其它原因,还未更新数据库
  2. B线程进行读操作,发现缓存中没有想要的数据,从数据库中读取到的是旧数据,并把旧数据放入缓存。此时数据库与缓存都是旧值,数据没有不一致
  3. A线程将数据库更新完成,数据库中是更新后的新数据,缓存中是更新前的旧数据,造成数据不一致。

【 方案二 】先更新数据库,再淘汰缓存
在并发量较大的情况下,会导致数据的短暂不一致,但是数据会最终一致。
  1. A线程进行写操作,更新数据库,还未淘汰缓存
  2. B线程从缓存中可以读取到旧数据,此时数据不一致
  3. A线程完成淘汰缓存操作,其它线程进行读操作,从数据库中读入最新数据,此时数据一致

延时双删

上述方案二更简单,在高并发场景下也能保证数据的最终一致性,但是如果我就想用方案一呢?

什么是延时双删

先删再更新数据库 过N秒后再删一次缓存,怎么实现放后面spring-cache集成里,大概有 1.延时队列、2.线程池实现延时任务。

小结

  • 这些都是理论,真正写代码,有cache框架,哪有这么烦,很多人喜欢问,那我们就得理,理了总比不理好,写这个就是怕我自己忘,呵
  • 无论怎么样在高并发场景下,我们也只能要求缓存数据与数据库的最终一致性,如果要求强一致性还要缓存干嘛呢?操作直接走DB更香
  • 大多数情况下建议使用淘汰缓存机制,然后先更新数据库,再淘汰缓存,满足大多数的场景了

相关文章:

问题: redis-高并发场景下如何保证缓存数据与数据库的最终一致性

在高并发场景下&#xff0c;Redis 通常用作缓存层&#xff0c;与数据库结合使用以提高系统的性能。为了保证缓存数据与数据库的最终一致性&#xff0c;通常采用的有双写机制、缓存失效机制&#xff0c;基于双写机制、缓存失效机制又衍生出来了消息队列、事件驱动架构等 常见机…...

Stable Diffusion核心网络结构——CLIP Text Encoder

&#x1f33a;系列文章推荐&#x1f33a; 扩散模型系列文章正在持续的更新&#xff0c;更新节奏如下&#xff0c;先更新SD模型讲解&#xff0c;再更新相关的微调方法文章&#xff0c;敬请期待&#xff01;&#xff01;&#xff01;&#xff08;本文及其之前的文章均已更新&…...

C语言-11-18笔记

1.C语言数据类型 类型存储大小值范围char1 字节-128 到 127 或 0 到 255unsigned char1 字节0 到 255signed char1 字节-128 到 127int2 或 4 字节-32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647unsigned int2 或 4 字节0 到 65,535 或 0 到 4,294,967,295short2 字节…...

数据结构_图的遍历

深度优先搜索遍历 遍历思想 邻接矩阵上的遍历算法 void Map::DFSTraverse() {int i, v;for (i 0; i < MaxLen; i){visited[i] false;}for (i 0; i < Vexnum; i){// 如果顶点未访问&#xff0c;则进行深度优先搜索if (visited[i] false){DFS(i);}}cout << endl…...

设计LRU缓存

LRU缓存 LRU缓存的实现思路LRU缓存的操作C11 STL实现LRU缓存自行设计双向链表 哈希表 LRU&#xff08;Least Recently Used&#xff0c;最近最少使用&#xff09;缓存是一种常见的缓存淘汰算法&#xff0c;其基本思想是&#xff1a;当缓存空间已满时&#xff0c;移除最近最少使…...

python中的base64使用小笑话

在使用base64的时候将本地的图片转换为base64 代码如下&#xff0c;代码绝对正确 import base64 def image_to_data_uri(image_path):with open(image_path, rb) as image_file:image_data base64.b64encode(image_file.read()).decode(utf-8)file_extension image_path.sp…...

Python之time时间库

time时间库 概述获取当前时间time库datetime库区别 时间元组处理获取时间元组的各个部分时间戳和时间元组的转换 格式化时间格式化时间解析时间格式符号说明 暂停程序计时操作简单计时高精度计时计时器类的实现 UTC时间操作time库datetime库 概述 time是Python标准库中的一个模…...

Easyexcel(4-模板文件)

相关文章链接 Easyexcel&#xff08;1-注解使用&#xff09;Easyexcel&#xff08;2-文件读取&#xff09;Easyexcel&#xff08;3-文件导出&#xff09;Easyexcel&#xff08;4-模板文件&#xff09; 文件导出 获取 resources 目录下的文件&#xff0c;使用 withTemplate 获…...

国产linux系统(银河麒麟,统信uos)使用 PageOffice 动态生成word文件

PageOffice 国产版 &#xff1a;支持信创系统&#xff0c;支持银河麒麟V10和统信UOS&#xff0c;支持X86&#xff08;intel、兆芯、海光等&#xff09;、ARM&#xff08;飞腾、鲲鹏、麒麟等&#xff09;、龙芯&#xff08;LoogArch&#xff09;芯片架构。 数据区域填充文本 数…...

Window11+annie 视频下载器安装

一、ffmpeg环境的配置 下载annie之前需要先配置ffmpeg视频解码器。 网址下载地址 https://ffmpeg.org/download.html1、在网址中选择window版本 2、点击后选择该版本 3、下载完成后对压缩包进行解压&#xff0c;后进行环境的配置 &#xff08;1&#xff09;压缩包解压&#…...

SAP GR(Group Reporting)配置篇(七)

1.7、合并处理的配置 1.7.1 定义方法 菜单路径 组报表的SAP S4HANA >合并处理的配置>定义方法 事务代码 SPI4...

共建智能软件开发联合实验室,怿星科技助力东风柳汽加速智能化技术创新

11月14日&#xff0c;以“奋进70载&#xff0c;智创新纪元”为主题的2024东风柳汽第二届科技周在柳州盛大开幕&#xff0c;吸引了来自全国的汽车行业嘉宾、技术专家齐聚一堂&#xff0c;共襄盛举&#xff0c;一同探寻如何凭借 “新技术、新实力” 这一关键契机&#xff0c;为新…...

优化表单交互:在 el-select 组件中嵌入表格显示选项

介绍了一种通过 el-select 插槽实现表格样式数据展示的方案&#xff0c;可更直观地辅助用户选择。支持列配置、行数据绑定及自定义搜索&#xff0c;简洁高效&#xff0c;适用于复杂选择场景。完整代码见GitHub 仓库。 背景 在进行业务开发选择订单时&#xff0c;如果单纯的根…...

每日一题 LCR 079. 子集

LCR 079. 子集 主要应该考虑遍历的顺序 class Solution { public:vector<vector<int>> subsets(vector<int>& nums) {vector<vector<int>> ans;vector<int> temp;dfs(nums,0,temp,ans);return ans;}void dfs(vector<int> &…...

cocos creator 3.8 Node学习 3

//在Ts、js中 this指向当前的这个组件实例 //this下的一个数据成员node&#xff0c;指向组件实例化的这个节点 //同样也可以根据节点找到挂载的所有组件 //this.node 指向当前脚本挂载的节点//子节点与父节点的关系 // Node.parent是一个Node,Node.children是一个Node[] // th…...

微信小程序底部button,小米手机偶现布局错误的bug

预期结果&#xff1a;某button fixed 到页面底部&#xff0c;进入该页面时&#xff0c;正常显示button 实际结果&#xff1a;小米13pro&#xff0c;首次进入页面&#xff0c;button不显示。再次进入时&#xff0c;则正常展示 左侧为小米手机第一次进入。 遇到bug的解决思路&am…...

【计组】复习题

冯诺依曼型计算机的主要设计思想是什么&#xff1f;它包括哪些主要组成部分&#xff1f; 主要设计思想&#xff1a; ①采用二进制表示数据和指令&#xff0c;指令由操作码和地址码组成。 ②存储程序&#xff0c;程序控制&#xff1a;将程序和数据存放在存储器中&#xff0c;计算…...

Apache Maven 标准文件目录布局

Apache Maven 采用了一套标准的目录布局来组织项目文件。这种布局提供了一种结构化和一致的方式来管理项目资源&#xff0c;使得开发者更容易导航和维护项目。理解和使用标准目录布局对于有效的Maven项目管理至关重要。本文将探讨Maven标准目录布局的关键组成部分&#xff0c;并…...

Android 功耗分析(底层篇)

最近在网上发现关于功耗分析系列的文章很少&#xff0c;介绍详细的更少&#xff0c;于是便想记录总结一下功耗分析的相关知识&#xff0c;有不对的地方希望大家多指出&#xff0c;互相学习。本系列分为底层篇和上层篇。 大概从基础知识&#xff0c;测试手法&#xff0c;以及案例…...

【Xbim+C#】创建圆盘扫掠IfcSweptDiskSolid

基础回顾 https://blog.csdn.net/liqian_ken/article/details/143867404 https://blog.csdn.net/liqian_ken/article/details/114851319 效果图 代码示例 在前文基础上&#xff0c;增加一个工具方法&#xff1a; public static IfcProductDefinitionShape CreateDiskSolidSha…...

逻辑回归:给不确定性划界的分类大师

想象你是一名医生。面对患者的检查报告&#xff08;肿瘤大小、血液指标&#xff09;&#xff0c;你需要做出一个**决定性判断**&#xff1a;恶性还是良性&#xff1f;这种“非黑即白”的抉择&#xff0c;正是**逻辑回归&#xff08;Logistic Regression&#xff09;** 的战场&a…...

涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战

“&#x1f916;手搓TuyaAI语音指令 &#x1f60d;秒变表情包大师&#xff0c;让萌系Otto机器人&#x1f525;玩出智能新花样&#xff01;开整&#xff01;” &#x1f916; Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制&#xff08;TuyaAI…...

力扣-35.搜索插入位置

题目描述 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 class Solution {public int searchInsert(int[] nums, …...

九天毕昇深度学习平台 | 如何安装库?

pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子&#xff1a; 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...

HDFS分布式存储 zookeeper

hadoop介绍 狭义上hadoop是指apache的一款开源软件 用java语言实现开源框架&#xff0c;允许使用简单的变成模型跨计算机对大型集群进行分布式处理&#xff08;1.海量的数据存储 2.海量数据的计算&#xff09;Hadoop核心组件 hdfs&#xff08;分布式文件存储系统&#xff09;&a…...

【VLNs篇】07:NavRL—在动态环境中学习安全飞行

项目内容论文标题NavRL: 在动态环境中学习安全飞行 (NavRL: Learning Safe Flight in Dynamic Environments)核心问题解决无人机在包含静态和动态障碍物的复杂环境中进行安全、高效自主导航的挑战&#xff0c;克服传统方法和现有强化学习方法的局限性。核心算法基于近端策略优化…...

LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf

FTP 客服管理系统 实现kefu123登录&#xff0c;不允许匿名访问&#xff0c;kefu只能访问/data/kefu目录&#xff0c;不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...

springboot 日志类切面,接口成功记录日志,失败不记录

springboot 日志类切面&#xff0c;接口成功记录日志&#xff0c;失败不记录 自定义一个注解方法 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/***…...

02.运算符

目录 什么是运算符 算术运算符 1.基本四则运算符 2.增量运算符 3.自增/自减运算符 关系运算符 逻辑运算符 &&&#xff1a;逻辑与 ||&#xff1a;逻辑或 &#xff01;&#xff1a;逻辑非 短路求值 位运算符 按位与&&#xff1a; 按位或 | 按位取反~ …...

java高级——高阶函数、如何定义一个函数式接口类似stream流的filter

java高级——高阶函数、stream流 前情提要文章介绍一、函数伊始1.1 合格的函数1.2 有形的函数2. 函数对象2.1 函数对象——行为参数化2.2 函数对象——延迟执行 二、 函数编程语法1. 函数对象表现形式1.1 Lambda表达式1.2 方法引用&#xff08;Math::max&#xff09; 2 函数接口…...