Mysql/Redis缓存一致性

如何保证MySQL和Redis的缓存一致。从理论到实战。总结6种来感受一下。

理论知识

不好的方案

1.先写MySQL,再写Redis

图解说明:

这是一幅时序图,描述请求的先后调用顺序;

黄色的线是请求A,黑色的线是请求B;

黄色的文字是MySQL和Redis最终不一致的数据;

数据是从10更新为11;

后面的图为此规定

请求A、B都是先写MySQL,然后再写Redis,在高并发的情况下,如果请求A在写Redis时卡了一会,请求B已经一次完成了数据的更新,就会出现图中描述的问题。

图表述很清楚了,不过这里有个前提,就是对于读请求,先去读Redis,如果没有,再去读DB,但是读请求不会再写回Redis。就是读请求不会更新Redis。

2.先写Redis,再写MySQL

同1描述一样,秒懂。

3.先删除Redis,再写MySQL

和上面不一样的是,前面的请求A和B都是更新请求,这里的请求·A是跟新请求,但B请求是读请求,并且B的读请求会写回Redis。

请求A先删除缓存,可能因为卡顿,数据一直没有更新到MySQL,导致数据不一致。

这种情况出现的概率比较大,因为请求A更新MySQL可能会耗时比较长,而请求B的前两者都是查询,会比较快。

好的方案

4.先删除Redis,再写MySQL,再删除Redis

对于“先删除Redis,再写MySQL” ,如果要解决最后的不一致问题,其实再对Redis重新删除即可,这个就是“缓存双删”。

这个方案看看就行。

更好的方案是,异步串行化删除,即删除请求入队列

异步删除除对线上业务无影响,串行化处理保障并发情况下正确删除。

5.先写MySQL,再删除Redis

对于上面这种情况,对于第一次查询,请求B查询的数据10,但是MySQL的数据是11,只存在这一次不一致的情况,对于不是强一致的情况,对于不是强一致性要求的业务,可以容忍。对秒杀,库存就不行。

当请求B进行第二次查询时,因为没命中Redis,会重新擦汗一次DB,然后再回写到Redis。

这里需要满足两个条件:

        缓存刚好自动失效;

        请求B从数据库查10,回写缓存的消耗,比请求A写数据库,并且删除缓存的还长。

对于第二个条件,我们都知道更新DB肯定比查询耗时要长,所以出现这个情况的概率很小,同时满足上述条件情况更小。

6.先写MySQL,通过Binlog,异步更新Redis

这个方案,主要是监听MySQL的Binlog,然后通过异步的方式,将数据更新到Redis,这种方案有个前提,查询的请求,不会写回Redis。

这个方案,保证MySQL和Redis的最终一致性,但是如果中途请求B需要查询数据,如果缓存无数据,就直接查DB;如果缓存有数据,查询的数据也会存在不一致的情况。

所以这个方案,是实现最终一致性的终极方案,但是不能保证实时性。

几种方案比较

我们对比上述讨论的6种方案:‘

1.先写Redis,再写MySQL

这种方案,坑定是不会用,万一DB挂了,你把数据写到缓存,DB无数据,这个是灾难性的;

如果写DB失败,对Redis进行逆操作,那如果逆向操作失败,是不是得又搞个重试?

2.先写MySQL,再写Redis

对于并发量、一致性要求不高的项目,很多就是这么用的,我之前也经常这么搞

但是不建议这么做;

当Redis瞬间不可用的情况,需要报警出来,然后线下处理。

3.先删除Redis,再写MySQL

有懂得回答?

4.先删除Redis,再写MySQL,再删除Redis

这种方式虽然可行,但是感觉复杂,还要搞个消息队列去异步删除Redis。

5.先写MySQL,再删除Redis

比较推荐这总方案,删除Redis如果失败,可以再多重试几次,否则报警出来;

这个方案,是实时性最好的方案,在一些高并发场景种,推荐。

6.先写MySQL,通过Binlog。异步更新Redis

对于异地容灾,数据汇总,建议用这种,比如binlog+kafka,数据得一致性也可以达到秒级;

纯粹得高并发场景,不建议这种方案,入抢购,秒杀等。

个人结论:

实时性一致方案:采用“先写MySQL ,再删除Redis”的策略,这种情况下虽然也会存在两者不一致,但是需要满足的条件有点苛刻,所以是满足实时性条件下,能尽量满足一致性的最优解。

最终一致性方案:采用“先写MySQL,通过Binlog,异步更新Redis“,可以通过Binlog,结合消息队列异步更新Redis,是最终一致性的最优解。

项目实战

数据更新

因为项目对实时性要求高,所以采用方案5,先写MySQL,再删除Redis方式。

下面是一个示例,我们将文章的标签放入MySQL之后,在删除Redis,所有涉及到DB更新的操作都需要按照这种方式处理。

这里加了一个事务,如果Redis删除失败,MySQL的更新操作也要回滚,避免查询读取到脏数据。

    @Override@Transactional(rollbackFor = Exception.class)public void saveTag(TagReq tagReq) {TagDO tagDO = ArticleConverter.toDO(tagReq);//先写MySQLif (NumUtil.nullOrZero(tagReq.getTagId())) {tagDao.save(tagDO);} else {tagDO.setId(tagReq.getTagId());tagDao.updateById(tagDO);}//再删除RedisString redisKey = CACHE_TAG_PRE + tagDO.getId();RedisClient.del(redisKey);}@Override@Transactional(rollbackFor = Excetion.class)public void deleteTag(Integer tagId) {TagDO tagDO = tagDao.getById(tagId);if (tagDO != null){//先写MySQLtagDao.removeById(tagId);//再删除RedisString redisKey = CACHE_TAG_PRE + tagDO.getId();RedisClien.del(redisKey);}}@Overridepublic void operateTag(Integer tagId, Integer pushStatus) {TagDO tagDO = tagDao.getById(tagId);if (tagDO != null){//先写MySQLtagDO.setStatus(pushStatus);tagDao.updateById(tagDO);//再删除RedisString redisKey = CACHE_TAG_PRE + tagDO.getId();RedisClient.del(redisKey);}}

获取数据

也比较简单,先查缓存,如果有就直接返回;如果未查询到,需要先查询DB,再写入缓存。

我们放入缓存时,加了一个过期时间,用于兜底,万一两者不一致,缓存过期后,数据会重新更新到缓存。

    @Overridepublic TagDTO getTagById(Long tagId) {String redisKey = CACHE_TAG_PRE + tagId;// 先查询缓存,如果有就直接返回String tagInfoStr = RedisClient.getStr(redisKey);if (tagInfoStr != null && !tagInfoStr.isEmpty()) {return JsonUtil.toObj(tagInfoStr, TagDTO.class);}// 如果未查询到,需要先查询 DB ,再写入缓存TagDTO tagDTO = tagDao.selectById(tagId);tagInfoStr = JsonUtil.toStr(tagDTO);RedisClient.setStrWithExpire(redisKey, tagInfoStr, CACHE_TAG_EXPRIE_TIME);return tagDTO;}

测试用例

@Slf4j
public class MysqlRedisService extends BasicTest {@Autowiredprivate TagSettingService tagSettingService;@Testpublic void save() {TagReq tagReq = new TagReq();tagReq.setTag("Java");tagReq.setTagId(1L);tagSettingService.saveTag(tagReq);log.info("save success:{}", tagReq);}@Testpublic void query() {TagDTO tagDTO = tagSettingService.getTagById(1L);log.info("query tagInfo:{}", tagDTO);}
}

我们看一下Redis:

结果输出:

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

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

相关文章

TYPE C模拟耳机POP音产生缘由

关于耳机插拔的POP音问题,小白在之前的文章中讲述过关于3.5mm耳机的POP音产生原因。其实这类插拔问题的POP音不仅仅存在于3.5mm耳机,就连现在主流的Type C模拟耳机的插拔也存在此问题,今天小白就来讲一讲这类耳机产生POP音的缘由。 耳机左右…

两个笔记本如何将一个笔记本作为另一个笔记本的拓展屏

需求是有两个笔记本,一个笔记本闲置,另一个笔记本是主力本。想将另一个闲置的笔记本连接到主力本上作为拓展屏使用。网上搜了好久,有一些人提到了,也有一些视频但是文章比较少。简单总结一下吧 上述需求有两种方式 第一种&#x…

浅谈Redis 的 保护模式(protected-mode)

今天在一台服务器上面部署了redis,发现始终无法用工具远程连接,项目里面是正常的,就是工具不行,防火墙也关闭了.折腾了一会才突然想起来,是不是触发了保护模式. 什么时候触发保护模式protected-mode: 同时满足以下两个: 1.bind未指定ip 2.未配置密码 解决方案: 编辑redis…

基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的交通标志识别系统详解(深度学习模型+UI界面代码+训练数据集)

摘要:本篇博客详细介绍了利用深度学习构建交通标志识别系统的过程,并提供了完整的实现代码。该系统采用了先进的YOLOv8算法,并与YOLOv7、YOLOv6、YOLOv5等早期版本进行了性能评估对比,分析了性能指标如mAP、F1 Score等。文章深入探…

计算机组成原理实验报告1 | 实验1.1 运算器实验(键盘方式)

本文整理自博主大学本科《计算机组成原理》课程自己完成的实验报告。 —— *实验环境为学校机房实验箱。 目录 一、实验目的 二、实验内容 三、实验步骤及实验结果 Ⅰ、单片机键盘操作方式实验 1、实验连线(键盘实验) 2、实验过程 四、实验结果的…

代码随想录-java-栈与队列总结

栈(Stack):是只允许在一端进行插入或删除的线性表。栈是一种线性表,限定这种线性表只能在某一端进行插入和删除操作。进行操作的这一端称为栈顶。 队列(Queue)是只允许在一端进行插入操作,而在另…

Python使用FastAPI提供图片缩略图生成接口

使用pillow的thumbnail生成缩略图时,会保持原图的宽高比;使用的opencv的resize则不会 具体代码如下: #!/usr/bin/env python import re import sys from enum import Enum from io import BytesIO from pathlib import Path from typing im…

汇编课设——秒表2

1. 设计要求 基于 51 开发板,利用键盘作为按键输入,将数码管作为显示输出,实现电子秒表。 功能要求: (1)计时精度达到百分之一秒; (2)能按键记录下5次时间并通过按键回看 (3)设置时间,实现倒计时,时间到,数码管闪烁 10 次,并激发蜂鸣器,可通过按键解除。 2. 设计思…

思科网络中如何进行动态NAT配置

一、什么是动态NAT?动态NAT与静态NAT的区别是什么? (1)动态NAT(Network Address Translation)是一种网络地址转换技术,它会动态地将内部私有网络中的局域网IP地址映射为公共IP地址,…

Hack The Box-Codify

目录 信息收集 rustscan nmap dirsearch WEB 提权 get user get root 信息收集 rustscan ┌──(root㉿ru)-[~/kali/hackthebox] └─# rustscan -b 2250 10.10.11.239 --range0-65535 --ulimit4500 -- -A -sC .----. .-. .-. .----..---. .----. .---. .--. .-. …

JVM 类的加载篇

我们都知道一个类从加载到卸载一共分为七个过程 加载 - 链接(验证 - 准备 - 解析) - 初始化 - 使用 - 卸载 下文我们将详细解析这些过程 谁需要加载? 在Java中数据类型分为基本数据类型和引用数据类型,基本数据类型由虚拟机预定义,引用数据类型则需要类的加载 1.加载/装载(loa…

Docker入门二(应用部署、迁移与备份)

文章目录 一、应用部署1.MySQL部署2.Redis部署3.Nginx部署 二、迁移与备份1.容器做成镜像2.把镜像被分成压缩包 一、应用部署 1.MySQL部署 在dokcer中部署mysql,以后不需要在宿主机上装mysql1.做端口映射docker run -id --namemysql5.7 -p 3306:3306 -e MYSQL_ROOT…

网工内推 | 国企、上市公司网工、运维,CCNA即可,补贴福利多

01 深圳新思 招聘岗位:网络工程师(中电集团) 职责描述: 1:负责办公室电脑的桌面运维,主要是windows维护与应用维护; 2:负责办公室网络设备配置,如防火墙,交换…

CMake 编译 raylib 程序

CMakeLists.txt 内容如下: cmake_minimum_required(VERSION 3.0) project(t001) # 搜索指定目录下源文件 file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) # 包含头文件路径 include_directories(F:/vclib/raylib-5.0_win64_mingw-w64/include) # 包含静态…

CountDownLatch介绍和使用

1. CountDownLatch是什么 CountDownLatch 是 Java.util.concurrent 包中的一个同步工具类,用于控制线程的执行顺序。它的主要作用是让一个或多个线程等待其他线程完成操作后再继续执行。 2. CountDownLatch 类常用方法 CountDownLatch(int count) 是 CountDownLa…

软件测试基础概念

一、需求 定义:满足用户期望或正式规定文档所需条件和技能,包含用户需求 用户需求:用户使用产品所必须完成的任务 该需求比较简略 软件需求:详细描述开发人员必须实现的软件功能 需求是标准,测试人员按照这个标准测…

全球首个 AI 超级工程师:拥有全栈技能,一个指令就能完成整个开发过程

全球首位AI软件工程师Devin是由初创公司Cognition推出的,它被认为是世界上第一个完全自主的AI软件工程师[2][15]。Devin具备强大的编程和软件开发能力,能够在多个方面协助或完全独立地完成软件开发任务[15]。它的核心能力包括自学新语言、开发迭代App、自…

C语言分析基础排序算法——归并排序

目录 归并排序 递归版本 非递归版本 非递归版本的问题 归并排序小优化 归并排序 归并排序,分为分治以及合并,分治部分可以使用递归或者非递归完成,归并排序的基本思路是:将已有序的子序列合并,得到完全有序的序列…

社区维修平台|基于SpringBoot+ Mysql+Java+JSP技术的社区维修平台设计与实现(可运行源码+数据库+设计文档+部署说明+视频演示)

推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 目录 前台功能效果图 住户后台功能 维修员前台功能 维修员后台功能 管理员功能登录 系统功能设计 数据库E…

C++面试题和笔试题(四)

一、intx[6][4],(*p)[4];px;则*(p2)指向哪里? A X[0][1]B X[0][2]C X[1][0]D X[2][0] 官方解释: D int x[6][4], (*p)[4]; p x; 在这里,x 是一个二维数组,它有6行和4列。p 是一个指向具有4个整数的数组的指针。 当你执行…