Redis的使用(四)常见使用场景-缓存使用技巧

1.绪论

redis本质上就是一个缓存框架,所以我们需要研究如何使用redis来缓存数据,并且如何解决缓存中的常见问题,缓存穿透,缓存击穿,缓存雪崩,以及如何来解决缓存一致性问题。

2.缓存的优缺点

2.1 缓存的优点

缓存可以降低数据库的负载,提高读写效率和响应时间。

2.2 缓存的缺点

1.需要额外的资源消耗

2.如何保证缓存和数据库的一致性是一个问题,所以要求强一致性的业务尽量不要用缓存

3.缓存一致性

为了保证缓存和数据库的一致性,我们一般有三种方式来操作缓存,分别是读写穿透,旁路缓存和异步写回。

3.1 Cache Aside Pattern (旁路缓存)

其实就是人工编码方式,也是我们操作redis的常见的使用方式,就是先更新完数据库再去更新缓存。

3.1.1 步骤

1.读缓存

如果命中缓存直接返回,如果未命中便从数据中读取数据并且更新缓存。

2.写缓存

如果未命中缓存直接更新数据库,如果命中缓存,便更新数据库,同时更新(删除)缓存。

3.1.2 更新缓存和数据库的方式

读缓存这里没有一致性问题,但是在写入缓存的时候是先写入到数据库,还是先写入到缓存这里就会有一致性问题。对于写缓存的操作方式主要有如下4种:写数据库写缓存,写缓存写数据库,写数据库删缓存,写缓存删数据。

1.写缓存写数据库

先写缓存,再写数据库的话,如果缓存写入成功,数据库写入失败,数据库回滚,此时缓存便会与数据库内容不一致。

2.写数据写缓存

可以看出在线程1更新数据库为1成功,线程2获得时间片,更新数据库和缓存为x,线程1再次获得时间片,更新缓存为1。这个时候缓存和数据库数据不一致。

3.删缓存更新数据库

可以看出线程1在删除缓存后,线程2获得时间片,读取到缓存为空,会查询数据库并将该旧值写入到缓存中,但是线程1会更新数据库为新值,导致缓存不一致。

4.更新数据库删除缓存

假设此时因为缓存淘汰策略,缓存已经失效。所以初始时缓存为null

可以看出,假设缓存初始时因为淘汰策略,缓存为null,然后线程1查询数据库得到A=1,线程2获得时间片,更新数据库为A=x并且删除缓存,线程1得到时间片,更新缓存为A=1,此时发生了缓存不一致。

但是上面主要有两个条件导致:

1.线程1查询缓存刚好失效,需要从数据库中重新读取;

2.线程1查完库后线程2立刻更新数据库并且删除缓存。

这两个条件在事件开发过程中是很难遇到的,所以该方式可以作为缓存更新数据库的方式。

5.延迟双删

延迟双删其实是在第3种方式删除缓存更新数据库上面再加了一次删除。如下:

前面说过,在删除缓存和更新数据之间,可能会有其他线程因为未命中缓存,所以读取到旧数据并且更新到缓存中。所以我们就延迟一定时间,尽量将这部分线程更新的缓存数据删除掉。

a) 为什么需要第一次删除

因为删除和写入数据库不是一个原子操作,如果是先更新数据库,然后延迟一段时间,在删除缓存,这样操作的话,如果第二次删除失败,会有不一致的问题。所以在更新数据库前引入一次删除操作,这样可以尽可能的保证删除成功。

b) 为什么需要删除第二次

前面已经讲过,第二次删除是为了删除掉因为第一次删除和更新期间其他线程查询数据库旧值 并写入到缓存的问题。

3.1.3 如何保证删除成功

根据前文的分析,我们在实际开发中可以通过如下两种方式来更新缓存:

1.更新数据库删除缓存

2.延迟双删

当时上面两种方式都需要保证删除成功才能保证缓存和数据库的一致性,我们应该怎样才能保证删除缓存成功呢?

1.设置超时时间

这种方式其实就是利用缓存自带的过期策略保证缓存一定会过期,尽量的减少脏读。

2.重试

当删除失败的时候,可以进行重试,但是可能影响接口性能。

3.监听binlog

比如延迟双删可以变成如下:

1.删除缓存

2.更新数据库。

3.监听binlog删除缓存。

监听binlog的中间件一般都有重试机制,能够保证删除尽量成功。

3.1.4 如何保证redis和数据库的强一致性

前面说的更新缓存的方案里面推荐的更新数据库+删除缓存和延迟双删都不能完全保证数据库和缓存的一致性。如果对一致性要求很高,可以做成同步的方式,先更新数据库再更新redis,并且给这两个操作加分布式锁,保证原子性。

3.2 Read/Write Through Pattern(读写穿透)

缓存和数据库为一个整体,用户只需要操作缓存,至于如何实现缓存与数据的一致性,交给缓存去实现。其实我觉得这种模式叫通过缓存读,通过缓存写更好理解。因为客户端基本上只更缓存打交道,如果缓存没有数据需要同步数据库内容,是通过缓存去更新的。更新缓存数据后,同步到数据库也是通过缓存更新的。

3.2.1 读缓存

读缓存的时候,如果数据存在,便直接返回,如果数据不存在,缓存便会从数据库中拉取数据,并用户返回。

3.2.2 写缓存

写缓存的时候,如果未命中缓存,便更新数据库。如果命中缓存,便更新缓存,缓存在更新数据到数据库。注意,缓存需要保证这两个动作的原子性。

这里,为什么,客户端更新缓存的时候,是直接更新数据库,而不更新缓存呢?其实是因为这样可以将更新操作分摊到读写缓存中来。读缓存时同步未命中缓存的那一部分数据,写缓存时同步命中缓存的那一部分数据。

3.2.3 优势

其实就是为了减少旁路缓存,用户的开发工作,缓存自己实现了和数据库同步的这部分工作。

3.3 Write Behind Caching Pattern(异步写回)

异步写回,其实就是用户只用更新缓存中的数据,然后启动一个线程,异步的将缓存中的数据刷到数据库中。这个其实在很多框架中都是采用这种方式,比如前面介绍的RocketMq中对MappedFile的持久化,还有linux中的页缓存等都是采用这种方式。

它的优点就是,只用和缓存打交道,所以速度极快。并且它和读穿/写穿模式的最主要的区别是。读穿/写穿模式是更新缓存过后,会同步刷新到数据库中。但是异步写回是异步的写入到库中。所以可能会有丢数据的风险。

4.缓存穿透

4.1 什么是缓存穿透

缓存穿透就是,当客户端访问缓存时,发现缓存中没有数据,然后去访问数据库,但是数据库中也没有数据。所以在读取数据的时候,因为数据库中没有数据给redis缓存,所以请求会一直到数据库中,导致数据库压力过大。

4.2 怎么解决缓存穿透

4.2.1 缓存空对象

1.操作

当数据库中没有数据的时候,可以缓存一个空对象到redis中。

2.优缺点

操作简单,但是如果数据库有对象的时候,并且采用的是过期淘汰的策略的话,会有一段时间和数据库不一致。

3.代码
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) {
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 6.存在,写入redis
        this.set(key, r, time, unit);
        return r;
    }

4.2.3 布隆过滤器

可以在请求前根据id到布隆过滤器中查询一下,判断该数据是否存在,如果不存在便直接返回。布隆过滤器时基于概率统计的判断某个元素是否存在某个位数组中的工具。

我们来看看其实现原理:

布隆过滤器由一组hash函数和一个数组组成。现在假设有k个hash函数,当有一个对象传入的时候,这k个hash喊出会将这个字符串进行hash运算,然后映射到数组的k个bit位上。在判断对象是否存在的时候,根据对象上面的bit位是否都位1,如果都为1的话,表示对象可能存在。但为什么是可能,而不是一定呢?因为有hash冲突,有极低的可能存在某两个元素,经过k个hash函数的映射到数组中的位置是一样的。

5.缓存雪崩

5.1 什么是缓存雪崩

缓存雪崩就是在某一个时刻大量的key同时过期或者redis直接宕机,导致大量请求涌入到数据库,数据库压力激增。

5.2 怎么解决缓存雪崩

为了预防大量key同时过期:给key的过期时间设置一个随机值;

为了防止redis过期:我们可以通过集群的方式保证redis服务高可用。

6.缓存击穿

6.1 什么是缓存击穿

缓存击穿就是在高并发场景下,因为热点key(这里热点key可以指访问频率高或者重建缓存时间长的key)过期,导致大量线程同时重建缓存。

6.2 怎么解决缓存击穿

6.2.1 互斥锁

1.思路

其实就是保证只有一个线程在重建缓存。当某个线程发现缓存不存在是,先加互斥锁,然后查询数据库,构建缓存,更新缓存。如果此时其他线程来获取缓存,发现缓存为空,重建缓存时需要先阻塞获取锁。

2.代码
    public <R, ID> R queryWithMutex(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(shopJson, type);
        }
        // 判断命中的是否是空值
        if (shopJson != null) {
            // 返回一个错误信息
            return null;
        }

        // 4.实现缓存重建
        // 4.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        R r = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2.判断是否获取成功
            if (!isLock) {
                // 4.3.获取锁失败,休眠并重试
                Thread.sleep(50);
                return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
            }
            // 4.4.获取锁成功,根据id查询数据库
            r = dbFallback.apply(id);
            // 5.不存在,返回错误
            if (r == null) {
                // 将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                // 返回错误信息
                return null;
            }
            // 6.存在,写入redis
            this.set(key, r, time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            // 7.释放锁
            unlock(lockKey);
        }
        // 8.返回
        return r;
    }

6.2.2 逻辑过期

1.思路

其主要步骤如下:

1.给数据设置一个逻辑过期时间,并且写入到缓存中比如{"name":"三',"expireTime":1720712827}

2.线程1查询缓存,发现数据已经过期,单独启动一个线程进行缓存重建,这里重建缓存也需要加互斥锁,防止多个线程进行重建。

3.其他线程访问缓存,发现缓存过期,首先会获取锁,如果发现数据已经过期,会去获取锁进行缓存重建,但是获取锁失败,返回redis中的旧数据。

2.代码
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.判断是否存在,这里会进行缓存预热,提前将热点数据加载到redis中
        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 = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        // 6.2.判断是否获取锁成功
        if (isLock){
            // 6.3.成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R newR = dbFallback.apply(id);
                    // 重建缓存
                    this.setWithLogicalExpire(key, newR, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        // 6.4.返回过期的商铺信息
        return r;
    }
3.优缺点

逻辑过期不用进行锁等待,但是会占用额外的空间(存储缓存过期时间)并且不能保证一致性(因为其他线程发现有线程在异步重建缓存过后,会返回旧数据)。

7.参考

1.Redis第12讲——缓存的三种设计模式_缓存的设计模式-CSDN博客

2.缓存一致性问题解决方案-CSDN博客

3.https://www.yuque.com/hollis666/un6qyk/tmcgo0

4. 黑马程序员Redis入门到实战教程,深度透析redis底层原理+redis分布式锁+企业解决方案​​​​​​+黑马点评实战项目_哔哩哔哩_bilibili

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

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

相关文章

JMeter进行HTTP接口测试的技术要点

参数化 用户定义的变量 用的时候 ${名字} 用户参数 在参数列表中传递 并且也是${} csv数据文件设置 false 不忽略首行 要首行 从第一行读取 true 忽略首行 从第二行开始 请求时的参数设置&#xff1a; 这里的名称是看其接口需要的请求参数的名称 这里的变量名称就是为csv里面…

SpringBatch文件读写ItemWriter,ItemReader使用详解

SpringBatch文件读写ItemWriter&#xff0c;ItemReader使用详解 1. ItemReaders 和 ItemWriters1.1. ItemReader1.2. ItemWriter1.3. ItemProcessor 2.FlatFileItemReader 和 FlatFileItemWriter2.1.平面文件2.1.1. FieldSet 2.2. FlatFileItemReader2.3. FlatFileItemWriter 3…

低空经济持续发热,无人机培训考证就业市场及前景剖析

随着科技的不断进步和社会需求的日益增长&#xff0c;低空经济已成为全球及我国经济增长的新引擎。作为低空经济的重要组成部分&#xff0c;无人机技术因其广泛的应用领域和显著的经济效益&#xff0c;受到了社会各界的广泛关注。为满足市场对无人机人才的需求&#xff0c;无人…

【动态规划1】斐波那契数列模型篇

文章目录 声明动态规划介绍1137.第N个泰波那契数题目描述分析代码 面试题 08.01. 三步问题题目描述分析代码 746.使用最小花费爬楼梯题目描述分析代码 91.解码⽅法题目描述分析代码 声明 本篇博客为动态规的基础篇&#xff0c;从零开始学习动态规划&#xff0c;如有错误&#…

MATLAB quiver矢量图 设置colorbar

给三维矢量图按照不同高度设置箭头颜色 figure clf X surfaceuz(:,1); Y surfaceuz(:,2); Z surfaceuz(:,3); hold onzcolor jet; % qquiver3(X,Y,Z,X,Y,W) for i 1:length(surfaceuz)quiver3(X(i),Y(i),Z(i),X(i),Y(i), Z(i),...Color,zcolor(floor((Z(i) - -0.1) * 2…

408数据结构-图的应用3-有向无环图、拓扑排序 自学知识点整理

前置知识&#xff1a;表达式&#xff0c;图的遍历 有向无环图描述表达式 有向无环图&#xff1a;若一个有向图中不存在环&#xff0c;则称为有向无环图&#xff0c;简称 D A G DAG DAG图 。 &#xff08;图片来自王道考研408数据结构2025&#xff09; 由王道考研-咸鱼学长的讲…

深圳晶彩智能JC3636W518C开箱实现电脑副屏功能

深圳晶彩智能发布了JC3636W518C 这是一款中国制造的&#xff0c;铝合金外壳&#xff0c;价格非常震撼的开发板。原创是billbill的up播主萨纳兰的黄昏设计的ESP32太极小派&#xff0c;由深圳晶彩智能批量生产。 该款 LCD 模块采用 ESP32-S3R8 芯片作为主控,该主控是双核 MCU&…

Vulnhub:DC-1

1.环境搭建 靶机下载地址 将下载的靶机导入到Oracle VM VirtualBox中&#xff0c;设置仅主机模式&#xff0c;使用和kali相同的网卡 2.渗透过程 使用nmap工具进行主机发现扫描 nmap -sn 192.168.56.0/24 发现靶机ip地址&#xff0c;使用nmap工具进行靶机端口扫描 nmap -sS…

一文说透Springboot单元测试

你好&#xff0c;我是柳岸花开。 一、单元测试说明 1 单元测试的优点与基本原则 一个好的单元测试应该具备以下FIRST 原则和AIR原则中的任何一条&#xff1a; 单元测试的FIRST 规则 Fast 快速原则&#xff0c;测试的速度要比较快&#xff0c; Independent 独立原则&#xff0c;…

Qt 多窗体、复用窗口的使用

1.继承自QWidge的窗口的呈现&#xff0c;作为tabPage呈现&#xff0c;作为独立窗口呈现 2.继承自QMainWindow的窗口的呈现&#xff0c;作为abPage呈现&#xff0c;作为独立窗口呈现 1. 继承自QWidge的窗口的呈现 1.1 作为tabPage呈现 void MutiWindowExample::on_actWidgetI…

AI绘画入门实践|Midjourney 提示词的使用技巧

提示词长短 尽可能做到简洁明了。 提示词很短 MJ 出图的随机性更高&#xff0c;创造的内容更有想象力&#xff0c;更适合创意发散的图像生成。 a dog 提示词很长 MJ 出图会更加精准&#xff0c;但描述太过详细&#xff0c;有可能出现AI理解不到位的情况。 越到后面的提示词&…

风险评估:IIS的安全配置,IIS安全基线检查加固

「作者简介」&#xff1a;冬奥会网络安全中国代表队&#xff0c;CSDN Top100&#xff0c;就职奇安信多年&#xff0c;以实战工作为基础著作 《网络安全自学教程》&#xff0c;适合基础薄弱的同学系统化的学习网络安全&#xff0c;用最短的时间掌握最核心的技术。 这一章节我们需…

Java面试八股之Redis集群Cluster

Redis集群Cluster Redis Cluster是一种基于数据分片&#xff08;Sharding&#xff09;的分布式缓存和存储系统&#xff0c;它实现了数据的水平扩展、高可用性和自动故障转移。以下是对Redis Cluster模式详细实现流程的描述&#xff1a; 1. 初始化与配置 部署节点&#xff1a…

flutter 手写 TabBar

前言&#xff1a; 这几天在使用 flutter TabBar 的时候 我们的设计给我提了一个需求&#xff1a; 如下 Tabbar 第一个元素 左对齐&#xff0c;试了下TabBar 的配置&#xff0c;无法实现这个需求&#xff0c;他的 配置是针对所有元素的。而且 这个 TabBar 下面的 滑块在移动的时…

产品经理-产品经理会在项目中遇到的几个问题(16)

项目中遇到了需求变更怎么办&#xff1f; 首先要弄清楚需求变更的原因是什么。如果是因为在迭代的过程中更好地理解了用户需求 进而产生了更好的需求则完全是正常的。如果是因为老板的需求 那就需要和老板沟通清楚&#xff0c;并且确保自己能理解老板的需求&#xff0c;而且这个…

软件测试——测试用例

工作职责&#xff1a; 1.负责产品系统测试&#xff0c;包括功能测试、性能测试、稳定性测试、用户场景测试、可靠性测试等。 2.负责测试相关文档的编写&#xff0c;包括测试计划、测试用例、测试报告等。 3.负责自动化测试框架、用例的维护。 岗位要求&#xff1a; 1.熟练…

800块,我从淘宝上买AGV……

导语 大家好&#xff0c;我是社长&#xff0c;老K。专注分享智能制造和智能仓储物流等内容。 新书《智能物流系统构成与技术实践》人俱乐部 从淘宝上打算够购买一台AGV小车&#xff0c;上去一搜&#xff0c;嘿&#xff0c;你别说&#xff0c;还真有。便宜的才200块钱。 很兴奋把…

17-8 向量数据库之野望8 - 7 个主流向量数据库

​​​​​​ 在快速发展的人工智能 (AI)、机器学习 (ML) 和数据工程领域,对高效数据存储和检索系统的需求至关重要。矢量数据库已成为管理这些技术通常依赖的复杂高维数据的关键解决方案。在这里,我们探讨了每个 AI/ML/数据工程师都应该熟悉的七个矢量数据库,重点介绍了它们…

【Linux】01.Linux 的常见指令

1. ls 指令 语法&#xff1a;ls [选项] [目录名或文件名] 功能&#xff1a;对于目录&#xff0c;该命令列出该目录下的所有子目录与文件。对于文件&#xff0c;将列出文件名以及其他信息 常用选项&#xff1a; -a&#xff1a;列出当前目录下的所有文件&#xff0c;包含隐藏文件…

JavaSE学习笔记第三弹之异常抛出

今天我们继续来学习JavaSE相关的知识&#xff0c;希望与大家共同努力。 目录 异常 什么是异常 运行时异常 编译时异常 ​编辑 为什么需要异常处理机制 错误 异常的处理与抛出 异常处理 异常抛出 自定义异常 结语 异常 什么是异常 Java中异常是一种在程序运行时发…