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

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%会出纰漏,做不到强一致性,你如何保证最终一致性?

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 在Ubuntu22.04上部署ComfyUI
  • 【51单片机】外部中断和定时器中断
  • 【数据结构】链表OJ面试题5(题库+解析)
  • Java异常处理 throw和throws
  • 黄金交易策略(Nerve Knife):反趋势锁定单的处理机制
  • RISC-V指令格式
  • 2024.2.5 vscode连不上虚拟机,始终waiting for server log
  • 极值图论基础
  • C#的Char 结构的像IsLetterOrDigit(Char)等常见的方法
  • 【OpenVINO™】在 MacOS 上使用 OpenVINO™ C# API 部署 Yolov5 (下篇)
  • 【Spring MVC篇】参数的传递及json数据传参
  • 蓝桥杯基础知识7 vector
  • 【java】Hibernate访问数据库
  • 【知识整理】招人理念、组织结构、招聘
  • re:从0开始的CSS学习之路 9. 盒子水平布局
  • python3.6+scrapy+mysql 爬虫实战
  • 07.Android之多媒体问题
  • 2018一半小结一波
  • Docker 笔记(2):Dockerfile
  • js如何打印object对象
  • scrapy学习之路4(itemloder的使用)
  • seaborn 安装成功 + ImportError: DLL load failed: 找不到指定的模块 问题解决
  • TypeScript实现数据结构(一)栈,队列,链表
  • vue从创建到完整的饿了么(18)购物车详细信息的展示与删除
  • 前端代码风格自动化系列(二)之Commitlint
  • 原生JS动态加载JS、CSS文件及代码脚本
  • #QT(QCharts绘制曲线)
  • (1)Hilt的基本概念和使用
  • (1)无线电失控保护(二)
  • (55)MOS管专题--->(10)MOS管的封装
  • (el-Transfer)操作(不使用 ts):Element-plus 中 Select 组件动态设置 options 值需求的解决过程
  • (Pytorch框架)神经网络输出维度调试,做出我们自己的网络来!!(详细教程~)
  • (Redis使用系列) Springboot 整合Redisson 实现分布式锁 七
  • (ZT)薛涌:谈贫说富
  • (超详细)2-YOLOV5改进-添加SimAM注意力机制
  • (附源码)springboot 校园学生兼职系统 毕业设计 742122
  • (转)Linux整合apache和tomcat构建Web服务器
  • (自适应手机端)响应式服装服饰外贸企业网站模板
  • .[hudsonL@cock.li].mkp勒索加密数据库完美恢复---惜分飞
  • .NET CF命令行调试器MDbg入门(四) Attaching to Processes
  • .NET Framework .NET Core与 .NET 的区别
  • .net 后台导出excel ,word
  • .NET编程——利用C#调用海康机器人工业相机SDK实现回调取图与软触发取图【含免费源码】
  • .NET建议使用的大小写命名原则
  • .NET开源纪元:穿越封闭的迷雾,拥抱开放的星辰
  • .NET命名规范和开发约定
  • /usr/bin/env: node: No such file or directory
  • [ vulhub漏洞复现篇 ] Grafana任意文件读取漏洞CVE-2021-43798
  • [1159]adb判断手机屏幕状态并点亮屏幕
  • [AI]文心一言出圈的同时,NLP处理下的ChatGPT-4.5最新资讯
  • [C++]unordered系列关联式容器
  • [CF]Codeforces Round #551 (Div. 2)
  • [GN] Vue3.2 快速上手 ---- 核心语法2
  • [hadoop读书笔记] 第十五章 sqoop1.4.6小实验 - 将mysq数据导入HBASE
  • [HTML]Web前端开发技术28(HTML5、CSS3、JavaScript )JavaScript基础——喵喵画网页