黑马点评-07缓存击穿问题(热点key失效)及解决方案,互斥锁和设置逻辑过期时间

news2024/11/16 10:27:16

缓存击穿问题(热点key失效)

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且重建缓存业务较复杂的key突然失效了,此时无数的请求访问会在瞬间打到数据库,带来巨大的冲击

  • 一件秒杀中的商品的key突然失效了,由于大家都在疯狂抢购那么这个瞬间就会有无数的请求访问去直接抵达数据库,从而造成缓存击穿

在这里插入图片描述

互斥锁

如果缓存中没有缓存对应的店铺信息时,所有的线程过来后需要先获取锁才能查询数据库中的店铺信息,保证只有一个线程访问数据库,避免数据库访问压力过大

  • 优点: 实现简单且没有额外内存销毁(加一把锁), 当拿到线程锁的线程把缓存数据重建好后,其他线程再访问时从缓存中查询的数据和数据库中的数据就是一致的
  • 缺点: 当拿到线程锁的线程在操作数据库的时候,其他线程只能等待,将查询的性能从并行变成了串行(tryLock方法+double check可以解决),但是还有死锁的风险

在这里插入图片描述

setnx实现互斥锁

根据店铺Id查询商铺信息,增加了获取互斥锁的环节,即缓存未命中时只有获取锁成功的线程才能查询数据库,保证只有一个线程去数据库执行查询语句,防止缓存击穿

在这里插入图片描述

利用redis提供的setnx key(锁Id) value命令判断是否有线程成功插入key(锁), del key表示释放锁

返回值描述
0表示线程插入key失败,即线程获取锁失败
1表示线程插入key成功即线程获取锁成功

StringRedisTemplate中对应setnx指令的方法是setIfAbsent(),返回true表示插入成功,fasle表示插入失败

// 每一个店铺都有自己的锁,根据锁的Id(锁前缀+店铺ID)尝试获取锁(本质是插入key)
private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    // 我们这里使用了BooleanUtil工具类将Boolean类型的变量转化为boolean,避免在拆箱过程中返回null
    return BooleanUtil.isTrue(flag);
}

// 释放锁(本质是删除key)
private void unlock(String key) {
    stringRedisTemplate.delete(key);
}

单独实现负责解决缓存击穿问题的方法queryWithMutex,在该方法中如果查到店铺信息返回shop查不到则返回null,最后在queryById中做统一判断返回结果类

  • 获取锁成功,应该再次检测redis缓存是否存在,因为此时可能其他线程重建完缓存刚释放完锁后,做双重检查,如果存在则无需重建缓存

在这里插入图片描述

@Override
public Result queryById(Long id) {    
    // 使用互斥锁解决缓存击穿
    Shop shop = queryWithMutex(id);
    // 如果shop等于null,表示数据库中对应店铺不存在或者缓存的店铺信息是空字符串
    if (shop == null) {
        return Result.fail("店铺不存在!!");
    }
    // shop不等于null,把查询到的商户信息返回给前端
    return Result.ok(shop);
}
@Override
public Shop queryWithMutex(Long id) {
    //1.先从Redis中查询对应的店铺缓存信息,这里的常量值是固定的店铺前缀+查询店铺的Id
    String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
    //2.如果在Redis中查询到了店铺信息,并且店铺的信息不是空字符串则转为Shop类型直接返回,""和null以及"/t/n(换行)"都会判定为空即返回false
    if (StrUtil.isNotBlank(shopJson)) {
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return shop;
    }
    //3.如果命中的是空字符串即我们缓存的空数据,返回null
    if (shopJson != null) {
        return null;
    }
    // 4.没有命中则尝试根据锁的Id(锁前缀+店铺Id)获取互斥锁(本质是插入key),实现缓存重构
    // 调用Thread的sleep方法会抛出异常,可以使用try/catch/finally把获取锁和释放锁的过程包裹起来
    Shop shop = null;
    try {
        // 4.1 获取互斥锁
        boolean isLock = tryLock(LOCK_SHOP_KEY + id);
        // 4.2 判断是否获取锁成功(插入key是否成功)
        if(!isLock){
            //4.3 获取锁失败(插入key失败),则休眠一段时间重新查询商铺缓存(递归)
            Thread.sleep(50);
            return queryWithMutex(id);
        }
        //4.4 获取锁成功(插入key成功),则根据店铺的Id查询数据库
        shop = getById(id);
        // 由于本地查询数据库较快,这里可以模拟重建延时触发并发冲突
        Thread.sleep(200);
        // 5.在数据库中查不到对应的店铺则将空字符串写入Redis同时设置有效期
        if(shop == null){
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }
        //6.在数据库中查到了店铺信息即shop不为null,将shop对象转化为json字符串写入redis并设置TTL
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
    }catch (Exception e){
        throw new RuntimeException(e);
    }
    finally {
        //7.不管前面是否会有异常,最终都必须释放锁
        unlock(lockKey);
    }
    // 最终把查询到的商户信息返回给前端
    return shop;
}

测试互斥锁解决缓存击穿

使用Jmeter模拟缓存击穿情景,在某时刻一个热点店铺的缓存的TTL到期了,此时用户不能从Redis中获取热点店铺的缓存数据,然后就都得去数据库里查询店铺信息

  • 首先将Redis中的热点店铺的缓存数据删除模拟TTL到期,然后使用Jmete开100个线程来访问这个没有缓存的店铺信息

  • 如果后台日志只输出了一条SQL语句则说明我们的互斥锁是生效的,没有造成大量用户都去数据库执行SQL语句查询店铺的信息

PLAINTEXT
: ==>  Preparing: SELECT id,name,type_id,images,area,address,x,y,avg_price,sold,comments,score,open_hours,create_time,update_time FROM tb_shop WHERE id=?
: ==> Parameters: 2(Long)
: <==      Total: 1

在这里插入图片描述

逻辑过期(缓存预热)

缓存击穿问题主要原因是由于我们对key设置了过期时间,假设我们不设置过期时间其实就不会有缓存击穿的问题,但是不设置过期时间,缓存数据又会一直占用内存

  • 优点: 通过异步线程构建缓存,避免其他线程出现等待,提高了性能
  • 缺点: 构建异步线程业务复杂,需要维护一个expire字段需要额外内存消耗, 在异步线程构建完缓存之前,其他线程返回的都是过期的数据(脏数据)导致数据不一致

在这里插入图片描述

逻辑过期应用

实现根据店铺Id查询商铺的业务,基于逻辑过期方式(需要提前添加热点key)来解决缓存击穿问题

在这里插入图片描述

第一步: 因为现在redis中存储的数据的value需要带上过期时间属性,可以新建一个实体类包含原有的数据和过期时间字段(不侵入原来代码)

@Data
public class RedisData {
    // 过期时间
    private LocalDateTime expireTime
    // 原有数据(用万能的Object) 
    private Object data;
}

第二步: 在ShopServiceImpl中新增一个方法,利用单元测试进行缓存预热即添加热点key,将热点店铺信息和过期时间字段封装到RedisData对象中并写入Redis缓存中

public void saveShop2Redis(Long id, Long expirSeconds) {
    // 1.根据店铺Id去数据库中查询店铺数据
    Shop shop = getById(id);
    // 由于本地查询数据库较快,模拟重建延时
    Thread.sleep(200);
    // 2.封装逻辑过期时间(当前时间转换为秒)
    RedisData redisData = new RedisData();
    // 设置热点店铺信息
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expirSeconds));
    // 3.将包含热点的店铺信息和逻辑过期时间字段的RedisData对象转化为JSON字符串缓存到Redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

第三步: 在测试类中运行测试方法,然后去Redis图形化页面查看存入的value(含有data字段即shop对象和expireTime逻辑过期时间字段)

@SpringBootTest
class HmDianPingApplicationTests {
    @Autowired
    private ShopServiceImpl shopService;
    @Test
    public void test(){
        shopService.saveShop2Redis(1L,1000L);
    }
}
{
    "data": {
        "area": "大关",
        "openHours": "10:00-22:00",
        "sold": 4215,
        "images": "https://qcloud.dpfile.com/pc/jiclIsCKmOI2arxKN1Uf0Hx3PucIJH8q0QSz-Z8llzcN56-IdNpm8K8sG4.jpg",
        "address": "金华路锦昌文华苑29号",
        "comments": 3035,
        "avgPrice": 80,
        "updateTime": 1666502007000,
        "score": 37,
        "createTime": 1640167839000,
        "name": "476茶餐厅",
        "x": 120.149192,
        "y": 30.316078,
        "typeId": 1,
        "id": 1
    },
    "expireTime": 1666519036559
}

第四步: 编写queryWithLogicalExpire方法,在该方法中如果查到店铺信息返回shop查不到则返回null,最后在queryById方法中做统一判断并返回结果类

在这里插入图片描述

//声明一个线程池,因为使用逻辑过期解决缓存击穿的方式需要新建一个线程来完成重构缓存
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

@Override
public Result queryById(Long id) {       
    // 测试使用逻辑过期的方式解决缓存击穿
    Shop shop = queryWithLogicalExpire(id);
    // 如果shop等于null,表示数据库中对应店铺不存在或者缓存的店铺信息是空字符串
    if (shop == null) {
        return Result.fail("店铺不存在!!");
    }
    // shop不等于null,把查询到的商户信息返回给前端
    return Result.ok(shop);
}

public Shop queryWithLogicalExpire(Long id) {
    //1.先从Redis中查询对应的热点店铺缓存信息(包含过期时间),这里的常量值是固定的店铺前缀+查询店铺的Id
    String json = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
    //2.如果未命中即json等于null或命中了但json等于空字符串直接返回null(说明我们没有导入对应的key)
    //""和null以及"/t/n(换行)"都会判定为空即返回false
    if (StrUtil.isBlank(json)) {
        return null;
    }
    //3.如果在Redis中查询到了热点店铺信息并且不是空字符串,则将JSON字符串转化为RedisData对象
    RedisData redisData = JSONUtil.toBean(json, RedisData.class);
    //4.redisData.getData()的本质类型是JSONObject类型(还是JSON字符串)并不是Object类型对象,所以不能直接强转为Shop类型,需要使用工具类
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    //5.获取RedisData对象中封装的过期时间,判断是否过期
    LocalDateTime expireTime = redisData.getExpireTime();
    if(expireTime.isAfter(LocalDateTime.now())) {
        // 5.1.未过期,直接返回店铺信息 
        return shop;
    }
    // 6.已过期,需要缓存重建,查询数据库对应的店铺信息然后写入Redis同时设置逻辑过期时间
    // 6.1.获取互斥锁
    boolean isLock = tryLock(LOCK_SHOP_KEY + id);
    // 6.2.判断是否获取锁成功
    if (isLock){
        // 再次检测Redis缓存是否过期(双重检查),如果存在则无需重建缓存
        // 如果Redis中缓存的店铺信息还是过期,开启独立线程,实现缓存重建(测试的时候可以休眠200ms),实际中缓存的逻辑过期时间设置为30分钟
        CACHE_REBUILD_EXECUTOR.submit( ()->{// 开启独立线程
            try{
                this.saveShop2Redis(id,20L);
            }catch (Exception e){
                throw new RuntimeException(e);
            }finally {
                unlock(LOCK_SHOP_KEY + id);
            }
        });
    }
    // 6.4.返回过期的商铺信息
    return shop;
}

public void saveShop2Redis(Long id, Long expirSeconds) {
    // 1.根据店铺Id去数据库中查询店铺数据
    Shop shop = getById(id);
    // 由于本地查询数据库较快,模拟重建延时
    Thread.sleep(200);
    // 2.封装逻辑过期时间(当前时间转换为秒)
    RedisData redisData = new RedisData();
    // 设置热点店铺信息
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expirSeconds));
    // 3.将包含热点的店铺信息和逻辑过期时间字段的RedisData对象转化为JSON字符串缓存到Redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

测试逻辑过期解决缓存击穿

使用Jmeter进行测试所有的线程查不到数据时是否都会执行缓存重建还是返回旧数据,重建数据时如果数据不一致会不会更新Redis中的缓存数据

  • 在测试类HmDianPingApplicationTests中使用saveShop2Redis方法,向Redis中添加一个热点店铺信息的缓存同时设置逻辑过期时间为2秒
  • 在MySQL数据库中手动修改这个热点店铺的信息,2秒后Redis中缓存的热点店铺数据逻辑过期且和MySQL数据库中对应的店铺信息不一致
  • 当用户访问到过期的缓存数据的时候就需要来新开一个线程重构缓存数据,在重构之前只能获得脏数据(修改前的数据),重构完后才能获得新数据(修改后的数据)

开100个去访问逻辑过期数据

在这里插入图片描述

前面的用户只能看到脏数据,后面的用户看到的才是新数据

在这里插入图片描述

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

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

相关文章

KmeansR趋势分析

KmeansR趋势分析 library(KmeansR) df <- data.frame(A runif(2000),B runif(2000),C runif(2000)) KmeansR::KmeansR(df,centers 6,table FALSE)

【JavaEE初阶】 线程安全

文章目录 &#x1f334;线程安全的概念&#x1f333;观察线程不安全&#x1f384;线程不安全的原因&#x1f6a9;修改共享数据&#x1f4cc;原子性&#x1f4cc; 可见性&#x1f4cc;代码顺序性 &#x1f332;解决之前的线程不安全问题⭕总结 &#x1f334;线程安全的概念 线程…

关于ABB速度,加速度,轴监控指令

关于ABB速度&#xff0c;加速度&#xff0c;轴监控 关于轴监控指令要选择启用和关闭&#xff0c;这个指令是为了防止机器人在抓件放件过程中6轴来回旋转&#xff0c;已最佳的姿态运动 收录于合集 #ABB机器人 9个 上一篇关于ABB机器人的IO创建和设置

代码随想录Day15 二叉树 LeetCodeT513 找树左下角的值 T112路径总和 T106 从中序和后序遍历构造二叉树

以上思路来自于:代码随想录 (programmercarl.com) LeetCode T513 找树左下角的值 题目思路: 本题思路:这题我们使用递归法和迭代法解决问题 注意:左下角的值不一定就是一直向左遍历的叶子结点的值,首先可以确定是最后一行的第一个叶子结点的值,也就是最大深度的叶子结点的值 定…

flask入门

第一个Flask项目 创建后项目如下图 static存放静态文件&#xff0c;templates存放Jinja2模板&#xff0c;app.py是整个项目的入口文件 我们略微理解下app.py这里的代码 # 从flask这个包中导入Flask类 from flask import Flask#使用Flask类创建一个app对象 #__name__:代表当前…

Linux下kibana的安装与配置

1. 环境配置 确保Linux服务器上已安装Java 8或更高版本。可以通过运行 java -version 来验证Java的版本。 下载Kibana 7.17.11的压缩文件&#xff0c;可以从Kibana 7.17.11下载 上传服务器&#xff0c;并解压Kibana压缩文件。 2. Kibana配置 编辑Kibana的配置文件 config/k…

JS中使用递归的一次探索

什么是递归&#xff1a;递归的思想是把一个大型复杂问题层层转化为一个与原问题规模更小的问题&#xff0c;问题被拆解成子问题后&#xff0c;递归调用继续进行&#xff0c;直到子问题无需进一步递归就可以解决的地步为止。 说白话就是函数自己调自己。 再翻译白话&#xff1…

【Unity ShaderGraph】| 如何快速制作一个炫酷 模型裁剪效果 实战

前言 【Unity ShaderGraph】| 如何快速制作一个炫酷 模型裁剪效果 实战一、效果展示二、简易裁剪效果三、进阶裁剪效果四、应用实例 前言 本文将使用Unity 的ShaderGraph制作一个模型裁剪的效果&#xff0c;可以直接拿到项目中使用。对ShaderGraph还不了解的小伙伴可以参考这篇…

练[CISCN2019 华东南赛区]Double Secret

[CISCN2019 华东南赛区]Double Secret 文章目录 [CISCN2019 华东南赛区]Double Secret掌握知识解题思路关键paylaod 掌握知识 ​ flask框架报错源码泄露&#xff0c;使用脚本进行RC4加解&#xff0c;ssti使用内置函数进行模板注入 解题思路 打开网站链接&#xff0c;页面就一…

【LeetCode75】第六十九题 或运算的最小翻转次数

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目给我们 a&#xff0c;b&#xff0c;c 三个数&#xff0c;我们可以对 a 和 b 的二进制形态中的任何一位做翻转&#xff0c;问我们最少…

MobileViT v2导出onnx模型时遇Col2Im算子无法导出问题

相关error log索引 onnxruntime.capi.onnxruntime_pybind11_state.InvalidGraph: [ONNXRuntimeError] : 10 : INVALID_GRAPH : This is an invalid model. In Node, ("/classifier/classifier.0/ReduceMean", ReduceMean, "", -1) : ("/layer_5/laye…

hive3.1核心源码思路

系列文章目录 大数据主要组件核心源码解析 文章目录 系列文章目录大数据主要组件核心源码解析 前言一、HQL转化为MR 核心思路二、核心代码1. 入口类&#xff0c;生命线2. 编译代码3. 执行代码 总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 对大…

UUID和雪花(Snowflake)算法该如何选择?

博主简介&#xff1a;不写代码没饭吃&#xff0c;一名全栈领域的创作者&#xff0c;专注于研究互联网产品的解决方案和技术。熟悉云原生、微服务架构&#xff0c;分享一些项目实战经验以及前沿技术的见解。关注我们的主页&#xff0c;探索全栈开发&#xff0c;期待与您一起在移…

嵌入式养成计划-38----C++--匿名对象--友元--常成员函数和常对象--运算符重载

八十七、匿名对象 概念&#xff1a;没有名字对象格式 &#xff1a;类名&#xff08;&#xff09;;作用 用匿名对象给有名对象初始化的用匿名对象给对象数组初始化的匿名对象作为函数实参使用 示例 : #include <iostream> using namespace std; class Dog { private:s…

小程序如何设置各种时间参数

在小程序管理员后台->基本设置处&#xff0c;可以设置各种时间。例如待支付提醒时间、待支付取消时间、自动发货时间、自动收货时间、自动评价时间等等。下面具体解释一下各个时间的意思。 1. 待支付提醒时间&#xff1a;在用户下单后&#xff0c;如果一段时间内没有完成支付…

IDEA的使用(一)代码模块的导入、快捷使用、自定义 (IntelliJ IDEA 2022.1.3版本)

目录 1. IDEA项目结构 2. 模块的导入操作 2.1 正规操作 2.2 取巧操作 2.3 出现乱码 2.4 模块改名 3. 代码模板的使用 后缀补全&#xff08;Postfix Completion&#xff09;、实时模板&#xff08;Live Templates&#xff09;菜单里面什么介绍都有&#xff0c;可以自学&a…

C#(Csharp)我的基础教程(四)(我的菜鸟教程笔记)-Windows项目结构分析、UI设计和综合事件应用的探究与学习

目录 windows项目是我们.NET学习一开始必备的内容。 1、窗体类&#xff08;主代码文件窗体设计器后台代码文件&#xff09; 主窗体对象的创建&#xff1a;在Program类里面&#xff1a; Application.Run(new FrmMain());这句代码就决定了&#xff0c;当前窗体是项目的主窗体。…

Python Opencv实践 - 车辆识别(1)读取视频,移除背景,做预处理

示例中的图像的腐蚀、膨胀和闭运算等需要根据具体视频进行实验得到最佳效果。代码仅供参考。 import cv2 as cv import numpy as np#读取视频文件 video cv.VideoCapture("../../SampleVideos/Traffic.mp4") FPS 10 DELAY int(1000 / FPS) kernel cv.getStructu…

Python数据容器——字典的常用操作(增、删、改、查)

作者&#xff1a;Insist-- 个人主页&#xff1a;insist--个人主页 本文专栏&#xff1a;Python专栏 专栏介绍&#xff1a;本专栏为免费专栏&#xff0c;并且会持续更新python基础知识&#xff0c;欢迎各位订阅关注. 目录 一、理解字典 1. Python字典是什么&#xff1f; 2. 字…

隆重宣布:.NET 8 RC1 现已推出

作者&#xff1a;Leslie Richardson 排版&#xff1a;Alan Wang .NET 8 RC1 现已推出。这是我们两个候选版本中的第一个。此版本包括适用于 Android 和 WASM 的新 AOT 模式、System.Text.Json 改进以及对容器的 Azure Managed Identity 支持。如果您还没有开始学习和测试 .NET …