Redis学习——高级篇③

news2024/11/18 5:41:41

Redis学习——高级篇③

    • = = = = = = Redis7高级之缓存双写一致性之更新策略探讨(三)= = = = = =
    • 1.缓存双写一致性
    • 2.数据库和缓存一致性的几种更新策略
      • 2.1 可停机的情况
      • 2.2 不可停机的情况,四种更新策略(推荐最后一种,看场景)
        • 1.❌先更新数据库,再更新缓存
        • 2.❌先更新缓存,再更新数据库
        • 3.❌先删除缓存,再更新数据库
        • 4.⚠️先更新数据库,再删除缓存
    • 3. 总结

在这里插入图片描述

在这里插入图片描述

= = = = = = Redis7高级之缓存双写一致性之更新策略探讨(三)= = = = = =

在这里插入图片描述

还是先放面试题,当一整遍看完,再回头看看 这个面试题题,自己会不会。

在这里插入图片描述

1.缓存双写一致性

  • 如果redis中有数据

    • 需要和数据库中的值相同
  • 如果redis中无数据

    • 数据库中的值是最新值,且准备回写redis
  • 缓存按照操作分

    • 只读缓存
    • 读写缓存
      • 同步直写策略
        • 写数据库后也同步写 redis 缓存,缓存中的数据和数据中的一致
        • 对于读写缓存来说,要想保证缓存和数据库中的数据一致
      • 异步缓写策略
        • 正常业务运行中,mysql数据变动了,但是可以在业务上容许出现一定时间后才作用于redis,比如仓库、物流系统
        • 异常情况出现了,不得不讲失败的动作重新修补,有可能需要借助kafka或者RabbitMQ等消息中间件,实现重写重试
  • 在这里插入图片描述

    • 采用双检加锁策略

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

@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;
    }
}

在这里插入图片描述

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

目的

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

上述方案和后续落地案例是调研后的主流+成熟的做法,但是考虑到各个公司业务系统的差距,不是100%绝对正确,不保证绝对适配全部情况,请同学们自行酌情选择打法,合适自己的最好。

2.1 可停机的情况

基本上怎么处理都可以

  • 挂牌报错
  • 凌晨升级
  • 服务降级
  • 温馨提示
  • 最好单线程操作(对于重量级的数据操作)

2.2 不可停机的情况,四种更新策略(推荐最后一种,看场景)

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

最终结果,mysq|和redis数据不一致,/(ㄒ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
  2. B update redis 80
  3. B update mysql 80
  4. A update mysql 100

最终结果,mysq|和redis数据不一致,/(ㄒoㄒ)/~~

mysql80,redis100

在这里插入图片描述

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

异常问题

在这里插入图片描述

在这里插入图片描述
3

  • A线程更新完mysq|,发现redis 里面的缓存是脏数据,A线程直接懵逼了
  • 两个并发操作,一个是更新操作,另一个是查询操作,
  • A删除缓存后,B查询操作没有命中缓存,B先把老数据读出来后放到缓存中,然后A更新操作更新了数据库。
  • 于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。

4总结流程:

  1. 请求A进行写操作,删除redis缓存后,工作正在进行中,更新mys…A还么有彻底更新完mysql,还没commit
  2. 请求B开工查询,查询redis发现缓存不存在(被A从redis中删除了)
  3. 请求B继续,去数据库查询得到了mysq|中的旧值(A还没有更新完)
  4. 请求B将旧值写回redis缓存

在这里插入图片描述
在这里插入图片描述

解决方案(延时双删策略)

注意关键点,我更新完数据库的时间 + sleep的时间 大于 读取数据并写入换的时间 即可(多个100ms即可)
在这里插入图片描述

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

关于延时双删的细节问题

  • 这个线程休眠时间(线程A sleep的时间,就需要大于线程B读取数据再写入缓存的时间)

    • 第一种 :在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,自行评估自己的项目的读数据业务逻辑的耗时,以此为基础来进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。
    • 第二种: 新启动一个后台监控程序,比如WatchDog监控程序,会加时
      这种同步淘汰策略,吞吐量降低怎么办
  • 第二次删除缓存使用 异步删除
    在这里插入图片描述

在这里插入图片描述

4.⚠️先更新数据库,再删除缓存

异常问题

在这里插入图片描述

订阅binlog程序再mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能(下一章具体实现)

解决方法

在这里插入图片描述

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

分布式的事务问题一定要遵守最终一致性,可以允许短暂的信息滞后

多补充一句:如果使用先更新数据库,再删除缓存的方案

如果业务层要求必须读取一致性的数据,那么我们就需要在更新数据库时,先在Redis缓存客户端暂停并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性,这是理论可以达到的效果,但实际,不推荐,因为真实生产环境中,分布式下很难做到实时一致性,一般都是最终一致性,请大家参考。

3. 总结

策略高并发多线程条件下问题现象解决方案
先删除redis缓存,再更新mysql缓存删除成功但数据库更新失败Java程序 从数据库中读到旧值再次更新数据库,重试
先删除redis缓存,再更新mysql缓存删除成功但数据库更新中。。。有并发读请求并发请求从数据库读到旧值并回写到redis,导致后续都是从redis读取到旧值延迟双删
先更新mysql,再删除redis缓存数据库更新成功,但缓存删除失败Java程序从redis中读到旧值再次删除缓存,重试
先更新mysql,再删除redis缓存数据库更新成功但缓存删除中。。。有并发读请求并发请求从缓存读到旧值等待redis删除完成,这段时间有数据不一致,短暂存在

在大多数业务场景下,阳哥建议是 优先使用先更新数据库,再删除缓存的方案(先更库,后删存)。理由如下:

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

  • 有可能导致请求因缓存缺失而访问数据库,给数据库带来压力导致打满mysql
  • 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置

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

  • 如果业务层要求必须读取一致性的数据,那么我们就需要在更新数据库时,先在Redis缓存客户端暂停并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性,这是理论可以达到的效果,但实际,不推荐,因为真实生产环境中,分布式下很难做到实时一致性,一般都是最终一致性。

在这里插入图片描述

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

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

相关文章

【Vue实用功能】Vue实现文档在线预览功能,在线预览PDF、Word等office文件

1、Office Web(微软的开发接口) 优点 没有 Office也可以直接查看Office 文件适用于移动端、PC无需下载文件就可以在浏览器中查看 <iframe src"文档地址" frameborder"0" /> const docUrl 外网可预览的地址 const url encodeURIComponent(docUrl…

基于SpringBoot+Redis的前后端分离外卖项目-苍穹外卖微信小程序端(十五)

用户端历史订单模块 1. 查询历史订单1.1 需求分析和设计1.2 代码实现1.2.1 user/OrderController1.2.2 OrderService1.2.3 OrderServiceImpl1.2.4 OrderMapper1.2.5 OrderMapper.xml1.2.6 OrderDetailMapper 2. 查询订单详情2.1 需求分析和设计2.2 代码实现2.2.1 user/OrderCon…

K8s 安装部署-Master和Minion(Node)

K8s 安装部署-Master和Minion(Node) 操作系统版本&#xff1a;CentOS 7.4 Master &#xff1a;172.20.26.167 Minion-1&#xff1a;172.20.26.198 Minion-2&#xff1a;172.20.26.210&#xff08;后增加节点&#xff09; ETCD&#xff1a;172.20.27.218 先安装部署ETCD y…

Linux实验记录:添加硬盘设备

前言&#xff1a; 本文是一篇关于Linux系统初学者的实验记录。 参考书籍&#xff1a;《Linux就该这么学》 实验环境&#xff1a; VmwareWorkStation 17——虚拟机软件 RedHatEnterpriseLinux[RHEL]8——红帽操作系统 目录 前言&#xff1a; 备注&#xff1a; 添加硬盘…

Java: Low Poly Islands

一大批岛屿资产供您在下一Unity项目中使用!包括热带岛屿、火山岛、热带山脉、植被、乡村房屋、木板路、船只、粒子、后期FX等。 适用于原型设计、移动、LOD或风格化游戏。 模块化部分很容易在Unity网格上组装在一起。 141种独特的预制件,包括: - 38个具有LOD的岛屿模型 - 30…

云上高可用系统-韧性设计模式

一、走近韧性设计模式 &#xff08;一&#xff09;基本概念 韧性设计模式是一系列在软件工程中用于提高系统韧性的设计原则、策略、实践和模式。韧性&#xff08;Resilience&#xff09;在这里指的是系统对于各种故障、异常和压力的抵抗能力&#xff0c;以及在遭受这些挑战后…

vue实现获取系统当前年月日时分秒星期

(壹)博主介绍 &#x1f320;个人博客&#xff1a; 尔滨三皮⌛程序寄语&#xff1a;木秀于林&#xff0c;风必摧之&#xff1b;行高于人&#xff0c;众必非之。 (贰)文章内容 <!-- 获取系统当前时间 --> <template><div class"currentTimeBox"><…

增存量市场大爆发!国产通信中间件「反攻」

梳理2023年智能驾驶的发展脉络可见&#xff0c;消费者对智能驾驶的认可度和接受度越来越高&#xff0c;带动高速NOA迈向了规模化普及新阶段&#xff0c;城市NOA初露锋芒。 从更长远的行业变革周期来看&#xff0c;智能驾驶的技术迭代还在继续&#xff0c;叠加电子电气架构的深…

【hcie-cloud】【23】容器编排【k8s】【Kubernetes常用工作负载、Kubernetes调度器简介、Helm简介、缩略词】【下】

文章目录 单机容器面临的问题、Kubernetes介绍与安装、Kubernetes对象的基本操作、Kubernetes YAML文件编写基础Kubernetes常用工作负载Kubernetes常用工作负载简介创建一个无状态nginx集群无状态工作负载Deployment说明无状态工作负载Deployment常见操作创建一个有状态的MySQL…

Notepad 将多行转换成字符串,合并成一行

notepad 将多行转换成字符串&#xff0c;合并成一行 (1) 快捷键 ctrl H &#xff0c;选择 【替换】&#xff0c; (2) 【查找目标】&#xff0c;输入 \r\n &#xff0c; 这个正则表达式的含义是 换行回到行首&#xff0c;相当于 windows的 enter 键&#xff1a; \r&#xff…

Redis 学习笔记 2:Java 客户端

Redis 学习笔记 2&#xff1a;Java 客户端 常见的 Redis Java 客户端有三种&#xff1a; Jedis&#xff0c;优点是API 风格与 Redis 命令命名保持一致&#xff0c;容易上手&#xff0c;缺点是连接实例是线程不安全的&#xff0c;多线程场景需要用线程池来管理连接。Redisson&…

Hinton、LeCun、Bengio、清华马维英等人当选2023 ACM Fellow!

大家好我是二狗。 美国当地时间1月24日&#xff0c;美国计算机学会&#xff08;ACM&#xff09;正式宣布了2023年 ACM Fellow的名单&#xff0c;今年一共有68名科学家入选。 其中包括万维网的发明人、2016年度图灵奖得主蒂姆伯纳斯李&#xff08;Tim Berners-Lee &#xff09…

Go语言基础之单元测试

1.go test工具 Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的&#xff0c;并不需要学习新的语法、规则或工具。 go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内&#xff0c;所有以_test.go为后缀名的源代码文件都是go …

使用CyberRT写第一个代码

0. 简介 计算框架是自动驾驶系统中的重中之重&#xff0c;也是整个系统得以高效稳定运行的基础。为了实时地完成感知、决策和执行&#xff0c;系统需要一系列的模块相互紧密配合&#xff0c;高效地执行任务流。由于各种原因&#xff0c;这些模块可能位于不同进程&#xff0c;也…

ArcGIS雨涝风险模拟

所谓雨涝模拟分析&#xff0c; 就是模拟降雨量达到一定强度&#xff0c; 城市的哪些区域容易被淹没形成内涝。 雨涝模拟更重要的是提前预测&#xff0c; 可在预测结果的基础上进行实地勘察&#xff0c; 为项目规划、风险防控等工作提供指导作用。 雨涝模拟的原理和思想多种…

MyBatis概述与MyBatis入门程序

MyBatis概述与MyBatis入门程序 一、MyBatis概述二、入门程序1.准备开发环境&#xff08;1&#xff09;准备数据库&#xff08;2&#xff09;创建一个maven项目 2.编写代码&#xff08;1&#xff09;打包方式和引入依赖&#xff08;2&#xff09;新建mybatis-config.xml配置⽂件…

基于springboo校园社团信息管理系统

摘要 随着高校规模的扩大和学生社团活动的日益丰富多彩&#xff0c;校园社团信息管理成为一个备受关注的问题。为了更有效地组织、管理和推动校园社团的发展&#xff0c;本文设计并实现了一套基于Spring Boot的校园社团信息管理系统。本系统以实现社团信息的集中管理和高效运营…

Pytest 识别case规则

一、Python测试框架&#xff0c;主要特点有以下几点&#xff1a; 简单灵活&#xff0c;容易上手&#xff1b;支持参数化&#xff1b;能够支持简单的单元测试和复杂的功能测试&#xff0c;还可以用来做selenium/appnium等自动化测试、接口自动化测试&#xff08;pytestrequests…

uniapp将方法挂载到全局

前言 首先需要有一个自己封装的方法,话不多说,直接上代码! 方法文件(common.js) const getnav (page, type, param token) > {// type 判断是否 需要验证登录if (!page) return uni.showModal({title: 提示,content: 功能暂未开通~,showCancel: false})let user uni.g…

大模型视觉理解能力更进一步,谷歌提出全新像素级对齐模型PixelLLM

论文题目&#xff1a;Pixel Aligned Language Models 论文链接&#xff1a;https://arxiv.org/abs/2312.09237 项目主页&#xff1a;Pixel Aligned Language Models 近一段时间以来&#xff0c;大型语言模型&#xff08;LLM&#xff09;在计算机视觉领域中也取得了巨大的成功&a…