【黑马点评Redis——002商户查询缓存】

news2024/11/17 0:03:48

1. 商户查询缓存

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

2. 知识储备和课程内容

2.1 什么是缓存

缓存是数据交换的缓冲区,是存贮数据的临时地方,一般读写性能较高。

  • 浏览器缓存
  • 应用层缓存
  • 数据库缓存
  • CPU缓存
  • 磁盘缓存

缓存的作用

  • 降低后端负载
  • 提高读写效率,降低响应时间

缓存的成本

  • 数据的一致性成本
  • 代码维护成本
  • 运维成本

2.2 缓存更新策略

在这里插入图片描述
业务查询:

  • 低一致性需求:使用内存淘汰策略。例如店铺类型的查询缓存
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存。

2.2.1 主动更新策略

Cache Aside Pattern(旁路缓存模式)(企业中用的比较多)
  • Cache Aside Pattern
    • 指缓存调用者在更新数据库的同时完成对缓存的更新。(一致性良好,实现难度一般)
  • Read/Write Through Pattern
    • 缓存和数据库集成为一个服务,由服务来保证两者的一致性,对外暴露API接口。调用者调用API,无序知道自己操作的是数据库还是缓存,不关心一致性。(一致性优秀,实现复杂,性能一般)
  • Write Behind Caching Pattern
    • 缓存调用者的CRUD都针对缓存完成,由独立线程异步的将缓存数据写到数据库,实现最终一致(一致性差,性能好,实现复杂)

Cache Aside Pattern基本思想

  1. 当需要获取数据时,首先在缓存中查找数据。
  2. 如果在缓存中找到了数据,则直接返回给客户端。
  3. 如果在缓存中没有找到数据,则从后端存储系统(如数据库)中读取数据,并将数据存储到缓存中。
  4. 在写入数据时,首先更新后端存储系统中的数据,然后让缓存中的数据失效或更新,以便下次读取时从后端存储系统中获取最新数据。

特点

  • 简单直观:模式简单易懂,易于实现。
  • 读性能提升:大部分读操作可以直接从缓存中获取数据,减少了对后端存储系统的访问。
  • 数据一致性:通过手动管理缓存和后端存储系统中的数据一致性,确保数据的准确性。

需要思考的问题!

  • 删除缓存还是更新缓存?
    • 更新缓存:每次更新数据库都更新缓存,无效写操作较多(❎)
    • 删除操作:更新数据库时让缓存失效,查询时再更新缓存(✅)
  • 如何保证缓存与数据库的操作同时成功或失败?
    • 单体系统,将缓存与数据库操作放在一个事务
    • 分布式系统,利用TCC等分布式事务方案
  • 先操作缓存还是先操作数据库?
    • 先删除缓存,在操作数据库
    • 先操作数据库,在删除缓存
先操作缓存还是先操作数据库(重要)?

在这里插入图片描述
相比较而言方案二安全性更高一些
原因:方案二需要满足,线程1查询时缓存恰好失效,且更新数据库的操作间隔要比写入缓存的时间短。(但还是有可能),需要赋予超时剔除作为兜底方案。

2.3 缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这些请求都会打到数据库,给数据库带来巨大的压力。
在这里插入图片描述
在这里插入图片描述

解决方案

  • 缓存空对象
  • 布隆过滤
  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

2.4 缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存
    在这里插入图片描述

2.5 缓存击穿

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

2.5.1 解决方案1:互斥锁

在这里插入图片描述
存在的问题:需要等待阻塞

在这里插入图片描述
利用setnx来模拟简单的分布式锁。

# 获得锁(一般上会设置有效期)
setnx lock 1
# 删除锁
del lock

2.5.2 解决方案2:逻辑过期

在这里插入图片描述
在这里插入图片描述
基于逻辑过期的方式会存在一段时间内的不一致性,一旦线程完成了缓存重建,就能够得到一致性的结果。

2.5.3 解决方案对比

在这里插入图片描述

2.6 缓存工具封装

基于StringRedisTemplate封装一个缓存工具类,满足下列需求:

  • 方法1: 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
  • 方法2: 将任意java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
  • 方法3: 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
  • 方法4: 根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }

    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
        // 设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        // 写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit){
        String key = keyPrefix + id;
        // 1. 从Redis中查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2. 判断是否存在
        if (StrUtil.isNotBlank(json)){
            // 3. 存在,直接返回
            return JSONUtil.toBean(json,type);
        }
        // 判断命中的是否是空值
        if (json!=null){
            return null;
        }
        // 4. 不存在,根据id查询数据库
        R r = dbFallback.apply(id);
        // 5. 数据库不存在,返回错误
        if (r==null){
            stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 6. 存在,写入Redis
        this.set(key,r,time,unit);
        // 7. 返回
        return r;
    }

    public <R,ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit){
        String key = keyPrefix + id;
        // 1. 从Redis中查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2. 判断是否存在
        if (StrUtil.isBlank(json)){
            // 3. 不存在直接返回
            return null;
        }
        // 4.命中,需要先把json反序列化对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5. 判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            // 5.1 未过期,直接返回店铺信息
            return r;
        }
        // 5.2 已过期,需要缓存重建
        // 6 缓存重建
        // 6.1 获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        // 6.2 判断是否获取锁成功
        if(isLock){
            // 6.3 成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                // TODO 重建缓存,需要修改过期时间为1800秒
                try {
                    // 查询数据库
                    R r1 = dbFallback.apply(id);
                    // 写入redis
                    this.setWithLogicalExpire(key,r1,time,unit);
                }catch (Exception e){
                    throw new RuntimeException(e);
                }finally {
                    unlock(lockKey);
                }});
        }
        // 6.4 先返回过期的商铺信息
        return r;
    }

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

3. 问题汇总

3.1 基于互斥锁的递归是否存在问题?

递归调用 queryWithMutex(id) 可能会导致栈溢出,因为没有任何条件来终止递归。在这种情况下,如果无法获取锁,线程会无限制地尝试递归调用自身,并且每次递归都会消耗一些栈空间,最终导致栈溢出异常。

    public Shop queryWithMutex(Long id){
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        // 1. 从Redis中查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2. 判断是否存在
        if (StrUtil.isNotBlank(shopJson)){
            // 3. 存在,直接返回
            return JSONUtil.toBean(shopJson,Shop.class);
        }
        // 判断命中的是否是空值
        if (shopJson!=null){
            return null;
        }
        // 4. 实现缓存重建
        // 4.1 获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2 判断是否获取成功
            if (!isLock){
                // 4.3 失败,则休眠并重试
                Thread.sleep(50);
                // TODO 感觉这里代码有问题。建议重新修改
                return queryWithMutex(id);
            }
            // 4.4 成功,根据id查询数据库
            shop = getById(id);
            // TODO 模拟重建的延时(正常运行时需要删除)
            Thread.sleep(200);
            // 5. 数据库不存在,返回错误
            if (shop==null){
                stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            // 6. 存在,写入Redis
            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        }catch (Exception e){
            throw new RuntimeException(e);
        }finally {
            // 7.释放获取锁
            unlock(lockKey);
        }
        // 8. 返回
        return shop;
    }

修改后的代码如下:采用了循环替代了递归,并设置了最大循环次数。达到最大循环后没有成功即返回null.在并发为200/s的时候平均每个请求需要在循环中执行的次数为7次。
在这里插入图片描述

    public Shop queryWithMutex2(Long id){
        String key = RedisConstants.CACHE_SHOP_KEY + id;

        // 从Redis中查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shopJson)){
            return JSONUtil.toBean(shopJson,Shop.class);
        }
        // 判断命中的是否是空值,即是否等于空字符串
        if (shopJson!=null){
            return null;
        }

        // 尝试准备从数据库中获取数据
        int MAX_RETRY_COUNT = 10;
        boolean isLock = false;
        int retryCount = 0;
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        Shop shop = null;
        try{
            // 4.2 循环重试直至获取锁成功或达到最大重试次数
            while (!isLock && retryCount < MAX_RETRY_COUNT) {
                isLock = tryLock(lockKey);
                if (!isLock) {
                    // 4.3 失败,则休眠并重试
                    Thread.sleep(50);
                    retryCount++;
                }
                //休眠结束后尝试从缓存中查询数据
                shopJson = stringRedisTemplate.opsForValue().get(key);
                if (StrUtil.isNotBlank(shopJson)){
                    System.out.println("Thread尝试的次数为:"+retryCount);
                    return JSONUtil.toBean(shopJson,Shop.class);
                }
                // 否者继续循环获得锁
            }
            // 判断是否获取锁成功,或者超过最大重试次数
            if (!isLock || retryCount == MAX_RETRY_COUNT){
                return null;
            }
            // 获取锁成功,根据id查询数据库
            shop = getById(id);
            // TODO 模拟重建的延时(正常运行时需要删除)
            Thread.sleep(200);
            // 5. 数据库不存在,返回错误
            if (shop==null){
                stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            // 6. 存在,写入Redis
            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        }catch (Exception e){
            throw new RuntimeException(e);
        }finally {
            // 7.释放获取锁
            unlock(lockKey);
        }
        // 8. 返回
        return shop;
    }

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

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

相关文章

计网笔记:第1章 计算机网络概论

计网笔记&#xff1a;第1章 计算机网络概论 第1章 计算机网络概论1.1 计算机网络发展与分类1.2 OSI和TCP/IP参考模型OSI与TCP/IP参考模型图 1.3 数据封装与解封过程借助OSI模型理解数据传输过程(封装)借助OSI模型理解数据传输过程(解封) 1.4 本章例题 第1章 计算机网络概论 1.…

喜讯!和鲸科技入选 “算力中关村”—— 2024 算力技术创新与应用服务案例集

2024 年 4 月 27 日上午&#xff0c;以"技术重塑生态 算力驱动未来"为主题的"算力中关村"技术成果对接交流专场活动在北京成功举办。作为 2024 中关村论坛的重要组成部分&#xff0c;该专场活动旨在为各界人士打造一个专业化、高效化的技术成果展示与对接平…

SpringMVC 源码剖析

SpringMVC 源码剖析 0 从源码角度分析SpringMVC执行流程 // 前端控制器&#xff0c;SpringMVC最核心的类 public class DispatcherServlet extends FrameworkServlet {// 前端控制器最核心的方法&#xff0c;这个方法是负责处理请求的&#xff0c;一次请求&#xff0c;调用一次…

Redis入门到通关之数据结构解析-IntSet

文章目录 概述IntSet升级简易源码总结 欢迎来到 请回答1024 的博客 &#x1f34e;&#x1f34e;&#x1f34e;欢迎来到 请回答1024的博客 关于博主&#xff1a; 我是 请回答1024&#xff0c;一个追求数学与计算的边界、时间与空间的平衡&#xff0c;0与1的延伸的后端开发者。 …

详细谈电脑ip、域名、内网、外网、localhost、127.0.0.1、网关等通讯基础知识(易懂)

1. ip地址与域名的定义以及其关系 ip地址的定义&#xff1a; IP地址&#xff08;Internet Protocol Address&#xff09;是指互联网协议地址&#xff0c;又译为网际协议地址。 IP地址是IP协议提供的一种统一的地址格式&#xff0c;它为互联网上的每一个网络和每一台主机分配一…

【ARM 裸机】BSP 工程管理

回顾一下上一节&#xff1a;【ARM 裸机】NXP 官方 SDK 使用&#xff0c;我们发现工程文件夹里面各种文件非常凌乱&#xff1b; 那么为了模块化整理代码&#xff0c;使得同一个属性的文件存放在同一个目录里面&#xff0c;所以学习 BSP 工程管理非常有必要。 1、准备工作 新建…

短视频账号矩阵系统===4年技术源头打磨

短视频矩阵系统技术源头打磨需要从多个方面入手&#xff0c;以下是一些建议&#xff1a; 1. 基础技术研发&#xff1a;不断投入资金和人力进行基础技术研发&#xff0c;包括但不限于视频处理、人工智能、大数据等技术&#xff0c;以提高短视频矩阵系统的性能和稳定性。 2. 优化…

多进程编程:原理、技术与应用

title: 多进程编程&#xff1a;原理、技术与应用 date: 2024/4/26 12:14:47 updated: 2024/4/26 12:14:47 categories: 后端开发 tags: 多进程并发编程网络服务分布式系统任务处理进程池线程对比 第一章&#xff1a;进程与线程 进程与线程的概念及区别&#xff1a; 进程&am…

四信智能化感知与控制方案,助推灌区续建配套与现代化改造建设

“十四五”明确提到推进大中型灌区节水改造和精细化管理&#xff0c;建设节水灌溉骨干工程&#xff0c;同步推进水价综合改革。 灌区是保障国家粮食安全的重要基础性设施&#xff0c;是实施乡村振兴战略的水利支撑。灌区续建配套与现代化改造是实施乡村振兴战略一项重要任务。为…

el-tab面板添加折叠按钮方法

折叠后 <template><div class"page-type-left-wrap"><div class"page-type-left-wrap-info nav-link" :class"{ leftCollapse }"><el-tabs v-model"activeName" class"page-tabs" tab-change"han…

Git系列:Refs与Reflog

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

【经典算法】LeetCode104二叉树的最大深度(Java/C/Python3实现含注释说明,Easy)

目录 题目描述思路及实现方式一&#xff1a;递归思路代码实现Java版本C语言版本Python3版本Go语言版本 复杂度分析 方式二&#xff1a;广度优先搜索(BFS)思路代码实现Java版本C语言版本Python3版本 复杂度分析 总结相似题目 标签(题目类型)&#xff1a;树、深度优先搜索(DFS)、…

B站无限评论暴力截留协议及教程

B站无限评论暴力截留协议及教程 B站无限评论暴力截留协议及教程&#xff0c;需要抓CK &#xff0c;教程里面有讲如何抓取 网盘自动获取 链接&#xff1a;https://pan.baidu.com/s/1lpzKPim76qettahxvxtjaQ?pwd0b8x 提取码&#xff1a;0b8x

PHP 错误 Unparenthesized `a ? b : c ? d : e` is not supported

最近在一个新的服务器上测试一些老代码的时候得到了类似上面的错误&#xff1a; [Thu Apr 25 07:37:34.139768 2024] [php:error] [pid 691410] [client 192.168.1.229:57183] PHP Fatal error: Unparenthesized a ? b : c ? d : e is not supported. Use either (a ? b : …

DeblurGAN-v2: Deblurring (Orders-of-Magnitude) Faster and Better

文章目录 摘要1、引言2、相关工作2.1、图像去模糊2.2、生成对抗网络 3、DeblurGAN-v2 架构3.1、特征金字塔去模糊3.2、骨干网络的选择&#xff1a;性能与效率之间的权衡3.3、双尺度RaGAN-LS判别器3.4、训练数据集 4、实验评估4.1、实现细节4.2、在GoPro数据集上的定量评估4.3、…

服务器数据恢复—存储硬盘坏道,指示灯亮黄色的数据恢复案例

服务器数据恢复环境&故障&#xff1a; 一台某品牌EqualLogic PS系列某型号存储&#xff0c;存储中有一组由16块SAS硬盘组建的RAID5磁盘阵列&#xff0c;RAID5上划分VMFS文件系统存放虚拟机文件。存储系统上层一共分了4个卷。 raid5阵列中磁盘出现故障&#xff0c;有2块硬盘…

5款文案生成器,帮你智能写作优秀文案

在当今数字化时代&#xff0c;文案写作是营销和传播领域中至关重要的一环。然而&#xff0c;对于许多人来说&#xff0c;撰写引人注目且有吸引力的文案可能是一项具有挑战性的任务。这就是为什么文案生成器变得如此受欢迎的原因。通过结合人工智能和自然语言处理技术&#xff0…

C++之STL-String

目录 一、STL简介 1.1 什么是STL 1.2 STL的版本 1.3 STL的六大组件 ​编辑 1.4 STL的重要性 二、String类 2.1 Sting类的简介 2.2 string之构造函数 2.3 string类对象的容量操作 2.3.1 size() 2.3.2 length() 2.3.3 capacity() 2.3.4 empty() 2.3.5 clear() 2.3.6…

JDBC查询大数据时怎么防止内存溢出-流式查询

文章目录 1.前言2.流式查询介绍3.使用流式查询3.1不开启流式查询的内存占用情况3.2开启流式查询的内存占用情况 4.开启流式查询的注意点 1.前言 在使用 JDBC 查询大数据时&#xff0c;由于 JDBC 默认将整个结果集加载到内存中&#xff0c;当查询结果集过大时&#xff0c;很容易…

【项目】YOLOv8/YOLOv5/YOLOv9半监督ssod火灾烟雾检测(YOLOv8_ssod)

假期闲来无事找到一份火灾烟雾数据集&#xff0c;自己又补充标注了一些&#xff0c;通过论文检索发现现在的火灾检测工作主要局限于对新场景的泛化性不够强&#xff0c;所以想着用半监督&#xff0c;扩充数据集的方法解决这个问题&#xff0c;所以本文结合使用现在检测精度较高…