Redis(十三)缓存双写一致性策略

文章目录

  • 概述
    • 示例
  • 缓存双写一致性
    • 缓存按照操作来分,细分2种
      • 读写缓存:同步直写策略
      • 读写缓存:异步缓写策略
      • 双检加锁策略
  • 数据库和缓存一致性更新策略
    • 先更新数据库,再更新缓存
    • 先更新缓存,再更新数据库
    • 先删除缓存,再更新数据库
      • 解决方案:延时双删策略
    • 先更新数据库,再删除缓存
    • 解决方案
    • 总结
  • 问题示例

概述

示例

在这里插入图片描述

缓存双写一致性

  1. 如果redis中有数据
    需要和数据库中的值相同
  2. 如果redis中无数据
    数据库中的值要是最新值,且准备回写redis

缓存按照操作来分,细分2种

  1. 只读缓存
  2. 读写缓存

读写缓存:同步直写策略

  1. 写数据库后也同步写redis缓存,缓存和数据库中的数据一致;
  2. 对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步直写策略

读写缓存:异步缓写策略

  1. 正常业务运行中,mysql数据变动了,但是可以在业务上容许出现一定时间后才作用于redis,比如仓库、物流系统
  2. 异常情况出现,不得不将失败的动作重新修补,有可能需要借助kafka或者RabbitMQ等消息中间件实现重试重写

双检加锁策略

多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。
后面的线程进来发现已经有缓存了,就直接走缓存。

在这里插入图片描述

import com.atguigu.redis.entities.User;
import com.atguigu.redis.mapper.UserMapper;
import io.swagger.models.auth.In;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;@Service
@Slf4j
public class UserService {public static final String CACHE_KEY_USER = "user:";@Resourceprivate UserMapper userMapper;@Resourceprivate RedisTemplate redisTemplate;/*** 业务逻辑没有写错,对于小厂中厂(QPS《=1000)可以使用,但是大厂不行* @param id* @return*/public User findUserById(Integer id){User user = null;String key = CACHE_KEY_USER+id;//1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysqluser = (User) redisTemplate.opsForValue().get(key);if(user == null){//2 redis里面无,继续查询mysqluser = userMapper.selectByPrimaryKey(id);if(user == null){//3.1 redis+mysql 都无数据//你具体细化,防止多次穿透,我们业务规定,记录下导致穿透的这个key回写redisreturn user;}else{//3.2 mysql有,需要将数据写回redis,保证下一次的缓存命中率redisTemplate.opsForValue().set(key,user);}}return user;}/*** 加强补充,避免突然key失效了,打爆mysql,做一下预防,尽量不出现击穿的情况。* @param id* @return*/public User findUserById2(Integer id){User user = null;String key = CACHE_KEY_USER+id;//1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql,// 第1次查询redis,加锁前user = (User) redisTemplate.opsForValue().get(key);if(user == null) {//2 大厂用,对于高QPS的优化,进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysqlsynchronized (UserService.class){//第2次查询redis,加锁后user = (User) redisTemplate.opsForValue().get(key);//3 二次查redis还是null,可以去查mysql了(mysql默认有数据)if (user == null) {//4 查询mysql拿数据(mysql默认有数据)user = userMapper.selectByPrimaryKey(id);if (user == null) {return null;}else{redisTemplate.opsForValue().setIfAbsent(key,user,7L,TimeUnit.DAYS);}}}}return user;}}

数据库和缓存一致性更新策略

给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案。
我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记,要以mysql的数据库写入库为准。

先更新数据库,再更新缓存

异常问题
1 先更新mysql的某商品的库存,当前商品的库存是100,更新为99个。
2 先更新mysql修改为99成功,然后更新redis。
3 此时假设异常出现,更新redis失败了,这导致mysql里面的库存是99而redis里面的还是100 。
4 上述发生,会让数据库里面和缓存redis里面数据不一致,读到redis脏数据

异常问题

【先更新数据库,再更新缓存】,A、B两个线程发起调用【正常逻辑】1 A update mysql 1002 A update redis 1003 B update mysql 804 B update redis 80=============================
【异常逻辑】多线程环境下,A、B两个线程有快有慢,有前有后有并行1 A update mysql 1003 B update mysql 804 B update redis 802 A update redis 100=============================最终结果,mysql和redis数据不一致,o(╥﹏╥)o,mysql80,redis100

先更新缓存,再更新数据库

mysql一般作为底单数据库,保证最后解释

【先更新缓存,再更新数据库】,AB两个线程发起调用【正常逻辑】1 A update redis 1002 A update mysql 1003 B update redis 804 B update mysql 80====================================
【异常逻辑】多线程环境下,AB两个线程有快有慢有并行A update redis  100B update redis  80B update mysql 80A update mysql 100
----mysql100,redis80

先删除缓存,再更新数据库

异常问题

(1)请求A进行写操作,删除redis缓存后,工作正在进行中,更新mysql......A还么有彻底更新完mysql,还没commit(2)请求B开工查询,查询redis发现缓存不存在(被A从redis中删除了)(3)请求B继续,去数据库查询得到了mysql中的旧值(A还没有更新完)(4)请求B将旧值写回redis缓存(5)请求A将新值写入mysql数据库 上述情况就会导致不一致的情形出现。 
时间线程A线程B出现的问题
t1请求A进行写操作,删除缓存成功后,工作正在mysql进行中…
t21 缓存中读取不到,立刻读mysql,由于A还没有对mysql更新完,读到的是旧值 2 还把从mysql读取的旧值,写回了redis1 A还没有更新完mysql,导致B读到了旧值 2 线程B遵守回写机制,把旧值写回redis,导致其它请求读取的还是旧值,A白干了。
t3A更新完mysql数据库的值redis是被B写回的旧值,mysql是被A更新的新值。出现了,数据不一致问题。

解决方案:延时双删策略

在这里插入图片描述
在这里插入图片描述
问题示例:

线程A sleep的时间,就需要大于线程B读取数据再写入缓存的时间。
这个时间怎么确定呢?

  • 第一种方法:
    在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,自行评估自己的项目的读数据业务逻辑的耗时,
    以此为基础来进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。
    这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
  • 第二种方法:
    新启动一个后台监控程序,比如后面要讲解的WatchDog监控程序,会加时

这种方法吞吐降低怎么办?
使用异步
使用WatchDog

在这里插入图片描述

先更新数据库,再删除缓存

时间线程A线程B出现的问题
t1更新数据库中的值…
t2缓存中立刻命中,此时B读取的是缓存旧值。A还没有来得及删除缓存的值,导致B缓存命中读到旧值。
t3更新缓存的数据

假如缓存删除失败或者来不及,导致请求再次访问redis时缓存命中,读取到的是缓存旧值。
微软云案例:https://docs.microsoft.com/en-us/azure/architecture/patterns/cache-aside
阿里巴巴canal:

解决方案

在这里插入图片描述

  1. 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka/RabbitMQ等)。
  2. 当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。
  3. 如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了,否则还需要再次进行重试
  4. 如果重试超过的一定次数后还是没有成功,我们就需要向业务层发送报错信息了,通知运维人员。

最终解决方案:最终一致性
案例:

  1. 流量充值,先下发短信实际充值可能滞后5分钟,可以接受
  2. 电商发货,短信下发但是物流明天见

总结

优先使用先更新数据库,再删除缓存的方案(先更库→后删存)。理由如下:

  1. 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力导致打满mysql。
  2. 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。

如果业务层要求必须读取一致性的数据,那么我们就需要在更新数据库时,先在Redis缓存客户端暂停并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性,这是理论可以达到的效果,但
实际,不推荐,因为真实生产环境中,分布式下很难做到实时一致性,一般都是最终一致性

如果使用先更新数据库,再删除缓存的方案

策略高并发多线程条件下问题现象解决方案
先删除redis缓存,再更新mysql缓存删除成功但数据库更新失败Java程序从数据库中读到旧值再次更新数据库,重试
缓存删除成功但数据库更新中…有并发读请求并发请求从数据库读到旧值并回写到redis,导致后续都是从redis读取到旧值延迟双删
先更新mysql,再删除redis缓存数据库更新成功,但缓存删除失败Java程序从redis中读到旧值再次删除缓存,重试
数据库更新成功但缓存删除中…有并发读请求并发请求从缓存读到旧值等待redis删除完成,这段时间有数据不一致,短暂存在。

问题示例

  1. 你只要用缓存,就可能会涉及到redis缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
  2. 双写一致性,你先动缓存redis还是数据库mysql哪一个?why?
  3. 延时双删你做过吗?会有哪些问题?
  4. 有这么一种憶况,微服务查询redis无,mysql有,为保证数据双写一致性回写redis你需要注意什么?双减加锁策略了解过吗?如何尽量避免缓存击穿
  5. redis和mysql双写100%会出纰漏,做不到强一致性,你如何保证最终一致性?

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

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

相关文章

不懂快团团大团长对接?凭什么快团团的钱轮到你赚?

对接头部快团团大团长,让快团团大团长帮你卖货 分享几个推品的关键词: 1.推品的内容:产品实拍图核心卖点 不要上来就发笔记,你的产品图和文案还没吸引人,就发笔记没有人看。 可以先发你产品的简短卖点和图片&#xff…

ionic报错:Cannot read properties of undefined (reading ‘classList‘)

报错信息&#xff1a; [ionic/vue Warning]: The view you are trying to render for path /tabs/tab1 does not have the required <ion-page> component. Transitions and lifecycle methods may not work as expected.See https://ionicframework.com/docs/vue/navig…

#Z1656. 树上觅食

Description 小J是一只猴子&#xff0c;住在树上&#xff0c;树上每个点都有一些果子 小J每天要固定吃掉S个果子 小J还要多在树上运动一下&#xff0c;于是他想知道这个树上有多少路径&#xff0c;路径上点的果子的总和正好为S 并且路径上的点的深度是连续增加的 Format …

Go语言每日一练链表篇(一)

题目传送门 牛客面试必刷101题 ----------反转链表 题目以及答案源码 反转链表 题目 解题方法 递归函数遍历链表并反转 package main import _"fmt" import . "nc_tools" /** type ListNode struct{* Val int* Next *ListNode* }*//*** 代码中的…

【数据结构笔记】线性表(代码)

文章目录 顺序表基本操作InitList(&L)静态分配动态分配cc 基本操作实现 顺序表 基本操作 InitList(&L) Length(L); LocateElem(L, e); GetElem(L, i); ListInsert(&L, i, e); ListDelete(&L, i, &e); PrintList(L); Empty(L); DestroyList(&L);InitL…

学习总结14

# 【CSGRound1】天下第一 ## 题目背景 天下第一的 cbw 以主席的身份在 8102 年统治全宇宙后&#xff0c;开始了自己休闲的生活&#xff0c;并邀请自己的好友每天都来和他做游戏。由于 cbw 想要显出自己平易近人&#xff0c;所以 zhouwc 虽然是一个蒟蒻&#xff0c;也有能和 c…

Docker- chapter 1

note 1: docker 利用 volume 进行 presist data。 eg : compose.yaml&#xff1a; volumes:database: //# named db by self list golbal volumes&#xff1a; docker volume ls # the volumes on the disk inpect someone volume&#xff1a; docker volume inspect m…

关于harbor做HA

我起初是用helm在k8s上装的harbor&#xff0c;最近遇到如下故障&#xff1a;就是服务器硬件设备故障突然死机&#xff0c;恰巧是harbor容器所在的服务器&#xff0c;其他在这个服务器上运行的容器&#xff0c;由kubelet调度到其他正常的服务器上重启去了&#xff0c;但harbkor并…

@ 代码随想录算法训练营第6周(C语言)|Day39(动态规划)

代码随想录算法训练营第6周&#xff08;C语言&#xff09;|Day39&#xff08;动态规划&#xff09; Day39、动态规划&#xff08;包含题目 ● 62.不同路径 ● 63. 不同路径 II &#xff09; 62.不同路径 题目描述 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点…

opencv案例实战:条码区域分割

前言 识别二维码是一个日常生活中常见的应用,而识别之前,需要先分割出条形码的区域来获取条形码。我们可以使用OpenCV便捷的获取条码的区域。 逐步分析 为了了解数据处理的过程,我们逐步分析并显示处理过程 查看图像 在读入时,传入参数cv2.IMREAD_GRAYSCALE可以直接按…

2024年陕西省安全员C证证考试题库及陕西省安全员C证试题解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年陕西省安全员C证证考试题库及陕西省安全员C证试题解析是安全生产模拟考试一点通结合&#xff08;安监局&#xff09;特种作业人员操作证考试大纲和&#xff08;质检局&#xff09;特种设备作业人员上岗证考试大…

【DC-9靶场渗透】

文章目录 前言 一、确定靶机地址 二、信息收集 三、寻找漏洞 四、进一步漏洞挖掘 五、关键文件 六、ssh爆破 七、提权 总结 前言 马上过年了&#xff0c;年前再做一下DC靶场最后一个靶机。 一、确定靶机地址 1、可使用arp-scan命令 靶机地址为&#xff1a;172.16.10…

C语言---------对操作符的进一步认识

操作符中有⼀些操作符和⼆进制有关系&#xff0c;我们先学习了⼀下⼆进制的和进制转换的知识。 1.原码、反码和补码。 有符号整数的三种表⽰⽅法均有符号位和数值位两部分&#xff0c; 2进制序列中&#xff0c;最⾼位的1位是被当做符号位&#xff0c;剩余的都是数值位。 符号…

GDB调试命令大全

GDB调试命令大全 启动运行设置/查看断点(breakpoint)设置/查看观察点(watchpoint)设置/查看捕捉点(catchpoint)维护停止点维护条件停止点停止点设置运行命令断点菜单恢复程序运行和单步调试信号产生信号量线程 转载自&#xff1a; https://www.cnblogs.com/gqtcgq/p/7511974.h…

树的三种遍历方式-算法

题目 树的三种遍历方式&#xff0c;前序&#xff0c;中序&#xff0c;后续 解题 通过递归 public static List<Integer> preorderTraversal(TreeNode treeNode) {List<Integer> result new ArrayList<>();if (treeNode null) {return result;}List<…

python coding with ChatGPT 打卡第18天| 二叉树:从中序与后序遍历序列构造二叉树、最大二叉树

相关推荐 python coding with ChatGPT 打卡第12天| 二叉树&#xff1a;理论基础 python coding with ChatGPT 打卡第13天| 二叉树的深度优先遍历 python coding with ChatGPT 打卡第14天| 二叉树的广度优先遍历 python coding with ChatGPT 打卡第15天| 二叉树&#xff1a;翻转…

【Spring基础】从0开始学习Spring(2)

前言 在上篇文章&#xff0c;我已经讲了Spring中最核心的知识点&#xff1a;IoC&#xff08;控制反转&#xff09;以及DI&#xff08;依赖注入&#xff09;。这篇文章&#xff0c;我将讲一下关于Spring框架中的其它比较琐碎但是又还是挺重要的知识点&#xff0c;因此&#xff…

【Android-Compose】手势检测实现按下、单击、双击、长按事件,以及避免频繁单击事件的简单方法

目录&#xff1a; 1 不需要双击事件 规避频繁单击事件2 需要双击事件&#xff08;常规写法&#xff09;3 后记&#xff1a;不建议使用上面的代码自定义按钮 1 不需要双击事件 规避频繁单击事件 var firstClickTime by remember { mutableStateOf(System.currentTimeMillis()…

vue实现查询搜索框下拉字典

字典表 前端页面显示 依据这个字典表实现动态查询 初始化数组 首先先在全局变量里定义一个数据存放查询出来的数据 data() {return {dicts: []};},生命周期 查询的时候是声明周期开始的时候&#xff0c;原本增删改查页面在生命周期开始的时候就查询了页面的数据获得了列表值…

ElasticSearch-SpringBoot整合ElasticSearch

六、SpringBoot整合ElasticSearch 1、浏览官方文档 1、查找跟ES客户端相关的文档 使用Java REST Client 选择Java Hight Level REST Client 2、创建项目的准备 1.找到原生的依赖 2.找到对象 3.分析这个类里面的方法 3、正式创建项目 1.创建工程 2.导入依赖 注意依赖版本…