Redis缓存数据 | 黑马点评

news2024/11/13 8:50:23

目录

一、什么是缓存

二、添加Redis缓存操作

三、缓存更新策略

缓存的更新策略

​编辑

业务场景

主动更新策略

案例

四、缓存穿透

1、是什么

2、解决方案

(1)缓存空对象

(2)布隆过滤器 

(3)其他策略

3、实践

五、缓存雪崩

1、是什么

2、解决方案

六、缓存击穿

1、是什么

2、解决方案

​编辑

互斥锁

逻辑过期

3、实践-互斥锁

用什么锁

实现代码

4、实践-逻辑删除

七、缓存工具封装


一、什么是缓存

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

缓存的作用:降低后端负载,提高读写效率,降低响应时间

缓存的成本:数据一致性成本、代码维护成本、运维成本

二、添加Redis缓存操作

三、缓存更新策略

缓存的更新策略

业务场景

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

主动更新策略

操作缓存和数据库时要考虑3个问题:

1、删除缓存还是更新缓存?

更新缓存:每次更新数据库都更新缓存,无效写操作很多但是一直没查就很亏

删除操作:更新数据库的时删除缓存,下次查询才有(推荐)

2、如何保证缓存与数据库操作同时成功或失败?

单体系统:将缓存和数据库操作放到一个事务里面

分布式系统:利用tcc等分布式事务方案

3、先操作缓存还是先操作数据库?(重点)(面试题)

先删除缓存:a在删除完缓存后,正在改数据库,这时候来了线程查询数据库,然后更新到缓存,这时候如果a更新完数据库,缓存和数据库就是不一致了

先改数据库:如果缓存过期失效了,a在去数据库查的过程中,b改了数据库,然后更新缓存,这时候a查到数据再更新缓存。缓存和数据库就不一致了。但是这种情况发送的概率远远小于前者。

因为后面的情况发生概率小,要满足很多条件,所以推荐用后者。推荐先改数据库再改缓存

案例

我们要加上事务注解,保证他们同时成功或同时失败,删之前也要判断一下看id是不是空

四、缓存穿透

1、是什么

缓存穿透是指客户端请求的数据在缓存和数据库都没有,这样缓存永远不会生效,这些请求都会打到数据库。

2、解决方案

(1)缓存空对象

第一次他随便乱打个没有的id来查询缓存和数据库,我们就会返回null,然后把null也存到缓存中。

这样再他下一次带这个id来请求的时候,就会从缓存中拿到null。

优点:简单实现,维护方便

缺点:

额外的内存消耗,缓存了很多没用的数据(可以通过设置时间来缓解)

可能造成短期的不一致。因为是随便乱打的id发的请求,我们帮这个id存缓存了,以后要是真的有这个id了,到时候拿这个id来查也是null(也是控制缓存的时间来缓解,也可以当我们新增数据的时候,主动插入缓存替换掉null)

(2)布隆过滤器 

在请求到缓存前加层布隆过滤器,如果这个数据存不存在,直接拒绝,不给继续请求。

布隆过滤器是怎么知道在不在呢?

是通过hash算法算出哈希值,将这些哈希值换成2进制位保存到布隆过滤器,判断数据是否存在的时候就判断对应的位置是0还是1。这种统计不是百分百准确

不存在的时候百分百不存在,说存在不一定存在,可以起到一定过滤作用。还是有一定击穿风险

优点:内存占用少,没有存多余的key

缺点:实现复杂,存在误判的可能。

(3)其他策略

  • 做好数据的基础格式校验
  • 增强id复杂度,避免被猜测id的规律
  • 加强用户权限校验
  • 做好热点参数的限流

3、实践

以缓存null值为例,在查询的接口做更改:

当我们查到数据不存在的时候,将这个id作为key,value为空值存储到redis中。

在判断完商品是否存在后加上判断查出来的是否为空值,为空就返回不存在

五、缓存雪崩

1、是什么

缓存雪崩是指同一时段大量的缓存key同时失效或redis服务宕机,导致大量请求打崩数据库

2、解决方案

给不同的key的TTL(过期时间)添加随机值(可以设置成30分钟到40分钟之间的随机数)

利用Redis集群提高服务器的可用性(哨兵机制)

给缓存业务添加降级限流策略(比如当服务器出现问题的时候,拒绝服务,牺牲部分服务来保证安全)

给业务添加多级缓存(nginx、redis、jvm都可以添加缓存,最后才到数据库)

六、缓存击穿

1、是什么

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

高并发访问可以理解为做活动的商品,同一时间有无数的请求来访问这个商品。

缓存重建比较复杂就是缓存可能会过期失效,失效的时候重新添加到缓存的时间很久(可能业务非常复杂,要多表查询运算才能得到的结果)

2、解决方案

互斥锁

如果查到缓存中没有,就加锁查数据库冲击,写入缓存之后再释放锁。这个期间内,所有访问的请求都没有拿到锁只能等待重试,直到缓存更新完毕读取缓存。

优点:没有额外的内存消耗、保证缓存和数据库一致性,实现简单

缺点:线程需要等待,性能受影响用户体验不好。可能有死锁的风险

逻辑过期

设置缓存过期时间的时候,不是真正的设置,而是设置在value里面,如果查询缓存发现逻辑删除时间过期了,就new一个新的线程加锁来查询数据库更新缓存,更新完毕后才释放锁,自己返回旧数据。这个期间内其他线程都没有拿到锁,都返回旧数据,直到更新完毕

优点:线程不用等待,性能好。

缺点:不保证一致性,有额外内存消耗,实现复杂。

3、实践-互斥锁

用什么锁

我们平时用synchronized和lock锁的时候,如果没有拿到锁就要等待,而我们这次是没有拿到锁和拿到锁都有不同的操作要执行。所有用不了这两个

我们可以用redis里面String数据类型的setnx命令来设置锁(当key不存在的时候才可以赋值,存在就不能赋值了)

加锁:setnx lock 1

释放锁:del lock

为了避免死锁,我们通常还会设置上有效期

加锁和释放锁的方法:

实现代码

 最后开启JMeter做测试,模拟1000个线程发请求,发现能扛住200qps

4、实践-逻辑删除

要用逻辑删除肯定要多个属性过期时间,我们就可以采用这种方式多增加一个类,这个类有过期时间属性,然后多一个object类,存放要存入redis的数据,这样就不用在原来的类上改动

编写添加到redis的方法

这个plusSeconds就是设置过期时间,用户参数传进来

重写查询的方法

七、缓存工具封装

我们发现缓存的操作还是挺复杂的,我们将来使用的时候不可能每次都这样重新写一遍。 我们可以将这些解决方案封装成工具。

我们封装四个方法,1和3可以是平时添加缓存用的,2和4一般是缓存热点数据用防止击穿问题

1、封装存储方法:将任意java对象序列化为json并存储在string类型的key中,并且设置ttl过期时间

2、封装存储方法(逻辑过期):将任意java对象序列化为json并存储在string类型的key中,并且设置逻辑过期时间,用于处理缓存击穿问题

3、查询缓存(空值解决击穿):根据指定key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透

4、查询缓存(逻辑删除解决穿透):根据指定key查询缓存,并反序列化为指定类型,需要用逻辑过期解决缓存击穿

工具类:

@Slf4j
@Component
public class CacheClient {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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


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

    /**
     * 向缓存中添加 key
     * */
    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)));
        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、根据 Id 查询 Redis
        String json = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        // 2、判断 shopJSON 是否为空
        if (StrUtil.isNotBlank(json)) {
            // 3、存在,直接返回
            R r = JSONUtil.toBean(json, type);
            return r;
        }

        // 增加对空字符串的判断
        if(json != null){
            return null;
        }

        // 4、不存在,查询数据库
        R r = dbFallback.apply(id);

        // 5、不存在,返回错误
        if(r == null){
            // 店铺不存在时,缓存空值
            stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        // 6、存在,写入 Redis
        this.set(key, r, time, unit);
        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、根据 Id 查询 Redis
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2、判断 shopJSON 是否为空
        if (StrUtil.isBlank(json)) {
            // 3、存在,直接返回
            return null;
        }

        // 命中,需要先把 json 反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);

        // 判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            return r;
        }

        // 已过期,需要缓存重建
        // 获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;

        boolean isLock = tryLock(lockKey);
        if (isLock) {
            // 开辟独立线程
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 缓存重建
                    R r1 = dbFallback.apply(id);
                    this.setWithLogicalExpire(key, r1, time, unit);
                }catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unLock(lockKey);
                }
            });
        }

        // 6、存在,写入 Redis
        this.set(key, r, time, unit);
        return r;
    }



    // 获取锁
    private boolean tryLock(String key){
        Boolean isTrue = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(isTrue);
    }

    // 释放锁
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

}

调用的service

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private CacheClient cacheClient;

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

    @Override
    public Result queryById(Long id) {
        // 解决缓存穿透
//        Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        // 互斥锁解决缓存击穿
//        Shop shop = queryWithMetux(id);

        // 使用逻辑过期时间解决缓存击穿问题
//        Shop shop = queryWithLogicalExpire(id);
        Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        if(shop == null){
            return Result.fail("店铺不存在!");
        }

        // 7、返回
        return Result.ok(shop);
    }

    public Shop queryWithLogicalExpire(Long id){
        // 1、根据 Id 查询 Redis
        String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        // 2、判断 shopJSON 是否为空
        if (StrUtil.isBlank(shopJson)) {
            // 3、存在,直接返回
            return null;
        }

        // 命中,需要先把 json 反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);

        // 判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            return shop;
        }

        // 已过期,需要缓存重建
        // 获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;

        boolean isLock = tryLock(lockKey);
        if (isLock) {
            // 开辟独立线程
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                    try {
                        // 缓存重建
                        this.saveShop2Redis(id, 20L);
                    }catch (Exception e) {
                        throw new RuntimeException(e);
                    } finally {
                        unLock(lockKey);
                    }
            });
        }

        // 6、存在,写入 Redis
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        return shop;
    }

}

 

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

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

相关文章

【春节安全保障有我们】安全狗春节放假值班通知

兔年纳福 辛勤拼搏了一年 终于迎来了福兔吉祥年 众人沉浸于准备过年的氛围中 却有些人为春节期间的网络安全担忧 因为春节也是不法分子们 伺机而动、“搞事情”的“好时机” 2023 NEW YEAR 不得不防的安全风险 1、主机安全遭受威胁 &#xff08;云&#xff09;主机系统…

Rust语言基础

安装 Rust 官网&#xff1a;https://www.rust-lang.org/Linux or Mac: curl https://rustup.rs -sSf | sh Windows: 按官网指示操作 Windows Subsystem for Linux: curl --proto ‘https’ --tlsv1.2 -sSf https://sh.rustup.rs | sh 查看是否安装成功 rustc --version 更…

【openEuler】x2openEuler工具使用

文章目录一、关于x2openEuler二、工具准备三、前期准备1、安装CentOS72、下载x2openEuler3、安装x2openEuler4、执行bash5、访问上述网站6、安装x2openEuler-client&#xff08;1&#xff09;在CentOS-7.6Evetything1上找到x2openEuler-client&#xff08;2&#xff09;把x2ope…

Android Studio 支持手机投屏电脑

有时当我们在线上做技术分享或者功能演示时&#xff0c;希望共享连接中的手机屏幕&#xff0c;此时我们会求助 ApowerMirror&#xff0c;LetsView&#xff0c;Vysor&#xff0c;Scrcpy 等工具。如果你是一个 Android Developer&#xff0c;那么现在你有了更好的选择。 Android…

蓝桥杯--快排+队列+尺取法

&#x1f603;这只松鼠如约而至 - 许嵩 - 单曲 - 网易云音乐 &#x1f603;你买菜吗玫瑰 - 要不要买菜 - 单曲 - 网易云音乐 &#x1f603;一起玩吧这世界那么多人&#xff08;电影《我要我们在一起》主题曲&#xff09; - 莫文蔚 - 单曲 - 网易云音乐 前言 这是我在CSD…

一文讲透单点登录架构思想(SSO)

目录什么是单点登录&#xff1f;非单点登录架构单点登录架构什么是CAS单点登录SSO演进1.同域2.同父域3.跨域CASCAS术语CAS场景单点登录优缺点优点缺点什么是单点登录&#xff1f; 单点登录(SingleSignOn&#xff0c;SSO)&#xff0c;就是通过用户的一次性鉴别登录。当用户在身份…

【数据结构与算法理论知识点】 4、树和二叉树

4、树和二叉树 逻辑结构 4.1、树的定义和基本术语 树是n个结点的有限集 树的其他表示方式 基本术语 根——即根结点&#xff08;没有前驱&#xff09; 叶子——即终端结点&#xff08;没有后继&#xff09; 森林——指m棵不相交的树的集合&#xff08;例如删除根节点A后的…

Apache Solr 9.1-(二)集群模式运行

Apache Solr 9.1-&#xff08;二&#xff09;集群模式运行 Solr是一个基于Apache Lucene的搜索服务器&#xff0c;Apache Lucene是开源的、基于Java的信息检索库&#xff0c;Solr能为用户提供无论在任何时候都可以根据用户的查询请求返回结果&#xff0c;它被设计为一个强大的文…

synchronized锁升级

假如 synchronized 是「王」身边的「大总管」&#xff0c;那么 Thread 就像是他后宫的王妃。「王」每日只能选择一个王妃陪伴&#xff0c;王妃们会想方设法争宠获得陪伴权&#xff0c;大总管需要通过一定的手段让王「翻牌」一个「王妃」与王相伴。 今日听「码哥」胡言乱语解开…

1. Linux 磁盘管理(分区、格式化、挂载)

目录 1. Linux 内核版与发行版 2. Linux中磁盘的管理(分区、格式化、挂载) 2.1 磁盘定义、分类和命名 2.2 分区的定义和划分 2.3 磁盘格式化(高级/逻辑格式化) 2.4 挂载操作 1. Linux 内核版与发行版 内核版&#xff1a;Linus Torvalds最初组织很多人完成的Linux操作系统只…

Ubuntu20.04下安装显卡驱动

环境配置 系统: Ubuntu 20.04 CPU: i5 GPU:Geforce 960M Ubuntu安装显卡驱动 1、查看当前显卡安装情况 使用glxinfo查看 https://dri.freedesktop.org/wiki/glxinfo/ $ glxinfo Command glxinfo not found, but can be installed with: sudo apt install mesa-utils需要安…

postgresql FDW概念、用法与原理小结

最近突然遇到了一批使用fdw的场景&#xff0c;整理记录一把。 一、 强大的FDW FDW (foreign-data wrapper&#xff0c;外部数据包装器)&#xff0c;可以让我们在PG中使用SQL查询极为丰富的外部数据&#xff1a; 本实例和其他pg实例中的pg库主流关系型数据库&#xff1a;Oracle…

装饰模式(decorator-pattern)

装饰模式(decorator-pattern) 文章目录装饰模式(decorator-pattern)一、手抓饼点餐系统二、要求进阶三、装饰模式概要四、装饰模式的优劣及应用场景1. 优点2.缺点3.应用场景一、手抓饼点餐系统 请设计一个手抓饼点餐系统&#xff0c;支持加配菜&#xff0c;比如里脊、肉松、火…

C++ STL

目录 1.STL诞生 2.STL概念 3.STL六大主件 4.STL容器 算法 迭代器 5.容器算法迭代器初识&#xff0c;vector 5.1vector存放内置数据类型&#xff0c; 5.2vector存放自定义数据类型&#xff0c;解引用.访问&#xff0c;指针->访问&#xff0c;存放自定义数据类型指针。迭代器…

LeetCode(Array)1365. How Many Numbers Are Smaller Than the Current Number

1.问题 Given the array nums, for each nums[i] find out how many numbers in the array are smaller than it. That is, for each nums[i] you have to count the number of valid j’s such that j ! i and nums[j] < nums[i]. Return the answer in an array. Examp…

多目标建模总结

1. 概述 在推荐系统中&#xff0c;通常有多个业务目标需要同时优化&#xff0c;常见的指标包括点击率CTR、转化率CVR、 GMV、浏览深度和品类丰富度等。为了能平衡最终的多个目标&#xff0c;需要对多个目标建模&#xff0c;多目标建模的常用方法主要可以分为&#xff1a; 多模…

Linux常用命令——top命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) top 显示或管理执行中的程序 补充说明 top命令可以实时动态地查看系统的整体运行情况&#xff0c;是一个综合了多方信息监测系统性能和运行信息的实用工具。通过top命令所提供的互动式界面&#xff0c;用热键可…

C primer plus学习笔记 —— 13、存储类别、内存管理

文章目录存储类别定义、声明和初始化的区别作用域翻译单元和文件链接属性存储期存储类别多文件共享全局变量函数的存储类别存储类别的选择分配内存&#xff08;malloc、free&#xff09;malloc和calloc创建数组方式free的重要性举例存储类别 int a 1; int *p &a; int ra…

【Stm32野火】:野火STM32F103指南者开发板烧写官方示例程序LCD无法点亮?LCD示例程序无法使用?

项目场景&#xff1a; 大家好&#xff0c;最近在使用野火STM32F103指南者开发板的时候发现官方的示例程序LCD驱动代码居然无法直接驱动LCD点亮&#xff0c;这让我百思不得其解&#xff0c;以下就是我的踩坑填坑的过程&#xff0c;希望对大家有所帮助。 野火官方资料下载文档链接…

systemd介绍

systemd是一个 Linux 系统基础组件的集合&#xff0c;提供了一个系统和服务管理器&#xff0c;运行为 PID 1 并负责启动其它程序。功能包括&#xff1a;支持并行化任务&#xff1b;同时采用 socket 式与 D-Bus 总线式激活服务&#xff1b;按需启动守护进程&#xff08;daemon&a…