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

news2024/11/15 15:34:05

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.coloradmin.cn/o/1928706.html

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

相关文章

Transformer模型解析:走进自然语言处理的新时代

UPDATED&#xff1a;2023 年 1 月 27 日&#xff0c;本文登上 ATA 头条。&#xff08;注&#xff1a;ATA 全称 Alibaba Technology Associate&#xff0c;是阿里集团最大的技术社区&#xff09;UPDATED&#xff1a;2023 年 2 月 2 日&#xff0c;本文在 ATA 获得鲁肃点赞。&…

华为OD算法题汇总

60、计算网络信号 题目 网络信号经过传递会逐层衰减&#xff0c;且遇到阻隔物无法直接穿透&#xff0c;在此情况下需要计算某个位置的网络信号值。注意:网络信号可以绕过阻隔物 array[m][n]&#xff0c;二维数组代表网格地图 array[i][j]0&#xff0c;代表i行j列是空旷位置 a…

数据结构(4.0)——串的定义和基本操作

串的定义(逻辑结构) 串&#xff0c;即字符串(String)是由零个或多个字符组成的有序数列。 一般记为Sa1a2....an(n>0) 其中&#xff0c;S是串名&#xff0c;单引号括起来的字符序列是串的值;ai可以是字母、数字或其他字符&#xff1b;串中字符的个数n称为串的长度。n0时的…

分布式对象存储minio

本教程minio 版本&#xff1a;RELEASE.2021-07-*及以上 1. 分布式文件系统应用场景 互联网海量非结构化数据的存储需求 电商网站&#xff1a;海量商品图片视频网站&#xff1a;海量视频文件网盘 : 海量文件社交网站&#xff1a;海量图片 1.1 Minio介绍 MinIO 是一个基于Ap…

Spring解决循环依赖:三级缓存

1.什么是循环依赖 通俗来讲&#xff0c;循环依赖指的是一个实例或多个实例存在相互依赖的关系&#xff08;类之间循环嵌套引用&#xff09;。 2.Spring如何解决循环依赖 首先&#xff0c;先介绍Spring是如何创建Bean的。 &#xff08;1&#xff09;createBeanInstance&…

【LoadRunner】博客笔记项目 性能测试报告

文章目录 前言一、博客笔记项目性能测试介绍二、编写性能测试脚本&#xff08;VUG&#xff09; 2.1 测试脚本编写步骤 2.2 脚本总代码和结果分析三、创建测试场景&#xff08;Controller&#xff09; 3.1 测试场景创建实现步骤四、生成测试报告&#xff08;Anal…

集合相关知识

string final&#xff0c;不能追加&#xff0c;需要重新new一个 stringbuild&#xff0c;内容 可变&#xff0c;可以重新赋能&#xff0c;能够追加&#xff0c;空间不足创造一个更大的&#xff0c;然后复制过去 stringbufferbuild 线程安全 javac编译&#xff0c;字符串加号…

SpringBoot介绍以及第一个SpringBoot程序

T04BF &#x1f44b;专栏: 算法|JAVA|MySQL|C语言 &#x1faf5; 今天你敲代码了吗 文章目录 2.第一个SpringBoot程序2.1Spring Boot介绍2.2使用idea创建Spring Boot程序2.2.1 社区版idea2.2.2专业版idea2.2.3创建SpringBoot项目2.2.4项目代码和目录介绍目录介绍pom文件 2.3Web…

Linux 上 TTY 的起源

注&#xff1a;机翻&#xff0c;未校对。 What is a TTY on Linux? (and How to Use the tty Command) What does the tty command do? It prints the name of the terminal you’re using. TTY stands for “teletypewriter.” What’s the story behind the name of the co…

【边缘计算网关教程】6.松下 Mewtocol TCP 协议

前景回顾&#xff1a;【边缘计算网关教程】5.三菱FX3U编程口通讯-CSDN博客 松下 Mewtocol TCP 协议 适配PLC&#xff1a;松下FP0H 松下XHC60ET 1. 硬件连接 Mewtocol TCP协议采用网口通信的方式&#xff0c;因此&#xff0c;只需要保证网关的LAN口和松下PLC的IP在一个网段即…

STM32怎么把VDD与VSS引脚配置为GPIO?

在 STM32 微控制器中&#xff0c;VDD 和 VSS 引脚是供电引脚&#xff0c;分别用于电源和接地。我收集归类了一份嵌入式学习包&#xff0c;对于新手而言简直不要太棒&#xff0c;里面包括了新手各个时期的学习方向编程教学、问题视频讲解、毕设800套和语言类教学&#xff0c;敲个…

使用base64通用文件上传

编写一个上传文件的组件 tuku,点击图片上传后使用FileReader异步读取文件的内容&#xff0c;读取完成后获得文件名和base64码&#xff0c;调用后端uploadApi,传入姓名和base64文件信息&#xff0c;后端存入nginx中&#xff0c;用于访问 tuku.ts组件代码&#xff1a; <templa…

变量筛选—特征包含信息量

在变量筛选中,通过衡量特征所包含信息量大小,决定是否删除特征,常用的指标有单一值占比、缺失值占比和方差值大小。单一值或缺失值占比越高,表示特征包含信息量越少,不同公司设置不同阈值,一般单一值、缺失值占比高于95%,建议删除。方差值越小,代表特征包含信息量越小。…

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

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

C语言程序设计实例2

C语言程序设计2 问题2_1代码2_1结果2_1 问题2_2代码2_2结果2_2 问题2_3代码2_3结果2_3 问题2_1 函数 f u n fun fun的功能是&#xff1a;计算如下公式前 n n n项的和&#xff0c;并作为函数值返回。 S 1 3 2 2 3 5 4 2 5 7 6 2 ⋅ ⋅ ⋅ ( 2 n − 1 ) ( 2 n 1 ) …

Linux操作系统——数据库

数据库 sun solaris gnu 1、分类&#xff1a; 大型 中型 小型 ORACLE MYSQL/MSSQL SQLITE DBII powdb 关系型数据库 2、名词&#xff1a; DB 数据库 select update database DBMS 数据…

Sql语句之增删改查(CRUD)

Sql语句的书写也被称之为CRUD&#xff0c;即C&#xff08;Create增加&#xff09;R&#xff08;Retrieve添加&#xff09;U&#xff08;Update更新&#xff09;D&#xff08;Delete删除&#xff09;四个操作的首字母。 我们先来看增、删、改这三个相对较为简单&#xff0c;语法…

string+迭代器

int main(){ string s0; string s1("hello word"); cout<<s1<<endl; //遍历string,下标[] for(size_t i0;i<s1.size();i) { cout<<s1[i]<<""; } cout<<endl; } 注意&#xff1a;这里size_t不算/0 迭代器 int main() {st…

Open3D 生成多个球形点云

一、概述 使用 Open3D 创建一个三角网格的球体&#xff0c;并从中均匀采样点生成点云&#xff0c;同时可以指定球体的半径和中心位置。生成 5 个不同大小和位置的圆球形点云&#xff0c;并将它们合并成一个点云以进行显示。 二、代码实现 import open3d as o3d import numpy …

Django任务管理

1、用django-admin命令创建一个Django项目 django-admin startproject task_manager 2、进入到项目下用命令创建一个应用 cd task_manager python manage.py startapp tasks 3、进入models.py定义数学模型 第2步得到的只是应用的必要空文件&#xff0c;要开始增加各文件实际…