[Redis-实战] 企业常用的缓存使用方案(查询、更新、击穿、穿透、雪崩) 附源码

news2024/9/9 7:20:14

目录

🍊 缓存查询策略

🍩 缓存更新策略

🍭 缓存穿透

🍣 缓存雪崩

🍕 缓存击穿

👾 项目源码下载​​​​​​​


🍊 缓存查询策略

我们要查询的业务数据并不是经常改变的, 这里我们可以放到Redis缓存中, 降低对数据库的请求

下面我们以查询店铺为例, 因为店铺列表是不经常改变的数据, 所以我们可以请求redis缓存来降低MySQL的查询压力 

 @Override
    public Result queryShopById(Long id) {

        //1.从Redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop" + id);
        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在, 直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //4.不存在, 根据id查询数据库
        Shop shop = getById(id);
        //不存在, 返回错误提示信息
        if (shop == null){
            return Result.fail("店铺不存在!");
        }
        //存在, 写入redis中
        stringRedisTemplate.opsForValue().set("cache:shop" + id, JSONUtil.toJsonStr(shop));

        return Result.ok(shop);
    }

🍩 缓存更新策略

在常规的企业开发中,我们优先选择的缓存策略是 更新数据库的同时也会去更新缓存

在此情况下我们也要考虑三点 : 

1. 更新数据库后再删除缓存, 再查询的时候重新添加缓存 (这样可以保证数据查询的是最新的)

2.在单体项目中, 将缓存与数据库操作放在同一个事务中, 这样方便回滚. 分布式项目中需要使用分布式事务

3. 在并发场景下, 应当先操作数据库,再删除缓存

    @Override
    @Transactional
    public void updateShop(Shop shop) {
        if (shop.getId() == null) {
            throw new RuntimeException("ID不能为null");
        }
        //1. 先更新数据库
        updateById(shop);
        //2. 后删除缓存
        stringRedisTemplate.delete("cache:shop" + shop.getId());
    }

🍭 缓存穿透

缓存穿透场景 : 假设用户恶意请求的数据在Redis和MySQL中均不存在, 导致Redis中的缓存不生效从而一直去请求MySQL

解决方案 : 因为用户传来的恶意数据在缓存和数据库中都不存在, 在从数据库中查询不到后将恶意数据缓存在Redis中

代码实现如下, 当在数据库没有查询到后, 将空信("")息存入到Redis中,并设置过期时间为2分钟, 当用户再次查询时, 校验如果为("") 直接返回 店铺信息不存在!

    @Override
    public Result queryShopById(Long id) {

        //1.从Redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop" + id);
        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在, 直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //判断命中的是否是空值
        if (Objects.equals(shopJson, "")) {
            //返回错误信息
            return Result.fail("店铺信息不存在!");
        }

        //4.不存在, 根据id查询数据库
        Shop shop = getById(id);
        //不存在, 返回错误提示信息
        if (shop == null){
            //将空值写入Redis中 并将有效期时间改为2分钟
            stringRedisTemplate.opsForValue().set("cache:shop" + id, "", 2L, TimeUnit.MINUTES);
            //返回错误信息
            return Result.fail("店铺不存在!");
        }
        //存在, 写入redis中 设置过期时间为30分钟
        stringRedisTemplate.opsForValue().set("cache:shop" + id, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);

        return Result.ok(shop);
    }

🍣 缓存雪崩

Redis的缓存雪崩意思是指: 在统一同一时间内Redis中的大量的Key失效, 导致请求压力到达数据库

解决办法 : 缓存数据的过期时间设置随机,将不同的Key的TTL设置随机值

🍕 缓存击穿

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

🍥 解决方案1 : 互斥锁

锁代码实现(获取锁, 释放锁)

    /**
     * 获取锁
     *
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        //相当于 SETNX:添加一个String类型的键值对,当key不存在的时候执行
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        // BooleanUtil可以帮你自动拆装箱解决可能空指针问题
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     *
     * @param key
     */
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

业务代码实现

这里的互斥锁如果获取不到锁就会进入休眠状态, 然后再去重新获取锁, 这样做能保持数据的一致性

 /**
     * 缓存击穿
     */
    public Shop queryWithmutex(Long id){

        //1.从Redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop" + id);
        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在, 直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //判断命中的是否是空值
        if (Objects.equals(shopJson, "")) {
            //返回错误信息
            log.error("店铺信息不存在");
            return null;
        }

        //4实现缓存重建
        //4.1 获取互斥锁
        String lockKey = "lock:shop" + id;
        Shop shop;
        try {
            //这块功能的业务是, 当A线程操作该方法是, B线程进来判断锁是否释放, 如果没有释放则休眠重试, 为了解决数据一致性的问题
            boolean isLock = tryLock(lockKey);
            //4.2判断锁是否获取成功
            if (!isLock){
                //4.3 失败,则休眠重试
                Thread.sleep(50);
                //递归重试(这块地方有异议 不建议递归, 后期用到类似业务可以寻找其他解决办法)
                return queryWithmutex(id);
            }

            //根据id查询数据库
            shop = getById(id);
            if (shop == null) {
                //不存在 将空值写入Redis中 并将有效期时间改为2分钟 (这里是为了解决缓存穿透问题)
                stringRedisTemplate.opsForValue().set("cache:shop" + id, "", 2L, TimeUnit.MINUTES);
                //返回错误信息
                log.error("店铺信息不存在");
                return null;
            }
            //存在, 写入redis中 设置过期时间为30分钟
            stringRedisTemplate.opsForValue().set("cache:shop" + id, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);
        }catch (InterruptedException e){
            throw new RuntimeException(e);
        }finally {
            //7. 释放互斥锁
            unlock(lockKey);
        }
        return (shop);
    }

解决方案2 : 逻辑过期

设置逻辑过期数据和过期时间

    /**
     * 向Redis中写入店铺信息并设置逻辑过期时间
     * @param id
     * @param expireSeconds
     */
    public 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" + id, JSONUtil.toJsonStr(redisData));
    }

手动加入逻辑过期数据

    @Test
    void test(){
        //模拟后台管理手动设置热点数据
        shopService.saveShop2Redis(1L, 10L);
    }

 逻辑过期业务代码

    /**
     * 逻辑过期
     * @param id
     * @return
     */
    public Shop queryWithLogicalExpire(Long id) {

        //1.从Redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop" + id);
        //2.判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            //3.不存在, 直接返回
            return null;
        }

        //4. 命中, 需要把JSON反序列化为对象
        log.info("打桩数据 : {}", shopJson);
        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" + id;
        boolean isLock = tryLock(lockKey);
        //6.2判断锁是否获取成功
        if (isLock) {
            //TODO 6.3成功, 开启独立线程, 实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    //重建缓存
                    log.info("开始缓存重建");
                    this.saveShop2Redis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    log.info("释放锁");
                    unlock(lockKey);
                }
            });
        }
        //6.4 返回过期的商铺信息
        return shop;
    }

这里实现的互斥锁, 如果没有拿到锁就会直接return返回历史数据, 在并发环境下短期内会造成数据的不一致性

比如我修改了name属性字段

 


👾 项目源码下载

扫描下方公众号二维码 回复: Redis缓存实战 即可领取项目源码 👇👇👇

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

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

相关文章

用visa进行仪表通信

第二份工作一直跟仪表打交道,用仪表来测试,当然主要还是搞自动化。仪表连接通信当然需要用到visa啦。 一.NI VISA的安装和TCP/IP配置 这里看到有人对于安装NI VISA介绍的很清楚,具体想了解就看他的吧。 NI VISA安装和TCP/IP配置 二.安装Py…

第6部分 单区域OSPF

目录 6.1 OSPF 概述 6.2 实验1:点到点链路上的OSPF 1.实验目的 2.实验拓扑 3.实验步骤 4.实验调试 (1)show ip route (2)show ip ospf neighbor 6.3 OSPF 命令汇总 OSPF(Open Shortest Path First&#xff0c…

Java实现 LeetCode 500.键盘行

500.键盘行 给你一个字符串数组 words ,只返回可以使用在 美式键盘 同一行的字母打印出来的单词。键盘如下图所示。 美式键盘 中: 第一行由字符 “qwertyuiop” 组成。第二行由字符 “asdfghjkl” 组成。第三行由字符 “zxcvbnm” 组成。 示例 1&…

【mysql是怎样运行的】-mysql字符集

文章目录1. 服务器级别2. 数据库级别3. 表级别4. 列级别MySQL 有 4 个级别的字符集和比较规则,分别是 服务器级别、数据库级别、表级别、列级别.1. 服务器级别 SHOW VARIABLES like character_set_server SHOW VARIABLES like collation_server在启动服务器程序时&a…

Vue响应式系统的作用与实现(一)

响应式系统的作用与实现 0.写在前面: 写了mini-vue之后的疑惑更多了,比如为什么要这样设计?这样做的好处是啥?为什么我想不出来?(我真菜于是决定去看霍春阳大佬的Vue.js设计与实现。一些参考资料&#xf…

x265 帧间预测

帧间编码入口函数: 从 Analysis::compressCTU 是ctu编码的入口函数,根据 slice 类型判断是 I 还是 BP,如果是BP则执行帧间编码函数 Analysis::compressInterCU_rdx_x::/*压缩分析CTU过程:1.为当前CTU加载QP…

m基于16QAM的自适应波束形成matlab仿真

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 16QAM全称正交幅度调制是英文Quadrature Amplitude Modulation的缩略语简称,意思是正交幅度调制,是一种数字调制方式。产生的方法有正交调幅法和复合相移法。 波束形成是…

工业级数据分发服务DDS之安全篇

目录引出问题分析问题未授权订阅者未授权发布者截胡篡改跨域攻击解决问题官方标准DDS的安全特性基于域的安全保护域内保护RTI方案RTI安全插件的特性DDS支持的加解密算法用于数据流保护的密码算法用于密钥交换的密码算法用于数字签名的密码算法RTPS-HMAC-Only插件用于数据流保护…

数据结构(10)图的概念、存储

目录 10.1.概念 10.2.存储 10.2.1.邻接矩阵 10.2.2.邻接表 10.1.概念 定义: 图,用来表示多对多的关系,比如地图里城市之间的通路、比如人际关系。 图由顶点和边组成,顶点是图里的每个结点,边是顶点之间的通路,可…

【计算机网络】网络基础(三)

自从计算机、手机被广泛应用于工作、生活、娱乐、学习,那你有没有考虑过QQ的消息、爱奇艺的视频、钉钉的网络会议的数据是如何传输的?这些信息自然是通过网络(WIFI、蜂窝网络等)传播的,而一个简单的通信网络是由路由器…

513.找树左下角的值

文章目录513.找树左下角的值题目题解 - BFS题解- DFS513.找树左下角的值 题目 给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至少有一个节点。 示例 1: 输入: root [2,1,3] 输出: 1示例 2: 输入: [1,2,3,4,null,5,…

计算机网络学习笔记(II)——应用层(二)

2.4、Email 电子邮件(Email) 主要由三个部分组成: 用户代理邮件服务器简单邮件传输协议:SMTP 用户代理(邮件阅读器): 撰写、编辑和阅读邮件输入和输出邮件保存在服务器上 EMail&#xff1…

UDS入门至精通系列:Service 27

文章目录 前言一、Service 27的功能二、企业规范中怎么定义Service 27三、AUTOSAR关于Service 27实现策略四、关于Service 27集成测试五、用于解锁的dll文件怎样得来?总结前言 本文将近8000字,详细分享了从需求规范提出、功能实现、集成测试等方面对Service 27的介绍! 在引…

PTA编程的一些总结

PTA 1.首先是float单精度浮点数和double双精度浮点数 float占4字节,有效数字7位,double占8字节,有效数字15位 double类型输入16位数字会有误差 老师给的PTA中的练习题中第一题的买U盘需要在结果后面0.000001来弥补double精度 2.判断浮点数…

从ASM看jacoco运行原理

前言 我们在开发中如何保证代码质量,我的回答是做充分的代码测试。Jacoco的出发点是为基于JVM运行的代码提供代码覆盖率统计,期望提供轻量级的、可伸缩的、文档较全的库文件来集成各类构建和开发工具。 ASM介绍 ASM 是一个通用的 Java 字节码操作和分…

架构设计(消息队列)

架构设计(消息队列) 消息队列 发送者将消息发送到topic,消费者从topic中拉取消息进行消费 发送端消息发送方式 同步发送:消息发送后,需要等待消息发送响应结果,发送失败可重试 异步发送:消息发…

Numpy入门[17]——数组广播机制

Numpy入门[17]——数组广播机制 参考: https://ailearning.apachecn.org/NumPy广播机制 使用Jupyter进行练习 NumPy 中的广播机制(Broadcast)旨在解决不同形状数组之间的算术运算问题。我们知道,如果进行运算的两个数组形状完全相…

linux网络编程epoll详解

目录epoll原理解析epoll提供的接口epoll的触发模式epoll原理解析 从socket接收网络数据说起: 1、网络传输中,网卡会把接收到的数据写入内存,网卡向 CPU 发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断…

第二证券|行业重磅白皮书发布,超高清视频产业规模剑指3万亿

在5G和超高清交融开展的布景下,下流使用需求有望迸发,超高清视频工业前景可观。 超高清工业规模有望突破3万亿 据报道,12月1日,2022国际显现工业大会分论坛——新式显现超高清主题论坛在成都举行。论坛上,中国电子信息…

文本编辑器vi--常用命令查阅版(记得收藏)

一.为何要学习vi   # 所有的UNIX-like系统都会内置vi文本编辑器,其他的文本编辑器则不一定会存在;   # 很多软件的编辑接口都会主动调用vi;   # vim具有程序编辑的能力,可以主动地以字体颜色辨别语法的正确性,方…