【Redis】Redis缓存双写一致性之更新策略

news2024/12/27 13:56:48

介绍

在这里插入图片描述

面试题

1、只要用到缓存,就可能会涉及到Redis缓存与数据库双存储双写,只要是双写,就一定会有数据一致性问题,怎么解决一致性问题?

2、双写一致性,先动缓存redis还是数据库mysql?为什么?

3、延时双删?有哪些问题?

4、有一种情况,微服务查询redis无mysql有,为保证数据双写一致性回写redis需要注意什么?

5、双检加锁策略?如何避免缓存击穿?

6、redis和mysql双写一定会出现纰漏,做不到强一致性,如何保证最终一致性?

双写一致性

Redis中有数据,需要和数据库中的值相同。

Redis中无数据,数据库中的值是最新的值,并且准备回写Redis。

缓存按照操作划分

  • 只读缓存

  • 读写缓存

    • 同步直写策略

      • 写数据库后同步写Redis缓存,缓存中的数据和数据库中的一致
      • 对于读写缓存,要保证缓存和数据库中数据的一致
    • 异步缓写策略

      • 正常业务运行,MySQL数据变动,但是可以在业务上容许出现一定时间后才作用于Redis,比如仓库等。
      • 异常情况出现后,不得不将失败的动作修补,可能需要借助kafka等消息中间件,实现重写重试。

采用双检加锁策略

  • 多个线程同时去查询数据库的这条数据,就在第一个查询数据的请求上使用一个互斥锁来锁住他。
  • 其他线程获取不到锁就一直等待,等第一个线程查询到了数据,然后做了缓存
  • 后面的线程进来发现已经有了缓存,就直接走缓存

Java示例

package com.lv.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lv.User;
import com.lv.mapper.UserMapper;
import com.lv.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * @author 晓风残月Lx
 * @date 2023/3/27 12:39
 */
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    public static final String CACHE_KEY_USER = "user:";
    @Resource
    private UserMapper userMapper;
    @Resource
    private RedisTemplate redisTemplate;


    /**
     *  业务逻辑没有写错,对于小厂中厂(QPS《=1000)可以使用,但是大厂不行
     * @param id
     * @return
     */
    public User findUserById1(Long 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.selectById(id);
             if (user == null){
                 // 3.1 redis + mysql 都无数据
                 // 具体细化,防止多次穿透,业务规定,记录下导致穿透的这个key回写redis
                 return user;
             }else {
                 // 3.2 mysql有,需要回写到redis,保证下一次的缓存命中率
                 redisTemplate.opsForValue().set(key,user);
             }
        }
        return user;
    }

    /**
     * 加强补充,避免突然key失效了,打爆mysql,做一下预防,尽量不出现击穿的情况
     * @param id
     * @return
     */
    public User findUserById2(Long id){
        User user = null;
        String key = CACHE_KEY_USER + id;

        // 1.先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql
        // 第一次查询redis,加锁前
        user = (User) redisTemplate.opsForValue().get(key);
        if (user == null){
            // 2.对于高QPS的优化,进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysql
            synchronized (UserServiceImpl.class){
                // 第二次查询redis,加锁后
                user = (User) redisTemplate.opsForValue().get(key);
                // 3. 二次查redis还是null,可以去查mysql了(mysql默认有数据)
                if (user == null) {
                    //4 查询mysql拿数据(mysql默认有数据)
                    user = userMapper.selectById(id);
                    if (user == null) {
                        return null;
                    } else {
                        // 5. mysql里面有数据的,需要回写redis,完成数据一致性的同步工作
                        redisTemplate.opsForValue().setIfAbsent(key, user, 7L, TimeUnit.DAYS);
                    }
                }
            }
        }
        return user;
    }
}

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

目的

达到最终一致性。

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

不可停机的四种更新策略

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

问题一

更新MySQL的某个商品的库存,当前商品的库存是100,更新为99 个。下一步先更新MySQL修改为99成功,然后更新Redis。这时出现异常,更新Redis失败,导致MySQL中的库存是99,而Redis中的还是100。这样会让数据库和缓存Redis中的数据不一致,读到Redis脏数据

问题二

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
2、B update mysql 80
3、B update redis 80
4、A update redis 100

最终,MySQL和Redis中的数据不一致

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

一般业务会将MySQL作为底单数据库,以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
2、B update redis 80
3、B update mysql 80
4、A update mysql 100

最终,MySQL和Redis中的数据不一致。

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

异常问题

A线程先成功删除了Redis中的数据,然后去更新MySQL,此时MySQL正在更新,还没结束。B突然出现要读取缓存数据。

在这里插入图片描述

此时Redis里面的数据是空的,B线程读取,先读取Redis里的数据(此时已经被A线程delete掉了),这里会出现两个问题:

B从MySQL获取了旧值,B线程发现了Redis中没有数据(缓存缺失),会马上去MySQL中读取,这时数据库还没更新完成,从数据库中读取的是旧值。

B会将获取到的旧值写回到Redis。B获取旧值数据后,返回前台并写回进Redis(刚被A线程删除的旧数据极大可能又被写回。)

在这里插入图片描述

之后,A线程更新完MySQL,发现Redis里面的缓存是脏数据,这时A线程就不好处理了。

这里有两个并发操作,一个更新操作,一个查询操作。

A删除缓存后,B查询操作没有命中缓存,B先把老数据读出来后,放到缓存中,然后A更新操作更新了数据库。

因此,在Redis缓存中的数据还是老数据,导致Redis缓存中的数据是脏的,而且会一直是脏数据。

时间线程A线程B出现问题
t1请求A进行写操作,删除缓存成功后,正在进行MySQL更新操作……
t21、缓存中读取不到,立刻读取MySQL,因为A还没有更新完MySQL,因此读到的是旧值
2、将从MySQL读取的旧值,写回了Redis
1、A没有更新完MySQL导致B读到了旧值
2、线程B遵守回写机制,将旧值写回了Redis,导致其他请求从缓存中读取的是旧值,并没有更新
t3A更新完MySQL数据库,完成Redis缓存是被B写回的旧值
MySQL中是被A更新的新值
出现了数据不一致的问题

总结

在该策略下,如果数据库更新失败或超时或返回不及时,导致B线程请求访问缓存时,发现Redis缓存中没有数据,缓存缺失,B会去MySQL数据库中进行读取,取到旧值,写回Redis缓存,导致数据不一致。

解决方案

延时双删策略

示例代码(Java):

在这里插入图片描述

上述代码中,加上sleep,为了让线程B能先从数据库中读取数据,将缺失的数据写入缓存,然后A线程进行删除,因此,线程A Sleep的时间要大于线程B读取数据写入缓存的时间

这个方案会在第一次删除缓存后,延迟一段时间后再次进行删除——延迟双删。

相关面试题

1、这个删除该休眠多久?

  • 在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,自行评估自己的项目的读数据业务逻辑的耗时,以此为基础来进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。
  • 新启动一个后台监控程序,比如WatchDog监控程序,会加时

2、这种同步淘汰策略,吞吐量降低怎么办?

第二次删除缓存使用异步删除

在这里插入图片描述

3、看门狗WatchDog源码分析

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

异常问题

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

解决方案(消息队列)

在这里插入图片描述

在这里插入图片描述

总结

在这里插入图片描述

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

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

相关文章

剪枝与重参第十课:RepVGG重参

目录 RepVGG重参前言1. RepVGG2. RepVGG网络搭建2.1 conv_bn2.2 RepVGG Block初始化2.3 forward2.4 branch的合并2.5 重参的实现2.6 整体网络结构搭建2.7 模型导出 3. 完整示例代码总结 RepVGG重参 前言 手写AI推出的全新模型剪枝与重参课程。记录下个人学习笔记&#xff0c;仅…

了解npm run指令

了解npm run指令 在package.json文件中的script字段&#xff0c;可以定义脚本命令&#xff0c;通过npm run指令运行该脚本。 比如简单定义一个输出打印的shell脚本。 {"script": {"hw": "echo hello world!"} }执行npm run hw可以看到终端上打…

Python---正则表达式与递归

1. 正则表达式&#xff1a; 是一种字符串验证的规则&#xff0c;通过特殊的字符串组合来确立规则 用规则去匹配字符串是否满足 如(^[\w-](\.[\w-])*[\w-](\.[\w-])$)可以表示为一个标准邮箱的格式 re模块的三个主要方法&#xff1a; re.match&#xff1a; re.match(匹配规…

电子电气架构——车辆E/E架构Software独立性

我是穿拖鞋的汉子,魔都中坚持长期主义的工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 人只有在举棋不定,无从把握的时候才感到疲惫。只有去行动就能获得解放,哪怕做的不好也比无所作为强! 本文主要介绍车辆E/E架构常识,主要涉及E/E架构车载…

Java 操作ElasticSearch

Java REST提供了两种风格的客户端连接工具&#xff0c;Java High Level REST Client、Java Low Level REST Client&#xff0c;这里我就不去细说Java Low Level REST Client了&#xff0c;因为这我确实没用到过&#xff0c;也不是很了解&#xff0c;我说一下Java High Level RE…

LVS 负载均衡群集的 NAT 模式和 DR 模式

文章目录 一、NAT 模式和 DR 模式的介绍DR模式NAT模式两种模式的区别 二、DR模式集群构建配置 一、NAT 模式和 DR 模式的介绍 DR模式 当用户向负载均衡调度器&#xff08;Director Server&#xff09;发起请求&#xff0c;调度器将请求发往至内核空间PREROUTING链首先会接收到…

【JavaEE】HTML基础知识

目录 1.HTML结构 2.HTML常见标签 3.表格标签 4.列表标签 5.表单标签 ​6.select 标签 7.textarea 标签 8.无语义标签: div & span 9.标签小练习 1.HTML结构 形如&#xff1a; <body idmyId>hello</body> HTML的书写格式 标签名 (body) 放到 <…

【操作系统OS】学习笔记:第二章 进程与线程 (上)【哈工大李治军老师】

基于本人观看学习 哈工大李治军老师主讲的操作系统课程 所做的笔记&#xff0c;仅进行交流分享 特此鸣谢李治军老师&#xff0c;操作系统的神作&#xff01; 如果本篇笔记帮助到了你&#xff0c;还请点赞 关注 支持一下 ♡>&#x16966;<)!! 主页专栏有更多&#xff0c;…

58.网页设计规则#5_阴影

一些概念 ● 在一个100%平面设计的时代之后&#xff0c;我们现在又回到了在UI设计中使用阴影(“平面设计2.0”) ● 阴影箱深度(3D):阴影越多&#xff0c;离元素界面越远 利用好阴影 你不必使用阴影!只在对网站个性有意义的情况下使用它们 使用少量的阴影:不要给每个元素…

Jmeter接口自动化测试系列之Http接口自动化实战

以下主要介绍Jmeter接口自动化需要哪些控件、接口自动化实战及总结。 前面的系列文章&#xff0c;介绍了常用组件、参数化、接口依赖、断言等知识点&#xff0c;今天我们要将这些结合起来&#xff0c;进行综合实战。 2023年B站最新Jmeter接口测试实战教程&#xff0c;精通接口…

从一到无穷大 #5 公有云时序数据库定价

文章目录 引言serverless实例售卖结论 Azure CosmosDB预配吞吐量自动缩放吞吐量Serverless预留容量存储量 Amazon Timestream写入计费查询计费存储 阿里云TSDB阿里云Lindom时序引擎实例固定费用存储费用节点费用 华为云GaussDB(for Influx)腾讯云CTSDBTDengineInfluxDB CloudAW…

c++11下篇 + 智能指针

c11下篇 智能指针 1 可变参数模板1.1 递归函数方式展开参数包1.2 逗号表达式展开参数包1.3 STL容器中的empalce相关接口函数&#xff1a; 2 lambda达式2.1 c的痛2.2 lambda表达式语法2.3 函数对象与lambda表达式 3 包装器3.1 bind 4 线程库4.1 thread类的简单介绍4.2 面试题&a…

MySQL示例数据库(MySQL Sample Databases) 之 world_x数据库

文章目录 MySQL示例数据库(MySQL Sample Databases) 之 world_x数据库官方示例数据介绍world_x数据库world_x数据库安装world-db/world.sql的脚本内容参考 MySQL示例数据库(MySQL Sample Databases) 之 world_x数据库 官方示例数据介绍 MySQL 官方提供了多个示例数据库&#…

差分数组 技巧小记

差分数组 差分数组二维差分 差分数组 如果两个信息“长得很像”&#xff0c;只要保留一个&#xff0c;对另一个&#xff0c;只要保留它们的差异&#xff0c;然后进行微调就行了。 差分数组&#xff1a; 3210&#xff0c;3208&#xff0c;3206&#xff0c;3211&#xff0c;32…

Three.js--》Gui.js库的使用讲解

目录 Gui.js库基本使用 使用three自带gui库实现基本操作 gui库实现下拉菜单和单选框 gui库分组方法实现 使用dat.gui第三方库 Gui.js库基本使用 gui.js说白了就是一个前端js库&#xff0c;对HTML、CSS和JavaScript进行了封装&#xff0c;学习开发3d技术时借助该库可以快速…

230502-LLM-Vicuna介绍、安装与注意事项整理

最终效果 在对话过程中&#xff0c;GPU与CPU均会有波动&#xff0c;但是主要还是CPU波动为主 相关资料 序号链接说明001本地CPU6G内存部署类ChatGPT模型&#xff08;Vicuna 小羊驼&#xff09; - 知乎极简安装版本&#xff0c;只支持CPU与命令行002最新开源语言模型 Vicuna 媲…

【Linux进阶之路】初始Linux

文章目录 一.时代背景二.硅谷发展模式三.操作系统基本定义常见的操作系统Linux系统的常见安装方式 四.基本指令的使用登录指令与用户相关的指令ls 指令——信息查看pwd指令——打印当前所处的文件位置cd指令——访问文件rm——删除指令touch——创建文件与修改文件信息tree ——…

Rust - 变量与数据的交互方式(clone)

在上一篇文章中我们介绍了变量与数据的交互方式-move&#xff0c;通过底层原理我们知道Rust 永远也不会自动创建数据的 “深拷贝”。因此&#xff0c;任何 自动的复制可以被认为对运行时性能影响较小。 但是如果我们 确实需要深度复制 String中堆上的数据&#xff0c;而不仅仅…

RT1010 PWM 组成配置和 PWMX 的使用

1. 前言 本篇博文将着眼于 i.MX RT1010 内部的 eFlexPWM&#xff0c;介绍其各个功能模块&#xff0c;以及 PWM 产生的原理。 2. 功能模块组成 以下是 RT1010 内部 PWM 的一个 Submoudle 的组成框图&#xff0c;从框图中我们可以看到&#xff1a; 自左向右依次有 Prescaler 对…

【C++入门】C++为什么要有缺省参数

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前学习C和算法 ✈️专栏&#xff1a;C航路 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你有帮助的话 欢迎 评论&#x1f4ac; 点赞&#x1…