Redis实战案例8-缓存击穿及其解决方案和案例说明

news2024/12/28 20:07:55

1. 缓存击穿

缓存击穿是指一个被频繁访问(高并发访问并且缓存重建业务较复杂)的缓存键因为过期失效,同时又有大量并发请求访问此键,导致请求直接落到数据库或后端服务上,增加了系统的负载并可能导致系统崩溃

在这里插入图片描述

常见的解决方案两种:
互斥锁
逻辑过期

在这里插入图片描述

在这里插入图片描述

互斥锁的优点是它可以确保只有一个线程在访问缓存内容,并且在缓存中没有命中时,只会读取一次后端数据库(或其他数据源),其余线程会等待读取完毕后再次读取缓存,这避免了大量的并发请求直接落到后端,从而减少了并发压力,保证系统的稳定性,并且可以保证数据的一致性;
互斥锁的缺点是会增加单个请求的响应时间,因为只有一个线程能够读取缓存值,其他线程则需要等待,这可能会在高并发场景下导致线程池饱和

逻辑过期的优点是可以减少缓存的更新次数,避免在没有必要的情况下过多地读取后端数据源,并且在数据本身有频繁更新的情况下可以避免缓存数据过时;
逻辑过期的缺点是在某些极端情况下会出现缓存为空的情况,如果此时恰巧有大量请求同时访问缓存,则可能导致缓存击穿,并且无法避免大量的并发请求直接落到后端,并且实现起来也是比较复杂和数据无法保证一致性(因为可能返回旧数据)。

2. 互斥锁解决缓存击穿问题

要与缓存穿透区分开来

在这里插入图片描述

使用 setnx 命令(在Java封装Redis功能的API中使用的是setIfAbsent()方法)可以实现互斥锁的功能, setnx 命令可以原子性地设置一个关键字,如果关键字不存在,则设置并返回 1,如果关键字已存在,则不做任何操作并返回 0;
在 Redis 中,该命令会在关键字不存在时将其设置为指定的值(锁),同时返回设置结果,因此可以在线程尝试去设置同一个关键字时,只有一个线程能够成功获取锁,其他线程会返回设置失败;
在释放锁时,应该先对锁进行校验(如,判断当前操作是否为拥有锁的线程),然后再执行删除操作,以确保当前线程不会释放其他线程加的锁;
最后在高并发的场景中,这种操作是会增加Redis服务器的负载的,因此需要合理设置 Redis 参数和优化 Redis 集群架构(在Redis高级中给出方案);

Boolean 类型的值可以自动拆箱为 boolean 类型的值,但是在进行自动拆箱时,如果 Boolean 类型的值为 null,则会抛出 NullPointerException 异常。因此,在进行自动拆箱时,需要注意可能出现的空指针异常。
在使用 Redis 进行分布式锁时,setIfAbsent 方法返回的是 Boolean 类型的值,因此在返回该值时,可能会出现自动拆箱引发的空指针异常。如果出现空指针异常,一般是因为 Redis 连接池未初始化或注入失败,或者 Redis 服务出现了故障。
加锁和释放锁的代码:

private boolean tryLock(String key){
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    // 这里的布尔类型自动拆箱可能会出现空指针异常
    return BooleanUtil.isTrue(flag);
}
private void unLock(String key){
    stringRedisTemplate.delete(key);
}

封装之前缓存穿透的代码:

/**
 * 缓存穿透功能封装
 * @param id
 * @return
 */
public Shop queryWithPassThrough(Long id) {
    String key = CACHE_SHOP_KEY + id;
    //1. 从Redis中查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    //2. 判断是否存在
    if(StrUtil.isNotBlank(shopJson)){
        //3. 存在,直接返回
        return JSONUtil.toBean(shopJson, Shop.class);
    }
    // 这里要先判断命中的是否是null,因为是null的话也是被上面逻辑判断为不存在
    // 这里要做缓存穿透处理,所以要对null多做一次判断,如果命中的是null则shopJson为""
    if("".equals(shopJson)){
        return null;
    }
    //4. 不存在,根据id查询数据库
    Shop byId = getById(id);
    if(byId == null) {
        //5. 不存在,将null写入redis,以便下次继续查询缓存时,如果还是查询空值可以直接返回false信息
        stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        return null;
    }
    //6. 存在,写入Redis
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(byId), CACHE_SHOP_TTL, TimeUnit.MINUTES);
    //7. 返回
    return byId;
}

合并封装缓存穿透和使用互斥锁实现缓存击穿的代码:

/**
 * 缓存击穿和缓存穿透功能合并封装
 * @param id
 * @return
 */
public Shop queryWithMutex(Long id) {
    String key = CACHE_SHOP_KEY + id;
    //1. 从Redis中查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    //2. 判断是否存在
    if(StrUtil.isNotBlank(shopJson)){
        //3. 存在,直接返回
        return JSONUtil.toBean(shopJson, Shop.class);
    }
    // 这里要先判断命中的是否是null,因为是null的话也是被上面逻辑判断为不存在
    // 这里要做缓存穿透处理,所以要对null多做一次判断,如果命中的是null则shopJson为""
    if("".equals(shopJson)){
        return null;
    }
    //4. 实现缓存重建
    //4.1 获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    Shop shop = null;
    try {
        boolean isLock = tryLock(lockKey);
        //4.2 判断获取是否成功
        if(!isLock) {
            //4.3 失败,则休眠并重试
            Thread.sleep(50);
            // 递归重试
            return queryWithMutex(id);
        }
        //4.4 成功,根据id查询数据库
        shop = getById(id);
        if(shop == null) {
            //5. 不存在,将null写入redis,以便下次继续查询缓存时,如果还是查询空值可以直接返回false信息
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //6. 存在,写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        //7. 释放互斥锁
        unLock(lockKey);
    }
    //8. 返回
    return shop;
}

这里并发测试采用JMeter

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

也可以看出查询数据库的操作只出现了一次

在这里插入图片描述

3. 逻辑过期方式解决缓存击穿问题

这里案例中的缓存key默认为高频key,这类key可看做是不会过期,所以这里不做缓存穿透和击穿处理;
这里获取锁之后,如果获取到了则是开启独立线程进行查询操作,如果没有获取到则是直接返回旧数据;
容易出现数据不一致;

在这里插入图片描述

将数据写入Redis,需要设置一个逻辑的过期时间;
目前写入的Shop是没有逻辑过期时间的字段,直接在该类中加入字段不推荐,有侵入性;
新建一个类,设置一个data属性来存Shop对象数据,并且添加字段逻辑过期时间;

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

注:热点key一般是提前写入的,缓存预热;
缓存预热代码示例,要进行单元测试,以便热点key缓存写入Redis中:

/**
 * 给热点key缓存预热
 * @param id
 * @param expireSeconds
 */
private void saveShop2Redis(Long id, Long expireSeconds) {
    // 1.查询店铺数据
    Shop shop = getById(id);
    // 2.封装逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    // 3.写入Redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

对我们提前写入的key进行逻辑过期方式处理,解决缓存击穿问题;
这里要避免更新过期数据,当缓存失效(逻辑过期),部分线程经过逻辑过期判断之后,会进行获取锁的操作并进入阻塞状态,可能已经有线程通过更新缓存已经将数据写入缓存中了,这是该线程会释放锁。如果不进行二次逻辑过期判断,当前等待互斥锁的线程可能会将已经更新的数据再次从数据库中读取并写入缓存,导致缓存中存储的是重复的数据。通过逻辑过期判断,可以避免这种重复更新的情况发生。

/**
 * 缓存击穿(逻辑过期)功能封装
 * @param id
 * @return
 */
public Shop queryWithLogicalExpire(Long id) {
    String key = CACHE_SHOP_KEY + id;
    //1. 从Redis中查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    //2. 判断是否存在
    if(StrUtil.isBlank(shopJson)){
        //3. 不存在,直接返回(这里做的事热点key预热,所以已经假定热点key已经在缓存中)
        return null;
    }
    //4. 存在,需要判断过期时间,需要先把json反序列化为对象
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    //5. 判断是否过期
    if(expireTime.isAfter(LocalDateTime.now())) {
        //5.1 未过期,直接返回店铺信息
        return shop;
    }
    //5.2 已过期,需要缓存重建
    //6. 缓存重建
    //6.1 获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    //6.2 判断是否获取锁成功
    boolean isLock = tryLock(lockKey);
    if(isLock) {
        // 二次验证是否过期,防止多线程下出现缓存重建多次
        String shopJson2 = stringRedisTemplate.opsForValue().get(key);
        // 这里假定key存在,所以不做存在校验
        // 存在,需要判断过期时间,需要先把json反序列化为对象
        RedisData redisData2 = JSONUtil.toBean(shopJson2, RedisData.class);
        Shop shop2 = JSONUtil.toBean((JSONObject) redisData2.getData(), Shop.class);
        LocalDateTime expireTime2 = redisData2.getExpireTime();
        if(expireTime2.isAfter(LocalDateTime.now())) {
            // 未过期,直接返回店铺信息
            return shop2;
        }
            //6.3 成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 重建缓存,这里设置的值小一点,方便观察程序执行效果,实际开发应该设为30min
                    this.saveShop2Redis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unLock(lockKey);
                }
            });
    }
    //7. 返回
    return shop;
}

对逻辑过期处理缓存击穿测试;
启动服务,并进行测试(将热点key值写入);

@SpringBootTest
class HmDianPingApplicationTests {

    @Resource
    private ShopServiceImpl shopService;

    @Test
    void testSaveShop() throws InterruptedException {
        shopService.saveShop2Redis(1L, 10L);
    }

}

在这里插入图片描述

这里修改一下数据数据,好观察重建缓存;

在这里插入图片描述

启动JMeter测试多线程下结果变化;
可以看出在更新缓存前获得的是旧数据,更新之后是新数据;

在这里插入图片描述

在这里插入图片描述

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

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

相关文章

开源虚拟化工具VirtualBox安装部署

什么是Virtualbox VirtualBox是一款由Oracle开发和维护的免费开源虚拟化软件,用于在一台计算机上创建和管理多个虚拟机。它允许用户在单个物理计算机上运行多个操作系统,例如Windows、Linux、macOS等。VirtualBox提供了一个虚拟化环境,使用户…

详细介绍如何使用 OpenCV 实现自动文档扫描仪--附实现源码

文末附相关源代码实现的免费下载链接 文档扫描是将物理文档转换为数字形式的过程。可以通过扫描仪或手机摄像头拍摄图像来完成。我们将在本教程中讨论如何使用计算机视觉和图像处理技术有效地实现这一目标。 在当今的计算机时代,几乎不需要任何物理文书工作。尽管如此,在仍…

现代操作系统第一章学习笔记

先附上一个原版的课后习题答案的链接。 课后习题答案 1.1 什么是操作系统 操作系统:他的任务是为应用程序提供一个更好、更简单、更清晰的计算机模型,并管理贮存、磁盘等所有设备。 计算机系统总的来说分为软件和硬件,如下图所示。多数计算…

华为孟晚舟:拥抱5G变革

2023 MWC上海开幕,华为副董事长、轮值董事长、CFO孟晚舟在大会上发表了“拥抱5G变革”的主题演讲。她表示:全球5G商用4年来,正持续引领价值创造,而5.5G是5G网络演进的必然之路;面向未来,科技走向复杂大系统…

突破未来:SAP助力新能源产业腾飞!

新能源行业概况 在互联网的时代浪潮下,多数行业都开始进行了调整与整合,竞争无处不在。作为新兴的新能源行业,如果不想从竞争中被淘汰,就需要把握时代的脉搏,找到突破口,带领企业在市场竞争中越走越远。我…

学习:PSM倾向得分匹配基于stata实现

PSM倾向性匹配得分 定义 就是一个用户属于实验组的倾向性,也就是在特定特征的情况下属于实验组的概率(条件概率) 其他定义: PSM 通过统计学模型计算每个观测的每个协变量的综合倾向性得分,再按照倾向性得分是否接近进⾏匹配。 用直白的话来…

大数据技术——电影推荐系统大数据综合实训项目

具体文档请参考如下链接: https://download.csdn.net/download/qq_53142796/87949491https://download.csdn.net/download/qq_53142796/87949491 https://download.csdn.net/download/qq_53142796/87949489https://download.csdn.net/download/qq_53142796/879494…

2.设计模式之前5种设计模式单例工厂原型建造者适配器

1.怎么掌握设计模式? 独孤5剑 先是锋利的剑 后面是无剑才是最强的 ,GOF四人组写的<设计模式>书,包含了23种,实际可能还有其他,不要被束缚(只是覆盖了大部分).设计模式适合的人群: 1.不知道设计模式 2.有编程经验,但是写的好多代码有设计模式却不知道 3.学习过设计模式,发…

Unity 提取并拷贝Hierarchy路径

提取并拷贝Hierarchy路径 &#x1f354;效果&#x1f96a;食用 &#x1f354;效果 &#x1f96a;食用 &#x1f4a1;.导入包后直接选中GameObject右键选择&#xff1a;复制Hierarchy路径 &#x1f4a1;.快捷键Ctrl V 即可粘贴路径

SurfaceFlinge/InputFlinger分析-android画面缩放后依然点击正常原理分析

hi&#xff0c;粉丝朋友们&#xff1a; 这两天刚好在做自由窗口相关国内需求&#xff0c;刚好遇到一个疑惑&#xff0c;那就是画面进行缩放后发现依然触摸画面可以正常反映问题。 具体疑惑背景 疑问点如下&#xff1a; 坐标是针对屏幕的&#xff0c;按钮也是相对Activity的&…

【SPI】STM32 SPI 双机通信,SPI从机模式使用

文章目录 一、SPI主机配置二、SPI从机配置三、双机通信1 轮询中断&#xff08;低速&#xff09;2 轮询DMA&#xff08;低速&#xff09;3 DMADMA&#xff08;高速&#xff09;4 开启CRC校验&#xff08;自选&#xff09; 四、遇到的问题1 高速使用时&#xff0c;程序卡死&#…

JavaScript 原型与原型链

所有的 JavaScript 对象都会从一个 prototype&#xff08;原型对象&#xff09;中继承属性和方法&#xff1a; Date 对象从 Date.prototype 继承。 Array 对象从 Array.prototype 继承。 所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。 JavaScript 对象有一个指…

【C#】代码解析--打印数据集

系列文章 【C#】编号生成器&#xff08;定义单号规则、固定字符、流水号、业务单号&#xff09; 本文链接&#xff1a;https://blog.csdn.net/youcheng_ge/article/details/129129787 【C#】日期范围生成器&#xff08;开始日期、结束日期&#xff09; 本文链接&#xff1a;h…

LangChain:LLM应用程序开发(中)——文档问答、评估、Agents(代理)

文章目录 四、文档问答4.1 快速入门4.2 逐步实现4.3 其它方法 五、评估5.1 创建QA app5.2 生成测试数据点5.2.1 Hard-coded examples5.2.2 LLM-Generated examples 5.3 link chain debug手动评估5.4 LLM assisted evaluation5.5 LangChain Evaluation platform 六、Agents&…

704.二分查找

一、题目 二、代码 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int search(int* nums, int numsSize, int target) {int left 0;int right numsSize - 1;while (left < right) {int middle (left right) / 2;if (target > nums[middle]) {left m…

pip3 报错 distribution was not found and is required by the application

环境&#xff1a; centos 8 &#xff0c; pyhton3.6 背景&#xff1a; 升级python3.6 至 python3.10后&#xff0c; pip3安装包报错&#xff1a; pkg_resources.VersionConflict: (pip 21.2.4 (/usr/local/python3/lib/python3.10/site-packages), Requirement.parse(pip23.…

Linux:scp命令

1.介绍 scp命令是cp命令的升级版&#xff0c;即&#xff1a;ssh cp&#xff0c;通过SSH协议完成文件的复制。其主要的功能就是&#xff1a;在不同的Linux服务器之间&#xff0c;通过SSH协议互相传输文件。只要知晓服务器的账户和密码&#xff08;或密钥&#xff09;&#xff0c…

美光之后,韩国存储芯片也将成为输家,将损失千亿美元

美光很可能将完全丧失中国的企业市场&#xff0c;随着影响的扩大&#xff0c;韩国存储芯片或也将因美光的影响而蒙受巨大的损失&#xff0c;预计损失最高可能达到千亿美元&#xff0c;韩国存储芯片的老大地位也将因此动摇。 美光日前对媒体披露的数据指出因中国的安全审查&…

GitHub下的项目有pom.xml但右侧没有Maven问题

1.可以使用idea自带功能将pom.xml作为Maven导入 双击shift&#xff0c;输入Maven点击Add Maven Projects注意&#xff1a;在settings中将Maven先配置好&#xff0c;我使用的是自己设置的Maven仓库而不是idea自动部署在C盘的仓库

SQL Server数据库 -- 表的创建与管理

文章目录 一、数据表的组成二、创建数据表 表的创建表的查看表的增加表的修改表的删除、三、表的架构操作四、总结 前言 上次博客写到了数据库的创建与管理&#xff0c;但是创建的库里面什么东西都没有&#xff0c;现在我们需要在库里面添加数据表内容 一、数据表的组成 在创…