解决Redis缓存击穿(互斥锁、逻辑过期)

文章目录

  • 背景
  • 代码实现
    • 前置
      • 实体类
      • 常量类
      • 工具类
      • 结果返回类
      • 控制层
    • 互斥锁方案
    • 逻辑过期方案

背景

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

image-20241026110231988

常见的解决方案有两种:

1.互斥锁

2.逻辑过期

互斥锁:

本质就是让所有线程在缓存未命中时,需要先获取互斥锁才能从数据库查询并重建缓存,而未获取到互斥锁的,需要不断循环查询缓存、未命中就尝试获取互斥锁的过程。因此这种方式可以让所有线程返回的数据都一定是最新的,但响应速度不高

image-20241026110251082

逻辑过期:

本质就是让热点 key 在 redis 中永不过期,而通过过期字段来自行判断该 key 是否过期,如果未过期,则直接返回;如果过期,则需要获取互斥锁,并开启新线程来重建缓存,而原线程可以直接返回旧数据;如果获取互斥锁失败,就代表已有其他线程正在执行缓存重建工作,此时直接返回旧数据即可

image-20241026110307793

两者的对比:

解决方案优点缺点
互斥锁没有额外的内存消耗
保证一致性
实现简单
线程需要等待,性能受影响
可能有死锁风险
逻辑过期线程无需等待,性能较好不保证一致性
有额外内存消耗
实现复杂

代码实现

前置

这里以根据 id 查询商品店铺为案例

实体类

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_shop")
public class Shop implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 商铺名称
     */
    private String name;

    /**
     * 商铺类型的id
     */
    private Long typeId;

    /**
     * 商铺图片,多个图片以','隔开
     */
    private String images;

    /**
     * 商圈,例如陆家嘴
     */
    private String area;

    /**
     * 地址
     */
    private String address;

    /**
     * 经度
     */
    private Double x;

    /**
     * 维度
     */
    private Double y;

    /**
     * 均价,取整数
     */
    private Long avgPrice;

    /**
     * 销量
     */
    private Integer sold;

    /**
     * 评论数量
     */
    private Integer comments;

    /**
     * 评分,1~5分,乘10保存,避免小数
     */
    private Integer score;

    /**
     * 营业时间,例如 10:00-22:00
     */
    private String openHours;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;


    @TableField(exist = false)
    private Double distance;
}

常量类

public class RedisConstants {
    public static final String CACHE_SHOP_KEY = "cache:shop:";

    public static final String LOCK_SHOP_KEY = "lock:shop:";
    public static final Long LOCK_SHOP_TTL = 10L;

    public static final String EXPIRE_KEY = "expire";
}

工具类

public class ObjectMapUtils {

    // 将对象转为 Map
    public static Map<String, String> obj2Map(Object obj) throws IllegalAccessException {
        Map<String, String> result = new HashMap<>();
        Class<?> clazz = obj.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            // 如果为 static 且 final 则跳过
            if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) {
                continue;
            }
            field.setAccessible(true); // 设置为可访问私有字段
            Object fieldValue = field.get(obj);
            if (fieldValue != null) {
                result.put(field.getName(), field.get(obj).toString());
            }
        }
        return result;
    }

    // 将 Map 转为对象
    public static Object map2Obj(Map<Object, Object> map, Class<?> clazz) throws Exception {
        Object obj = clazz.getDeclaredConstructor().newInstance();
        for (Map.Entry<Object, Object> entry : map.entrySet()) {
            Object fieldName = entry.getKey();
            Object fieldValue = entry.getValue();
            Field field = clazz.getDeclaredField(fieldName.toString());
            field.setAccessible(true); // 设置为可访问私有字段
            String fieldValueStr = fieldValue.toString();
            // 根据字段类型进行转换
            fillField(obj, field, fieldValueStr);

        }
        return obj;
    }

    // 将 Map 转为对象(含排除字段)
    public static Object map2Obj(Map<Object, Object> map, Class<?> clazz, String... excludeFields) throws Exception {
        Object obj = clazz.getDeclaredConstructor().newInstance();
        for (Map.Entry<Object, Object> entry : map.entrySet()) {
            Object fieldName = entry.getKey();
            if(Arrays.asList(excludeFields).contains(fieldName)) {
                continue;
            }
            Object fieldValue = entry.getValue();
            Field field = clazz.getDeclaredField(fieldName.toString());
            field.setAccessible(true); // 设置为可访问私有字段
            String fieldValueStr = fieldValue.toString();
            // 根据字段类型进行转换
            fillField(obj, field, fieldValueStr);
        }
        return obj;
    }

    // 填充字段
    private static void fillField(Object obj, Field field, String value) throws IllegalAccessException {
        if (field.getType().equals(int.class) || field.getType().equals(Integer.class)) {
            field.set(obj, Integer.parseInt(value));
        } else if (field.getType().equals(boolean.class) || field.getType().equals(Boolean.class)) {
            field.set(obj, Boolean.parseBoolean(value));
        } else if (field.getType().equals(double.class) || field.getType().equals(Double.class)) {
            field.set(obj, Double.parseDouble(value));
        } else if (field.getType().equals(long.class) || field.getType().equals(Long.class)) {
            field.set(obj, Long.parseLong(value));
        } else if (field.getType().equals(String.class)) {
            field.set(obj, value);
        } else if(field.getType().equals(LocalDateTime.class)) {
            field.set(obj, LocalDateTime.parse(value));
        }
    }

}

结果返回类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
    private Boolean success;
    private String errorMsg;
    private Object data;
    private Long total;

    public static Result ok(){
        return new Result(true, null, null, null);
    }
    public static Result ok(Object data){
        return new Result(true, null, data, null);
    }
    public static Result ok(List<?> data, Long total){
        return new Result(true, null, data, total);
    }
    public static Result fail(String errorMsg){
        return new Result(false, errorMsg, null, null);
    }
}

控制层

@RestController
@RequestMapping("/shop")
public class ShopController {

    @Resource
    public IShopService shopService;

    /**
     * 根据id查询商铺信息
     * @param id 商铺id
     * @return 商铺详情数据
     */
    @GetMapping("/{id}")
    public Result queryShopById(@PathVariable("id") Long id) {
        return shopService.queryShopById(id);
    }
    
}

互斥锁方案

流程图为:

image-20241026114917122

服务层代码:

public Result queryShopById(Long id) {
    Shop shop = queryWithMutex(id);
    if(shop == null) {
        return Result.fail("店铺不存在");
    }
    return Result.ok(shop);
}

// 互斥锁解决缓存击穿
public Shop queryWithMutex(Long id) {
    String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
    boolean flag = false;
    try {
        do {
            // 从 redis 查询
            Map<Object, Object> entries = redisTemplate.opsForHash().entries(shopKey);
            // 缓存命中
            if(!entries.isEmpty()) {
                try {
                    // 刷新有效期
                    redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
                    Shop shop = (Shop) ObjectMapUtils.map2Obj(entries, Shop.class);
                    return shop;
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
            // 缓存未命中,尝试获取互斥锁
            flag = tryLock(id);
            if(flag) { // 获取成功,进行下一步
                break;
            }
            // 获取失败,睡眠后重试
            Thread.sleep(50);
        } while(true); //未获取到锁,休眠后重试
        // 查询数据库
        Shop shop = this.getById(id);
        if(shop == null) {
            // 不存在,直接返回
            return null;
        }
        // 存在,写入 redis
        try {

            // 测试,延迟缓存重建过程
            /*try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }*/

            redisTemplate.opsForHash().putAll(shopKey, ObjectMapUtils.obj2Map(shop));
            redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        return shop;
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        if(flag) { // 获取了锁需要释放
            unlock(id);
        }
    }

}

测试:

这里使用 JMeter 进行测试

image-20241026125851573

image-20241026125856764

运行结果如下:

image-20241026125910639

image-20241026125948926

可以看到控制台只有一个查询数据库的请求,说明互斥锁生效了

逻辑过期方案

流程图如下:

image-20241026132400171

采用逻辑过期的方式时,key 是不会过期的,而这里由于是热点 key,我们默认其是一定存在于 redis 中的(可以做缓存预热事先加入 redis),因此如果 redis 没命中,就直接返回空

服务层代码:

public Result queryShopById(Long id) {
    // 逻辑过期解决缓存击穿
    Shop shop = queryWithLogicalExpire(id);
    if (shop == null) {
        return Result.fail("店铺不存在");
    }
    return Result.ok(shop);
}

// 逻辑过期解决缓存击穿
private Shop queryWithLogicalExpire(Long id) {
    String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
    // 从 redis 查询
    Map<Object, Object> entries = redisTemplate.opsForHash().entries(shopKey);
    // 缓存未命中,返回空
    if(entries.isEmpty()) {
        return null;
    }
    try {
        Shop shop = (Shop) ObjectMapUtils.map2Obj(entries, Shop.class, RedisConstants.EXPIRE_KEY);
        LocalDateTime expire = LocalDateTime.parse(entries.get(RedisConstants.EXPIRE_KEY).toString());
        // 判断缓存是否过期
        if(expire.isAfter(LocalDateTime.now())) {
            // 未过期则直接返回
            return shop;
        }
        // 过期需要先尝试获取互斥锁
        if(tryLock(id)) {
            // 获取成功
            // 双重检验
            entries = redisTemplate.opsForHash().entries(shopKey);
            shop = (Shop) ObjectMapUtils.map2Obj(entries, Shop.class, RedisConstants.EXPIRE_KEY);
            expire = LocalDateTime.parse(entries.get(RedisConstants.EXPIRE_KEY).toString());
            if(expire.isAfter(LocalDateTime.now())) {
                // 未过期则直接返回
                unlock(id);
                return shop;
            }
            // 通过线程池完成重建缓存任务
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    rebuildCache(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unlock(id);
                }
            });
        }
        return shop;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

// 尝试加锁
private boolean tryLock(Long id) {
    Boolean isLocked = redisTemplate.opsForValue().setIfAbsent(RedisConstants.LOCK_SHOP_KEY + id,
            "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(isLocked);
}

// 解锁
private void unlock(Long id) {
    redisTemplate.delete(RedisConstants.LOCK_SHOP_KEY + id);
}

// 重建缓存
private void rebuildCache(Long id, Long expireTime) throws IllegalAccessException {
    Shop shop = this.getById(id);
    Map<String, String> map = ObjectMapUtils.obj2Map(shop);
    // 添加逻辑过期时间
    map.put(RedisConstants.EXPIRE_KEY, LocalDateTime.now().plusMinutes(expireTime).toString());
    redisTemplate.opsForHash().putAll(RedisConstants.CACHE_SHOP_KEY + id, map);
}

测试:

这里先预热,将 id 为 1 的数据加入,并且让过期字段为过去的时间,即表示此数据已过期

image-20241026140610758

然后将数据库中对应的 name 由 “101茶餐厅” 改为 “103茶餐厅”

然后使用 JMeter 测试

image-20241026142837975

image-20241026142842083

测试结果:

image-20241026142942997

image-20241026142950834

可以看到部分结果返回的旧数据,而部分结果返回的是新数据

且 redis 中的数据也已经更新

image-20241026143033274

并且,系统中只有一条查询数据库的请求
image-20241026143107503

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

详细解读 CVPR2024:VideoBooth: Diffusion-based Video Generation with Image Prompts

Diffusion Models专栏文章汇总:入门与实战 前言:今天是程序员节,先祝大家节日快乐!文本驱动的视频生成正在迅速取得进展。然而,仅仅使用文本提示并不足以准确反映用户意图,特别是对于定制内容的创建。个性化图片领域已经非常成功了,但是在视频个性化领域才刚刚起步,这篇…

10.28.2024刷华为OD C题型

文章目录 HJ9HJ10HJ11HJ13HJ17 HJ9 HJ10 HJ11 HJ13 HJ17

2024年【浙江省安全员-C证】新版试题及浙江省安全员-C证模拟考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 浙江省安全员-C证新版试题考前必练&#xff01;安全生产模拟考试一点通每个月更新浙江省安全员-C证模拟考试题目及答案&#xff01;多做几遍&#xff0c;其实通过浙江省安全员-C证模拟考试很简单。 1、【多选题】5kW以…

《计算机网络网络层:连接虚拟世界的关键桥梁》

一、网络层概述 网络层在计算机网络中占据着至关重要的地位&#xff0c;它作为连接不同网络的关键层次&#xff0c;起着承上启下的作用。网络层的主要任务是实现网络互连&#xff0c;将数据设法从源端经过若干个中间节点传送到目的端&#xff0c;为分组交换网上的不同主机提供通…

【LeetCode每日一题】——862.和至少为 K 的最短子数组

文章目录 一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【题目提示】七【解题思路】八【时空频度】九【代码实现】十【提交结果】 一【题目类别】 前缀和 二【题目难度】 困难 三【题目编号】 862.和至少为 K 的最短子数组 四【题目描述】 …

【Vue】word / excel / ppt / pdf / 视频(mp4,mov) 预览

文件预览 Vue3一. word二. excel三. ppt四. pdf4.1 vue-pdf-embed4.2 iframe 五. 视频六&#xff1a;扩展——kkFileView Vue3 一. word 安装&#xff1a;npm install docx-preview父页面 <template><div><DocPreviewv-if"filePath.includes(docx)"…

【Go-Taskflow:一个类似任务流的有向无环图(DAG)任务执行框架,集成了可视化和性能分析工具,旨在简化并行任务的复杂依赖管理】

Go-Taskflow是一个静态有向无环图&#xff08;DAG&#xff09;任务计算框架&#xff0c;它受到taskflow-cpp的启发&#xff0c;结合了Go语言的原生能力和简洁性&#xff0c;特别适合于并发任务中复杂的依赖管理。 Go-Taskflow的主要特点包括&#xff1a; 高可扩展性&#xff1…

两套环境同一个接口返回不一致的排查

最近遇到个文件下载的问题&#xff0c;在开发环境好好的&#xff0c;测试环境就不行404。查了接近两天才解决。整个思路做个记载。 问题描述&#xff1a;通过视图解析器下载项目中的静态资源文件模板。也就是sringboot的resource目录下的文件。开发环境下载正常&#xff0c;测…

PHP员工管理系统小程序

&#x1f4bc;高效管理&#xff0c;从“员工管理系统”开始&#x1f4bc; &#x1f4cb;【一键录入&#xff0c;信息整合】&#x1f4cb; 你是否还在为整理员工信息而手忙脚乱&#xff1f;纸质档案易丢失、电子表格易混乱&#xff0c;这些问题在“员工管理系统”面前都将迎刃…

MemoRAG:重新定义长期记忆的AI问答模型

MemoRAG模型是如何实现长记忆的&#xff1f; ©作者|Blaze 来源|神州问学 引言 随着人工智能的发展&#xff0c;AI问答模型在各种应用场景中表现出色&#xff0c;尤其是在信息检索和知识问答领域。传统的RAG模型通过结合外部知识库的实时检索与生成模型&#xff0c;极大地…

再次被约谈了

大家好&#xff0c;我又来了&#xff0c;从上周一开始&#xff0c;一直听到不好的传言&#xff0c;下午听说有些人被约谈了&#xff0c;看来裁员工作已经开始了 就在我坐立不安时&#xff0c;看到领导飞书发来信息&#xff1a; 看来终于轮到我了&#xff0c;虽然做好了心里准…

ELK的ElasticStack概念

目录 传送门前言一、ElasticStack是什么二、ElasticStack数据格式1、Elasticsearch的概述2、Elasticsearch核心概念&#xff08;1&#xff09;接近实时&#xff08;NRT&#xff09;&#xff08;2&#xff09;集群&#xff08;cluster&#xff09;&#xff08;3&#xff09;节点…

从零开始docker-compose入门教程,快速上手多容器管理!

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 docker-compose 📒📝 Docker Compose的作用📝 Docker Compose的安装1. 在Linux或macOS上安装2. 在Windows上安装3. 在Linux或macOS上卸载4. 在Windows上卸载📝 Docker Compose基本语法📝 示例:使用Docker Compose部署…

聚水潭到畅捷通T+的数据高效集成方案解析

聚水潭到畅捷通T的数据高效集成方案解析 聚水潭销售出库单到畅捷通销货单的高效数据集成方案 在企业日常运营中&#xff0c;数据的高效流转和准确对接是提升业务效率的关键。本文将分享一个实际案例&#xff0c;展示如何通过轻易云数据集成平台&#xff0c;将聚水潭奇门系统中…

Flink(一)

目录 架构处理有界与无界数据部署应用到任意地方运行任意规模应用利用内存性能 流应用流处理应用的基本组件流状态时间 应用场景事件驱动应用事件驱动应用的优势Flink如何支持事件驱动应用&#xff1f; 典型的事件驱动示例 数据分析应用流式分析应用的优势&#xff1f;Flink 如…

word怎么压缩文件大小?这几种压缩word文件方法超级好用!

word怎么压缩文件大小&#xff1f;在当今快节奏的工作环境中&#xff0c;Word文档已成为我们日常工作的得力助手&#xff0c;然而&#xff0c;随着文档数量的不断增加&#xff0c;文档体积的膨胀成为了一个亟待解决的问题&#xff0c;这不仅导致了存储空间的紧张&#xff0c;也…

【grafana+Prometheus(普罗米修斯)实现监控功能】

一、背景&#xff1a; 在性能测试的时候经常需要观察对应服务器的cpu、内存等指标,或者有些性能测试需要监控数据库的一些信息 二、监控服务器工具&#xff1a; 1、使用jmeter时可以自带监控服务的功能 缺点&#xff1a;只能在运行jmeter的时候才能实现监控功能 2、使用li…

WPF+MVVM案例实战(八)- 自定义开关控件封装实现

文章目录 1、案例运行效果2、项目准备2、功能实现1、控件模板实现2、控件封装1、目录与文件创建2、各文件功能实现 3、开关界面与主窗体菜单实现1、开关界面实现2、主窗体菜单实现 4、源代码获取 1、案例运行效果 2、项目准备 打开项目 Wpf_Examples&#xff0c;新建ToggleBut…

无法启动此程序win10玩游戏找不到d3dx9_43.dll缺失的五种常用有效解决方法

d3dx9_43.dll 是 DirectX 9 的一个关键组件&#xff0c;属于动态链接库&#xff08;DLL&#xff09;文件&#xff0c;由微软公司开发。DirectX 是一组用于多媒体应用的 API&#xff0c;包括 d3dx9_43.dll 在内的组件对游戏和图形应用程序至关重要。该文件主要负责提供3D图形渲染…

手机折叠屏贴膜应用

折叠手机贴膜的主要难点在于其独特的可折叠设计。折叠屏的弯曲部分对贴膜材料提出了更高要求&#xff0c;需要材料具备足够的柔韧性和耐折痕性&#xff0c;以避免在折叠过程中产生裂痕或脱落。此外&#xff0c;贴膜过程中需要确保无气泡、无褶皱&#xff0c;且能完美贴合屏幕的…