Redis 双写一致原理篇

news2024/9/20 9:36:15

前言

我们都知道,redis一般的作用是顶在mysql前面做一个"带刀侍卫"的角色,可以缓解mysql的服务压力,但是我们如何保证数据库的数据和redis缓存中的数据的双写一致呢,我们这里先说一遍流程,然后以流程为切入点来谈谈redis和mysql的双写一致性是如何保证的吧

流程

首先我们先看一个图

这就是进行一次查询的基本流程

第一步就是查询redis看看是否有对应的热点数据,没有的话,就去mysql进行查询

mysql查询到了再进行回写进redis,这样下一个用户来进行查询的时候,这里就可以直接从redis进行查询对应的数据了

但是这里就会涉及到很多问题了,如何保证双写一致性??

我更新数据的更新策略是先更新mysql还是先更新redis??

下面我们慢慢说

缓存双写一致性的理解 

这里查询如过redis有数据那么就进行立即返回

如果redis没有数据那么就打到mysql中查看数据并进行回写

这里的缓存我们可以分为两种

只读缓存和可写缓存

可写缓存这里我们也分为两种写入策略

同步直写策略和异步缓写策略

同步直写策略就是读取完mysql的数据迅速进行一个回写操作

如果这里想保存数据的高度一致,就最好是使用同步缓写的操作

比如这个时候我们想把一个vip的状态进行快速的切换,充值成功立马就得更新

异步缓写策略就是我们一个物流状态的更新,或者是订单成功的积分操作都可以使用一个异步的操作,因为这个操作是非即时性质的

但是这里也可能导致很多错误

比如假设这里回写失败了咋办

我们可以使用一个消息队列等来进行对应的补偿重试机制

假设高并发的情况下出现了对应的数据进行覆盖

或者可能出现mysql死锁mysql负载过高的情况

这里我们就可以使用双检加锁策略解决问题  

这里主要是为了保证每次只有一个请求打在mysql上,减少mysql服务器的负载

至于后面的值覆盖问题一会儿再说

我们展示一段代码再进行对应的讲解

@Service
@Slf4j
public class UserService {
    public static final String CACHE_KEY_USER = "user:";
    @Resource
    private UserMapper userMapper;
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 业务逻辑没有写错,对于小厂中厂(QPS《=1000)可以使用,但是大厂不行
     * @param id
     * @return
     */
    public User findUserById(Integer id)
    {
        User user = null;
        String key = CACHE_KEY_USER+id;

        //1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql
        user = (User) redisTemplate.opsForValue().get(key);

        if(user == null)
        {
            //2 redis里面无,继续查询mysql
            user = userMapper.selectByPrimaryKey(id);
            if(user == null)
            {
                //3.1 redis+mysql 都无数据
                //你具体细化,防止多次穿透,我们业务规定,记录下导致穿透的这个key回写redis
                return user;
            }else{
                //3.2 mysql有,需要将数据写回redis,保证下一次的缓存命中率
                redisTemplate.opsForValue().set(key,user);
            }
        }
        return user;
    }

这段代码对于并发量低的情况下还是可以使用的

但是假设这里redis的数据同一时间有很多用户访问,但是redis没有,得去mysql的底单数据表去查询,这里我们就得考虑万一都打在mysql上,导致mysql的压力过大就不好了,所以我们建议加锁,每次只让一个线程去操作对应的用户即可  

这里代码示例可以在mysql操作加上一个互斥锁

注意这里我们检查了两次,这是因为假设a线程和b线程都查询到redis没有这个数据,但是此时a线程被调度走了,b线程已经将数据带回来了,此时再调度到a线程a线程直接查询redis即可,避免给mysql更大的压力,下面我们展示加锁后的代码

/**
     * 加强补充,避免突然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等待一下,避免击穿mysql
            synchronized (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{
                        //5 mysql里面有数据的,需要回写redis,完成数据一致性的同步工作
                        redisTemplate.opsForValue().setIfAbsent(key,user,7L,TimeUnit.DAYS);
                    }
                }
            }
        }
        return user;
    }

}

更新策略

我们知道mysql和redis的数据得保证一致性,但是这个强一致性是不太好保证的,我们只能保证最终一致性,那么mysql和redis我们先保证谁的数据更新呢,就是我们接下来要探讨的问题了

注:这里的策略仅供参考,以实际需求为准

策略1:停机更新

首先第一个策略不是很常用但是很有效,直接在用户量较少的时候停机进行服务降级更新

此时让运维工程师使用单线程来操作即可,因为多线程出错的概率更大

策略2:先更新数据库,再更新redis

先更新数据库再更新redis可能导致一些异常,举例如下

假设现在更新mysql成功了,但是redis回写却失败了

这里就很可能导致数据库和缓存中的数据就不一致了

策略3:先更新redis,再更新数据库

这也是存在和以上差不多的情况的

技术上可以做,但是不太推荐,因为我们一般是将mysql作为一个底单数据库的

这里异常情况下数据同样是不一致的

策略4:先删除缓存,再更新数据库

这也不太行假设先删除redis的数据,而mysql还没更新完成

这个时候有一个线程来读取缓存的数据没找到,读取mysql就可能导致了脏读问题,

然后将对应的脏数据回写进了redis,此时mysql更新完了发现缓存中已经有数据了

这里就引入一种延时双删的策略

我们非常悲观的以为一定会有这么一个线程读取脏数据

所以我们在mysql更新结束之后我们对redis在进行一次删除的操作

但是这里延迟的时间不一定好确定,一般是写数据在业务耗时加上100ms即可

还有就是使用后台监控的策略(咱们后面再说)

策略5:先更新数据库再删除缓存

最后一个策略就是较为折中的策略,我们选择先更新数据库再删除缓存

这里的缺点是假设a线程没有更新完mysql并且删除缓存之前就有另外的线程读取对应的数据

这里可能就导致读到了缓存里面的旧值

这里也是有一些成熟的解决方案的

下面我们介绍一下流程

比如使用阿里的canal

其实也就是在更新完数据库之后,写入mysql的binlog日志文件中

订阅程序或者是消息中间价提取出对应的key

然后另起一段非业务代码来获取这里的信息

尝试删除缓存,删除失败的话就将这里的数据发送给消息队列

然后重新重消息队列中获取数据重新复写缓存

流程图如下

我们其实就是做不到强一致性,所以我们之只能采取最终一致性的方案

这也就导致了充值话费或者是短信有一定的滞后性

小总结

我们大多数情况下都是先更新数据库,再删除缓存

这是因为先删除缓存能保证每次获取数据的时候是直接访问数据库,可能导致数据库负载过高

其次就是即时使用延时双删的操作,这里可能延时的时间也不好计算等等

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

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

相关文章

flink源码系列:RPC通信

这里写目录标题 1. 本节课目的2.开始本节内容2.1.RPC概念3.2.大数据组件常见的RPC实现技术3.3.Pekko(Akka)3.3.1. Akka、Pekko基本概念3.3.2.Pekko Demo事例3.3.2.1.PekkoData 类3.3.2.2.PekkoRpcReceiverActor类3.3.2.3.PekkoRpcSenderActor 类3.3.2.4.…

传感器展会|2024厦门传感器与应用技术展览会

传感器展会|2024厦门传感器与应用技术展览会 时间:2024年11月1-3日 地点:厦门国际会展中心 XISE EXPO展会介绍: 2024中国(厦门)国际传感器与应用技术展览会将于2024年11月1-3日在厦门国际会展中心举行&#xf…

基于标定数据将3D LiDAR点云与相机图像对齐(含C++版本代码)

这段C代码演示了如何将Velodyne激光雷达的点云数据投影到相机图像上。该过程涉及以下主要步骤: 读取并解析来自文件的标定数据,包括P2矩阵、R0_rect矩阵和Tr_velo_to_cam矩阵。这些矩阵用于将激光雷达点云从Velodyne坐标系转换到相机坐标系。从二进制文件中读取Velo…

10.1 Go Goroutine

💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

解密Spring Boot:深入理解条件装配与条件注解

文章目录 一、条件装配概述1.1 条件装配的基本原理1.2 条件装配的作用 二、常用注解2.1 ConditionalOnClass2.2 ConditionalOnBean2.3 ConditionalOnProperty2.4 ConditionalOnExpression2.5 ConditionalOnMissingBean 三、条件装配的实现原理四、实际案例 一、条件装配概述 1…

SpringBoot中实现一个通用Excel导出功能

SpringBoot中实现一个通用Excel导出功能 文章目录 SpringBoot中实现一个通用Excel导出功能这个导出功能的特色看效果代码解析1、依赖2、Excel 入参(ExcelExportRequest)3、Excel 出参(ExcelExportResponse)4、ExcelExportField5、ExcelExportUtils 工具类6、ExcelHead 头部…

LeetCode1318或运算的最小翻转次数

题目描述 给你三个正整数 a、b 和 c。你可以对 a 和 b 的二进制表示进行位翻转操作,返回能够使按位或运算 a OR b c 成立的最小翻转次数。「位翻转操作」是指将一个数的二进制表示任何单个位上的 1 变成 0 或者 0 变成 1 。 解析 这一题就按位依次比较就行了。取这…

【SQLAlChemy】Query函数可传入的参数有哪些?

Query 函数的使用 参数种类 一般可以传递的参数有如下三种: 模型名。指定查找这个模型的全部属性(对应于数据库查询中的全表查询)。模型中的属性。可以用来指定只查询某个模型的几个属性值。使用聚合函数。 func.count():统计行的数量。fu…

【机器学习】GPT-4中的机器学习如何塑造人类与AI的新对话

🚀时空传送门 🔍引言📕GPT-4概述🌹机器学习在GPT-4中的应用🚆文本生成与摘要🎈文献综述与知识图谱构建🚲情感分析与文本分类🚀搜索引擎优化💴智能客服与虚拟助手&#x1…

打造智慧校园信息系统,提升学校科技实力

在如今数字化的时代,打造智慧校园信息系统已成为提升学校科技实力的关键。随着科技的迅猛发展,学校需要跟上时代步伐,利用先进技术建设一个高效、智能的信息系统,为学生、教师和管理人员提供更好的学习和工作环境。 智慧校园信息系…

企业官网:过时了,但又没完全过时

作为一名互联网冲浪级选手,我经常会看到一些有趣的产品。 这两年比较让我感兴趣的产品有「飞聊」、「即刻」及其旗下的「橙 App」等等,然后我就想上它们的官网看看。 虽然现在 app 是主流,但我非常不喜欢下载 app,一是麻烦&…

数据结构与算法笔记:基础篇 - 二叉树基础(下):有了如此高效的散列表,为什么还需要二叉树?

概述 上篇文章,我们学习了树、二叉树及二叉树的遍历,本章来学习一种特殊的二叉树,二叉查找树。二叉查找树最大的特点就是,支持动态数据集合的快速插入、删除、查找操作。 之前说过,散列表也是支持这些操作的&#xf…

RAG:如何从0到1搭建一个RAG应用

通过本文你可以了解到: 什么是RAG?如何搭建一个RAG应用?目前开源的RAG应用有哪些? 大模型学习参考: 1.大模型学习资料整理:大模型学习资料整理:如何从0到1学习大模型,搭建个人或企业…

67. UE5 RPG 创建法师敌人角色

我们设置的敌人类型分三种,分别时战士类型,远程射手,和法师类型。在前面,我们创建了战士和射手类型的,还没有法师类型的,在这一篇文章中,我们创建一个法师类型的角色。 在64. UE5 RPG 创建新的双…

矩阵杯2024 Re wp 前两题

1. packpy UPX壳,但不能直接脱,应该是修改了头文件(l_info) 改一下就能脱了 脱完是个elf文件 进IDA看一眼 明显的Py打包标志,用pyinstxtractor解包出来(最好用对应的python3.8) 可以得到packpy.pyc文件&a…

ROS学习记录:C++节点发布自定义地图

前言 ROS栅格地图格式 在了解了ROS地图消息包的数据结构后(链接在上),本文将编写一个节点,发布地图消息包,看看在RViz中显示是什么效果。 一、准备 1、为了简单起见,发布一个两行四列的地图 2、为了便于观测,只对地…

textattack报错:不能导入自定义search_methods (cannot import name ‘xxx‘ from ‘xxx‘)

1. 报错信息 ImportError: cannot import name AAA from textattack.search_methods (/home/666/anaconda3/envs/textattack37_env/lib/python3.7/site-packages/textattack/search_methods/__init__.py)2. 出错简述 贴一段test1.py的模块导入 #建议使用! import…

时钟影响ADC性能不仅仅是抖动

时钟影响ADC性能除了抖动,还有占空比。 在高速AD采样中,时钟占空比是非常重要的一个参数。时钟信号的上升沿控制ADC的采样,而下降沿控制着信号的保持,在一个周期内才可以完成量化输出,所以必须保持时钟的占空比为50%&…

Typora Markdown编辑器 for Mac v1.8.10 安装

Mac分享吧 文章目录 效果一、准备工作二、开始安装1、双击运行软件,将其从左侧拖入右侧文件夹中,等待安装完毕2. 应用程序显示软件图标,表示安装成功 三、运行调试1、修改主题2、显示文档列表,如下图3、查看版本信息 **安装完成&…

AI智能体做高考志愿填报分析

关注公众号,赠送AI/Python/Linux资料,对AI智能体有兴趣的朋友也可以添加一起交流 高考正在进行时,学生焦虑考试,家长们焦虑的则是高考志愿怎么填。毕竟一个好的学校,好的专业是进入社会的第一个敲门砖 你看张雪峰老师…