Redis高级篇之缓存一致性详细教程

文章目录

  • 0 前言
  • 1.缓存双写一致性的理解
    • 1.1 缓存按照操作来分
  • 2. 数据库和缓存一致性的几种更新策略
    • 2.1 可以停机的情况
    • 2.2 我们讨论4种更新策略
    • 2.3 解决方案
  • 总结

0 前言

  缓存一致性问题在工作中绝对没办法回避的问题,比如:在实际开发过程中,通常添加把权限菜单存在缓存中,而用户登录成功以后获取的都是缓存中的权限菜单,当发现用户没有权限,想要添加时,已经添加上了,但是用户却查不出该权限,这说明添加只保存在数据中,并没有同步数据到缓存中,这就是本章节要讨论的缓存双写死一致性问题。
  而在找工作面试时,或遇到的问题如下:

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

1.缓存双写一致性的理解

  如下图所示,数据库中,缓存一致性问题,简单的说就是,数据库中的数据和缓存中的数据保持一致性。通常在开发时,查找数据是,先找缓存,如果缓存没有数据则查找数据库。
  查找流程一共分为三个步骤:
  1.缓存里有数据,直接返回
  2.缓存里无数据,查找数据库。
  3.从数据库中查找数据后,数据回写Redis,保持数据两边一致。
  其中,Redis挡在前面起到保护数据库的作用。因为数据库支持的并发量和Redis支持的并发量不是一个等级的。至于Redis为什么能够支持那么多的并发量,可去看看我之前写过的相关文章。Redis高阶篇之Redis单线程与多线程
在这里插入图片描述
  总之简单一句话,如果redis中有数据,​ 需要和数据库中的值相同。如果redis中无数据,​ 数据库中的值要是最新值,且准备回写redis。

1.1 缓存按照操作来分

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

  • 同步直写策略
    写数据库之后也同步写redis缓存,缓存和数据库中的数据一致;
    对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步直写策略
  • 异步缓写策略
    正常业务中,MySQL数据变了,但是可以在业务上容许出现一定时间后才作用于redis,比如仓库、物流系统
    异常情况出现了, 不得不将失败的动作重新修补,有可能需要借助kafka或者RabbitMQ等消息中间件,实现重试重写
  • 采用双检加锁策略
    多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。具体代码如下所示,仅供参考。
public User findUserById(Integer id){User user = null ;String key = CACHE_KEY_USER+iduser = redisTemplate.opsForValue.get(key);if(user  == null){// 2.高并发场景使用,进来先加锁,保证一个请求操作,让外面的线程等待,避免击穿数据库。synchronized(UserService.class){user = redisTemplate.opsForValue.get(key);if(user ==null){user = userMapper.selectByPrimaryKey(id);if(user == null){//3.1  redis和数据库  都无数据// 你具体细化,防止多次穿透,我们业务规定,记录一下这个null值的key,			   列入黑名单或者记录异常return user;}else{// 3.2 数据库里有数据,需要将数据回写到redis,保证下一次命中redisTemplate.opsForValue.setIfAbsent(key,user,7L,TimeUnit.DAYS);}}}}return user;
}

2. 数据库和缓存一致性的几种更新策略

  基本准则:总之,我们要达到最终一致性的目的。
  给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案。
  我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记,要以mysql的数据库写入库为准。
  上述方案和后续落地案例是调研后的主流+成熟的做法,但是考虑到各个公司业务系统的差距,不是100%绝对正确,不保证绝对适配全部情况,需要自己酌情选择打法,合适自己的最好。

2.1 可以停机的情况

  挂牌报错,凌晨升级,温馨提示,服务降级;
  单线程,这样重量级的数据操作最好不要多线程;

2.2 我们讨论4种更新策略

  1. 先更新数据库,在更新缓存,本文中在高并发的系统下不建议使用。原因是线程的执行顺序快慢先后顺序问题,造成了缓存存了脏数据。具体案例问题描述如下:
异常问题1

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

异常问题2

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

  2.先更新缓存,再更新数据库
  不推荐,业务上一般把MySQL作为底单数据库 ,保证最后解释。

[先更新缓存,再更新数据库],A、B两个线程发起调用 [正常逻辑]
1 A update redis 100
2 A update mysql 100
3 B update redis 80
4 B update mysql 80
[异常逻辑]多线程环境下,A. B两个线程有快有慢有并行
1 A update redis 100
3 B update redis 80
2 B update mysq| 80
4 A update mysql 100
mysql 100,redis 80

  3.× 先删除缓存,在更新数据库
  不推荐使用,已经很接近下面要讲的延迟双删了 ,步骤分析,先删除缓存,再更新数据库。异常问题如下描述:

1 A线程先成功删除了redis里面的数据,然后去更新mysql,此时mysql正在更新中,还没有结束。(比如网络延时)
B突然出现要来读取缓存数据。
2 此时redis里面的数据是空的,B线程来读取,先去读redis里数据(已经被A线程delete掉了),此处出来2个问题:
2.1 B从mysq|获得了旧值 B线程发现redis里没有(缓存缺失)马上去mysql里面读取,从数据库里面读取来的是旧值。
2.2 B会把获得的旧值写回redis 获得旧值数据后返回前台并回写进redis(刚被A线程删除的旧数据有极大可能早被写回了)。 3 A线程更新完mysql,发现redis里面的缓存是脏数据,A线程直接懵逼了,o(T_ .τ)o
两个并发操作,一个是更新操作,另一个是查询操作,A删除缓存后,B查询操作没有命中缓存,B先把老数据读出来后放到缓存中,然后A更新操作更新了数据库。
于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。 4总结流程:
(1)请求A进行写操作,删除redis缓存后,工作正在进行中,更新mysql… A还么有彻底更新完mysql,还没commit
(2)请求B开工查询,查询redis发现缓存不存在(被A从redis中删除了)
(3)请求B继续,去数据库查询得到了mysq中的旧值(A还没有更新完) (4)请求B将旧值写回redis缓存
(5)请求A将新值写入mysql数据库 上述情况就会导致不一致的情形出现。

  先删除缓存,再更新数据库:如果数据库更新失败或超时或返回不及时,导致B线程请求访问缓存时发现redis里面没数据,缓存缺失,B再去读取mysql时,从数据库中读取到旧值,还写回redis, 导致A白干了。

  4.先更新数据库,再删除缓存。
  目前用的比较多,但是也会有异常情况。异常问题如下表格所示:
在这里插入图片描述
  先更新数据库,在删除缓存,假如缓存删除失败或者来不及删除,导致请求再次访问redis时缓存命中,读取到的是缓存的旧值。

2.3 解决方案

  采用延时双删策略。
  加上sleep的这段时间,就是为了让线程B能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程A再进行删除。所以,线程A sleep的时间,就需要大于线程B读取数据再写入缓存的时间。这样一来,其它线程读取数据时,会发现缓存缺失,所以会从数据库中读取最新值。因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以我们也把它叫做“延迟双删”。
这个删除该休眠多久呢?线程A sleep的时间,就需要大于线程B读取数据再写入缓存的时间。

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

  • 业务指导思想
    微软云:点击链接跳转
    后面的阿里巴巴canal也是类似的思想
    订阅binlog程序在MySQL中有现成的中间件叫canal,可以完成订阅binlog日志的功能。

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

  • 类似经典的分布式事务问题,只有一个权威答案,只能达到最终一致性。
    流量充值,先下发短信实际充值可能滞后5分钟,可以接受。
    电商发货,短信下发但是物流明天见。

总结

  方案如何选择?利弊如何
  在大多数业务场景下, 个人建议是,优先使用先更新数据库,再删除缓存的方案(先更库→后删存)。理由如下:
  1.先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力导致打满mysql。
  2.如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。
  多补充一句:如果使用先更新数据库,再删除缓存的方案
  如果业务层要求必须读取一致性的数据,那么我们就需要在更新数据库时,先在Redis缓存客户端暂停并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性,这是理论可以达到的效果,但实际,不推荐,因为真实生产环境中,分布式下很难做到实时一致性,一般都是最终一致性。
在这里插入图片描述
  ,本篇只介绍了缓存数据一致性问题,下一篇就可以到数据一致性的落地了。

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

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

相关文章

C++_day2

目录 1. 引用 reference(重点) 1.1 基础使用 1.2 特性 1.3 引用参数 2. C窄化(了解) 3. 输入(熟悉) 4. string 字符串类(掌握) 4.1 基础使用 4.2 取出元素 4.3 字符串与数字转换 5. …

Vuex的基本使用

文章目录 一、Vuex概述1.是什么2.使用场景3.优势4.注意二、如何构建vuex多组件共享数据环境1.创建项目2.创建三个组件3.源代码三、vuex 的使用 - 创建仓库1.安装 vuex2.新建 `store/index.js` 专门存放 vuex3.创建仓库 `store/index.js`4 在 main.js 中导入挂载到 Vue 实例上5.…

WPF+MVVM案例实战(二十一)- 制作一个侧边弹窗栏(CD类)

文章目录 1、案例效果1、侧边栏分类2、CD类侧边弹窗实现1、样式代码实现2、功能代码实现3 运行效果4、源代码获取1、案例效果 1、侧边栏分类 A类 :左侧弹出侧边栏B类 :右侧弹出侧边栏C类 :顶部弹出侧边栏D类 :底部弹出侧边栏2、CD类侧边弹窗实现 1、样式代码实现 在原有的…

揭开广告引擎的神秘面纱:如何在0.1秒内精准匹配用户需求?

目录 一、广告系统与广告引擎介绍 (一)广告系统与广告粗分 (二)广告引擎在广告系统中的重要性分析 二、广告引擎整体架构和工作过程 (一)一般概述 (二)核心功能架构图 三、标…

[论文阅读]A Survey of Embodied Learning for Object-Centric Robotic Manipulation

Abstract --以对象为中心的机器人操纵的Embodied learning是体现人工智能中一个快速发展且具有挑战性的领域。它对于推进下一代智能机器人至关重要,最近引起了人们的极大兴趣。与数据驱动的机器学习方法不同,具身学习侧重于通过与环境的物理交互和感知反…

NFTScan Site:以蓝标认证与高级项目管理功能赋能 NFT 项目

自 NFTScan Site 上线以来,它迅速成为 NFT 市场中的一支重要力量,凭借对各类 NFT 集合、市场以及 NFTfi 项目的认证获得了广泛认可。这个平台帮助许多项目提升了曝光度和可见性,为它们在竞争激烈的 NFT 市场中创造了更大的成功机会。 在最新更…

指数分布的原理和应用

本文介绍指数分布,及其推导原理。 Ref: 指数分布 开始之前,先看个概率密度函数的小问题: 问题描述:你于上午10点到达车站,车在10点到10:30 之间到达的时刻 X 的概率密度函数如图: 则使用分段积分&#xff0…

Javase——正则表达式

正则表达式的相关使用 public static void main(String[] args) {//校验QQ号 System.out.println("3602222222".matches("[1-9][0-9]{4,}"));// 校验18位身份证号 System.out.println("11050220240830901X".matches("^([0-9]){7,18}…

安装中文版 Matlab R2022a

下载安装包 压缩包有点大,大概20G 百度网盘:下载链接 提取码:rmja 安装 解压后打开目录,右键以管理员身份运行 setup.exe 选择输入安装秘钥 输入秘钥: 50874-33247-14209-37962-45495-25133-28159-33348-18070-6088…

SICTF Round #4|MISC

1.派森 腐乳昂木 奥普瑞特儿 阴坡尔特 艾克斯奥尔 腐乳昂木 提克有第爱慕 阴坡尔特 ⭐ 弗拉格 等于 布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉 印刻 等于 左中括号右中括号 佛儿 唉 因 梯软者左括号 零,楞左括号弗拉格右…

保研考研机试攻略:python笔记(2)

🐨🐨🐨宝子们好呀,今天我们继续来学习N诺提供的python笔记,fighting!( •̀ ω •́ )✧ 对这个系列感兴趣的宝子欢迎关注保研考研机试攻略专栏哦 ~ 目录 🐨🐨🐨4进制转…

Hyper-V 安装 KylinOS V10【图文教程】

文章目录 下载 KylinOSHyper-V 安装 KylinOS新建虚拟机配置虚拟机启动虚拟机并配置下载 KylinOS KylinOS 没有直接提供下载地址,需要在页面上点试用,填写个人信息后,才能看到下载地址。 https://www.kylinos.cn/support/trial.html?trial=425887 试用地址:产品试用申请国…

LeetCode 0685.冗余连接 II:并查集(和I有何不同分析)——详细题解(附图)

【LetMeFly】685.冗余连接 II:并查集(和I有何不同分析)——详细题解(附图) 力扣题目链接:https://leetcode.cn/problems/redundant-connection-ii/ 在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点&…

mysql查表相关练习

作业要求: 单表练习: 1 . 查询出部门编号为 D2019060011 的所有员工 2 . 所有财务总监的姓名、编号和部门编号。 3 . 找出奖金高于工资的员工。 4 . 找出奖金高于工资 40% 的员工。 5 找出部门编号为 D2019090011 中所有财务总监,和…

GHuNeRF: Generalizable Human NeRF from a Monocular Video

研究背景 研究问题:这篇文章要解决的问题是学习一个从单目视频中泛化的人类NeRF模型。尽管现有的泛化人类NeRF已经取得了令人印象深刻的成果,但它们需要多视图图像或视频,这在某些情况下可能不可用。此外,一些基于单目视频的人类…

Linux - grep的正则用法

新建u.txt,文本内容如图: 搜寻特定字符串 利用中括号[]搜寻集合字符 行首与行位字符^$ 任意一个字符.与重复字符*限定连续RE字符范围{} 总结:

项目模块十五:HttpResponse模块

一、模块设计思路 存储HTTP应答要素&#xff0c;提供简单接口 二、成员变量 int _status; // 应答状态码 unordered_map<string, string> _headers; // 报头字段 string _body; // 应答正文 bool _redirect_flag; // 是否重定向信息 stri…

从零开始的c++之旅——继承

1. 继承 1.继承概念及定义 继承是面向对象编程的三大特点之一&#xff0c;它使得我们可以在原有类特性的基础之上&#xff0c;增加方法 和属性&#xff0c;这样产生的新的类&#xff0c;称为派生类。 继承 呈现了⾯向对象程序设计的层次结构&#xff0c;以前我们接触的…

6.1、实验一:静态路由

源文件获取&#xff1a;6.1_实验一&#xff1a;静态路由.pkt: https://url02.ctfile.com/f/61945102-1420248902-c5a99e?p2707 (访问密码: 2707) 一、目的 理解路由表的概念 会使用基础命令 根据需求正确配置静态路由 二、准备实验 1.实验要求 让PC0、PC1、PC2三台电脑…

logback日志级别动态切换四种方案

生产环境中经常有需要动态修改日志级别。 现在就介绍几种方案 方案一&#xff1a;开启logback的自动扫描更新 配置如下 <?xml version"1.0" encoding"UTF-8"?> <configuration scan"true" scanPeriod"60 seconds" debug…