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) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.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) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.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.mzph.cn/news/872278.shtml

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

相关文章

睿考网:造价员和造价工程师是一个意思吗?

在工程建设领域中&#xff0c;经常会有人问&#xff1a;“造价员和造价工程师是一样的吗?”这两者代表的是两种独立的职业身份&#xff0c;职责和资格要求有明显的差异&#xff0c;是两种完全不同的考试。 造价工程师是一种具有专业资质的人员&#xff0c;通过国家统一的执业…

『 Linux 』命名管道

文章目录 命名管道与匿名管道命名管道特点命名管道的理解命名管道实现两个毫无关联的进程间通信 命名管道与匿名管道 命名管道是管道的一种,数据流向为单向故被称为管道; 与匿名管道相同属于一种内存级文件; 区别如下: 名字 匿名管道 没有名字,只存在于内存当中(类似内核缓冲…

【软件测试】编写测试用例篇

前面部分主要是编写测试用例的方法和方向&#xff0c;后面一部分是编写出具体的测试用例 目录 什么是测试用例 1.设计测试用例的万能公式 1.1.从思维出发 1.2.万能公式 1.3.弱网测试 1.4.安装与卸载测试 2.设计测试用例的方法 2.1.基于需求的设计方法 2.2.等价类 2.3…

测试开发面经总结(三)

TCP三次握手 TCP 是面向连接的协议&#xff0c;所以使用 TCP 前必须先建立连接&#xff0c;而建立连接是通过三次握手来进行的。 一开始&#xff0c;客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口&#xff0c;处于 LISTEN 状态 客户端会随机初始化序号&…

原来,BI数据分析也是有模板的

在当今数据驱动的时代&#xff0c;商业智能&#xff08;BI&#xff09;数据分析已经成为企业决策的重要工具。然而&#xff0c;很多人可能并不了解&#xff0c;BI数据分析并非从零开始&#xff0c;而是可以依托现成的模板和解决方案来快速搭建和实施的。以奥威BI方案为例&#…

React+TS前台项目实战(二十九)-- 首页构建之性能优化实现首页Echarts模块数据渲染

文章目录 前言Echart模块源码功能分析数据渲染一、HashRateEchart统计图1. 功能分析2. 代码详细注释 二、BlockTimeChart统计图1. 功能分析2. 代码详细注释 三、使用方式四. 数据渲染后效果如下 总结 前言 还记得之前我们创建的 高性能可配置Echarts组件 吗&#xff1f;今天我…

redis 配置文件参数详解

1、redis.conf 通用类 Redis的配置文件是一个文本文件&#xff0c;通常名为redis.conf。以下是一些常见配置项的解释和示例&#xff1a; 1、bind 127.0.0.1&#xff1a;绑定的主机地址 2、 protected-mode ,默认是开启状态&#xff0c;一般不需要修改&#xff0c;可以保证服务…

唯众物联网综合实训台 物联网实验室建设方案

物联网综合实训装置 物联网工程应用综合实训台是我公司针对职业院校物联网行业综合技能型人才培养&#xff0c;综合运用传感器技术、RFID技术、接口控制技术、无线传感网技术、Android应用开发等&#xff0c;配合实训台上的433M无线通信设备、ZigBee节点、射频设备、控制设备、…

智能家居产品公司网站源码,自适应布局设计,带完整演示数据

适合各类智能家居电子产品使用的网站源码&#xff0c;深色大气设计&#xff0c;自适应布局设计&#xff0c;pc手机均可完美适配&#xff0c;带完整演示数据。 独家原创资源。源码是asp开发的&#xff0c;数据库是access&#xff0c;主流的虚拟主机空间都支持asp&#xff0c;直…

第三届经济、智慧金融与当代贸易国际学术会议(ESFCT2024)

【五大高校联合支持】第三届经济、智慧金融与当代贸易国际学术会议(ESFCT 2024) 2024 3rd International Conference on Economics, Smart Finance and Contemporary Trade 文章投稿均可免费参会 高录用快见刊【最快会后1-2个月左右见刊】【最快刊后1个月内上知网&谷歌学…

【人工智能】高级搜索技术(模拟退火搜索算法和遗传算法解决旅行商问题)

目录 一、旅行商问题 1. 需求分析 2. 数据结构、功能模块设计与说明 2.1 数据结构 &#xff08;1&#xff09;模拟退火搜索算法 &#xff08;2&#xff09;遗传算法 2.2 功能模块设计 &#xff08;1&#xff09;模拟退火搜索算法 &#xff08;2&#xff09;遗传算法 …

在 PostgreSQL 里如何处理数据的存储优化和查询复杂度的平衡?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01;&#x1f4da;领书&#xff1a;PostgreSQL 入门到精通.pdf 文章目录 在 PostgreSQL 里如何处理数据的存储优化和查询复杂度的平衡&#xff1f;一、理解数据存储优化和查询复…

亚马逊、ebay、沃尔玛卖家打造爆款如何利用测评提高转化率?

做亚马逊、速卖通、ebay只有打造爆款&#xff0c;才能够挣到钱&#xff0c;如果一年到头&#xff0c;不断测款&#xff0c;不断测试不同的广告打法&#xff0c;那么代表了什么&#xff1f;代表了你的试错成本相当高&#xff0c;一不小心&#xff0c;分分钟就能够把手头上仅有的…

工业智能网关的边缘计算能力赋能工业4.0

边缘计算是将数据处理和分析能力推向网络边缘的技术&#xff0c;使得终端设备能够实时、快速地响应环境变化&#xff0c;并做出相应决策。在智能制造中&#xff0c;通过5G工业网关的边缘计算能力&#xff0c;企业可以实现对生产线上大量传感器数据的实时采集、处理和分析&#…

开发实战经验分享:互联网医院系统源码与在线问诊APP搭建

作为一名软件开发者&#xff0c;笔者有幸参与了多个互联网医院系统的开发项目&#xff0c;并在此过程中积累了丰富的实战经验。本文将结合我的开发经验&#xff0c;分享互联网医院系统源码的设计与在线问诊APP的搭建过程。 一、需求分析 在开发任何系统之前&#xff0c;首先要…

用chatgpt写了个二级导航,我全程一个代码没写,都是复制粘贴

今天心血来潮&#xff0c;让chatgpt给我写个移动端的二级导航菜单&#xff0c;效果如下&#xff1a; 1、两级导航&#xff0c;竖向排列&#xff0c;一级导航默认显示&#xff0c;二级隐藏 2、抽屉伸缩效果&#xff0c;点击一级导航&#xff0c;展开二级导航&#xff0c;再次点…

条件匹配工具之ACL概述

基本概念 ACL&#xff0c;即Access Control List&#xff08;访问控制列表&#xff09;&#xff0c;每个ACL但是是由单条或多条Rule&#xff08;规则&#xff09;组成的一个集合 技术背景&#xff1a; 1.用户需求&#xff1a; 用户对网络服务体验的要求越来越高&#xff0c…

冒泡,选择,插入,希尔排序

目录 一. 冒泡排序 1. 算法思想 2. 时间复杂度与空间复杂度 3. 代码实现 二. 选择排序 1. 算法思想 2. 时间复杂度与空间复杂度 3. 代码实现 三.插入排序 1. 直接插入排序 (1). 算法思想 (2). 时间复杂度与空间复杂度 (3). 代码实现 2. 希尔排序 (1). 算法思想 …

使用mitmproxy抓包详细记录(一)

1、安装mitmproxy pip install mitmproxy 安装失败解决方案&#xff0c;见上一篇 2、编辑代码&#xff0c;可以直接复制我的. 给文件起名&#xff0c;attacy.py import mitmproxyimport csv from mitmproxy import httpclass RequestRecorder:def __init__(self):self.records…

文件安全传输系统,如何保障信创环境下数据的安全传输?

文件安全传输系统是一套旨在保护数据在传输过程中的安全性和完整性的技术或解决方案。通常包括以下几个关键组件&#xff1a; 加密&#xff1a;使用强加密算法来确保文件在传输过程中不被未授权访问。 身份验证&#xff1a;确保只有授权用户才能访问或传输文件。 完整性校验…