利用Redis进行数据缓存

article/2025/8/27 12:25:08

1. 引言

缓存有啥用?

  1. 降低对数据库的请求,减轻服务器压力
  2. 提高了读写效率

缓存有啥缺点?

  1. 如何保证数据库与缓存的数据一致性问题?
  2. 维护缓存代码
  3. 搭建缓存一般是以集群的形式进行搭建,需要运维的成本

2. 将信息添加到缓存的业务流程

在这里插入图片描述
上图可以清晰的了解Redis在项目中所处的位置,是数据库与客户端之间的一个中间件,也是数据库的保护伞。有了Redis可以帮助数据库进行请求的阻挡,阻止请求直接打入数据库,提高响应速率,极大的提升了系统的稳定性。

3. 实现代码

在这里插入图片描述
下面将根据查询商铺信息来作为背景进行代码书写,具体的流程图如上所示。

3.1 代码实现(信息添加到缓存中)

public static final String SHOPCACHEPREFIX = "cache:shop:";@Autowiredprivate StringRedisTemplate stringRedisTemplate;// JSON工具ObjectMapper objectMapper = new ObjectMapper();@Overridepublic Result queryById(Long id) {//从Redis查询商铺缓存String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);//判断缓存中数据是否存在if (!StringUtil.isNullOrEmpty(cacheShop)) {//缓存中存在则直接返回try {// 将子字符串转换为对象Shop shop = objectMapper.readValue(cacheShop, Shop.class);return Result.ok(shop);} catch (JsonProcessingException e) {e.printStackTrace();}}//缓存中不存在,则从数据库里进行数据查询Shop shop = getById(id);//数据库里不存在,返回404if (null==shop){return Result.fail("信息不存在");}//数据库里存在,则将信息写入Redistry {String shopJSon = objectMapper.writeValueAsString(shop);stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX+id,shopJSon,30,TimeUnit.MINUTES);} catch (JsonProcessingException e) {e.printStackTrace();}//返回return Result.ok(shop);}

3.2 缓存更新策略

数据库与缓存数据一致性问题,当数据库信息修改后,缓存的信息应该如何处理?

内存淘汰超时剔除主动更新
说明不需要自己进行维护,利用Redis的淘汰机制进行数据淘汰给缓存数据添加TTL编写业务逻辑,在修改数据库的同时更新缓存
一致性差劲一般
维护成本

这里其实是需要根据业务场景来进行选择

  1. 高一致性:选主动更新
  2. 低一致性:内存淘汰和超时剔除

3.3 实现主动更新

此时需要实现数据库与缓存一致性问题,在这个问题之中还有多个问题值得深思

  1. 删除缓存还是更新缓存?
    当数据库发生变化时,我们如何处理缓存中无效的数据,是删除它还是更新它?
    更新缓存:每次更新数据库都更新缓存,无效写操作较多
    删除缓存:更新数据库时删除缓存,查询时再添加缓存
    由此可见,选择删除缓存是高效的。

  2. 如何保证缓存与数据库的操作的同时成功或失败?
    单体架构:单体架构中采用事务解决
    分布式架构:利用分布式方案进行解决

  3. 先删除缓存还是先操作数据库?

在这里插入图片描述
在并发情况下,上述情况是极大可能会发生的,这样子会导致缓存与数据库数据库不一致。
请添加图片描述
先操作数据库,在操作缓存这种情况,在缓存数据TTL刚好过期时,出现一个A线程查询缓存,由于缓存中没有数据,则向数据库中查询,在这期间内有另一个B线程进行数据库更新操作和删除缓存操作,当B的操作在A的两个操作间完成时,也会导致数据库与缓存数据不一致问题。

完蛋!!!两种方案都会造成数据库与缓存一致性问题的发生,那么应该如何来进行选择呢?

虽然两者方案都会造成问题的发生,但是概率上来说还是先操作数据库,再删除缓存发生问题的概率低一些,所以可以选择先操作数据库,再删除缓存的方案。

个人见解:
如果说我们在先操作数据库,再删除缓存方案中线程B删除缓存时,我们利用java来删除缓存会有Boolean返回值,如果是false,则说明缓存已经不存在了,缓存不存在了,则会出现上图的情况,那么我们是否可以根据删除缓存的Boolean值来进行判断是否需要线程B来进行缓存的添加(因为之前是需要查询的线程来添加缓存,这里考虑线程B来添加缓存,线程B是操作数据库的缓存),如果线程B的添加也在线程A的写入缓存之前完成也会造成数据库与缓存的一致性问题发生。那么是否可以延时一段时间(例如5s,10s)再进行数据的添加,这样子虽然最终会统一数据库与缓存的一致性,但是若是在这5s,10s内又有线程C,D等等来进行缓存的访问呢?C,D线程的访问还是访问到了无效的缓存信息。
所以在数据库与缓存的一致性问题上,除非在写入正确缓存之前拒绝相关请求进行服务器来进行访问才能避免用户访问到错误信息,但是拒绝请求对用户来说是致命的,极大可能会导致用户直接放弃使用应用,所以我们只能尽可能的减少问题可能性的发生。(个人理解,有问题可以在评论区留言赐教)

  @Override@Transactionalpublic Result updateShop(Shop shop) {Long id = shop.getId();if (null==id){return Result.fail("店铺id不能为空");}//更新数据库boolean b = updateById(shop);//删除缓存stringRedisTemplate.delete(SHOPCACHEPREFIX+shop.getId());return Result.ok();}

4. 缓存穿透

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

解决方案:

缓存空对象
在这里插入图片描述

缺点:

  1. 空间浪费
  2. 如果缓存了空对象,在空对象的有效期内,我们后台在数据库新增了和空对象相同id的数据,这样子就会造成数据库与缓存一致性问题

布隆过滤器

在这里插入图片描述

优点:

  1. 内存占用少

缺点:

  1. 实现复杂
  2. 存在误判的可能(存在的数据一定会判断成功,但是不存在的数据也有可能会放行进来,有几率造成缓存穿透)

4.1 解决缓存穿透(使用空对象进行解决)

public static final String SHOPCACHEPREFIX = "cache:shop:";@Autowiredprivate StringRedisTemplate stringRedisTemplate;// JSON工具ObjectMapper objectMapper = new ObjectMapper();@Overridepublic Result queryById(Long id) {//从Redis查询商铺缓存String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);//判断缓存中数据是否存在if (!StringUtil.isNullOrEmpty(cacheShop)) {//缓存中存在则直接返回try {// 将子字符串转换为对象Shop shop = objectMapper.readValue(cacheShop, Shop.class);return Result.ok(shop);} catch (JsonProcessingException e) {e.printStackTrace();}}// 因为上面判断了cacheShop是否为空,如果进到这个方法里面则一定是空,直接过滤,不打到数据库if (null != cacheShop){return Result.fail("信息不存在");}//缓存中不存在,则从数据库里进行数据查询Shop shop = getById(id);//数据库里不存在,返回404if (null==shop){// 缓存空对象stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX+id,"",2,TimeUnit.MINUTES);return Result.fail("信息不存在");}//数据库里存在,则将信息写入Redistry {String shopJSon = objectMapper.writeValueAsString(shop);stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX+id,shopJSon,30,TimeUnit.MINUTES);} catch (JsonProcessingException e) {e.printStackTrace();}//返回return Result.ok(shop);}

上述方案终究是被动方案,我们可以采取一些主动方案,例如

  1. 给id加复杂度
  2. 权限
  3. 热点参数的限流

5. 缓存雪崩

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

解决方案:

  1. 给不同的Key的TTL添加随机值
    大量的Key同时失效,极大可能是TTL相同,我们可以随机给TTL
  2. 利用Redis集群提高服务的可用性
  3. 给缓存业务添加降级限流策略
  4. 给业务添加多级缓存

6. 缓存击穿

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

常见的解决方案:

  1. 互斥锁
  2. 逻辑过期

互斥锁:

在这里插入图片描述
即采用锁的方式来保证只有一个线程去重建缓存数据,其余拿不到锁的线程休眠一段时间再重新重头去执行查询缓存的步骤

优点:

  1. 没有额外的内存消耗(针对下面的逻辑过期方案)
  2. 保证了一致性

缺点:

  1. 线程需要等待,性能受到了影响
  2. 可能会产生死锁

逻辑过期:

在这里插入图片描述
逻辑过期是在缓存数据中额外添加一个属性,这个属性就是逻辑过期的属性,为什么要使用这个来判断是否过期而不使用TTL呢?因为使用TTL的话,一旦过期,就获取不到缓存中的数据了,没有拿到锁的线程就没有旧的数据可以返回。

它与互斥锁最大的区别就是没有线程的等待了,谁先获取到锁就去重建缓存,其余线程没有获取到锁就返回旧数据,不去做休眠,轮询去获取锁。

重建缓存会新开一个线程去执行重建缓存,目的是减少抢到锁的线程的响应时间。

优点:

  1. 线程无需等待,性能好

缺点:

  1. 不能保证一致性
  2. 缓存中有额外的内存消耗
  3. 实现复杂

两个方案各有优缺点:一个保证了一致性,一个保证了可用性,选择与否主要看业务的需求是什么,侧重于可用性还是一致性。

6.1 互斥锁代码

互斥锁的锁用什么?

使用Redis命令的setnx命令。

首先实现获取锁和释放锁的代码

    /*** 尝试获取锁** @param key* @return*/private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}/*** 删除锁** @param key*/private void unLock(String key) {stringRedisTemplate.delete(key);}

在这里插入图片描述
代码实现

public Shop queryWithMutex(Long id) throws InterruptedException {//从Redis查询商铺缓存String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);//判断缓存中数据是否存在if (!StringUtil.isNullOrEmpty(cacheShop)) {//缓存中存在则直接返回try {// 将子字符串转换为对象Shop shop = objectMapper.readValue(cacheShop, Shop.class);return shop;} catch (JsonProcessingException e) {e.printStackTrace();}}// 因为上面判断了cacheShop是否为空,如果进到这个方法里面则一定是空,直接过滤,不打到数据库if (null != cacheShop) {return null;}Shop shop = new Shop();// 缓存击穿,获取锁String lockKey = "lock:shop:" + id;try{boolean b = tryLock(lockKey);if (!b) {// 获取锁失败了Thread.sleep(50);return queryWithMutex(id);}//缓存中不存在,则从数据库里进行数据查询shop = getById(id);//数据库里不存在,返回404if (null == shop) {// 缓存空对象stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX + id, "", 2, TimeUnit.MINUTES);return null;}//数据库里存在,则将信息写入Redistry {String shopJSon = objectMapper.writeValueAsString(shop);stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX + id, shopJSon, 30, TimeUnit.MINUTES);} catch (JsonProcessingException e) {e.printStackTrace();}}catch (Exception e){}finally {// 释放互斥锁unLock(lockKey);}//返回return shop;}

6.2 逻辑过期实现

逻辑过期不设置TTL

在这里插入图片描述
代码实现

@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}

由于是热点key,所以key基本都是手动导入到缓存,代码如下

  /*** 逻辑过期时间对象写入缓存* @param id* @param expireSeconds*/public void saveShopToRedis(Long id,Long expireSeconds){// 查询店铺数据Shop shop = getById(id);// 封装为逻辑过期RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));// 写入RedisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id, JSONUtil.toJsonStr(redisData));}

逻辑过期代码实现

/*** 缓存击穿:逻辑过期解决* @param id* @return* @throws InterruptedException*/public Shop queryWithPassLogicalExpire(Long id) throws InterruptedException {//1. 从Redis查询商铺缓存String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);//2. 判断缓存中数据是否存在if (StringUtil.isNullOrEmpty(cacheShop)) {// 3. 不存在return null;}// 4. 存在,判断是否过期RedisData redisData = JSONUtil.toBean(cacheShop, RedisData.class);JSONObject jsonObject = (JSONObject) redisData.getData();Shop shop = JSONUtil.toBean(jsonObject, Shop.class);LocalDateTime expireTime = redisData.getExpireTime();// 5. 判断是否过期if (expireTime.isAfter(LocalDateTime.now())){// 5.1 未过期return shop;}// 5.2 已过期String lockKey = "lock:shop:"+id;boolean flag = tryLock(lockKey);if (flag){// TODO 获取锁成功,开启独立线程,实现缓存重建,建议使用线程池去做CACHE_REBUILD_EXECUTOR.submit(()->{try {// 重建缓存this.saveShopToRedis(id,1800L);}catch (Exception e){}finally {// 释放锁unLock(lockKey);}});}// 获取锁失败,返回过期的信息return shop;}/*** 线程池*/private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

http://chatgpt.dhexx.cn/article/OR0wSHmy.shtml

相关文章

Linux下如何清空Redis缓存

1.首先进到redis的安装目录,进到src目录下,找到redis-cli 2.首先用账号密码的方式进入到redis的服务端 ./redis-cli -h 127.0.0.1 -p 6379 进去后会出现下面的界面ip:port> 然后,输入密码进行鉴权>auth "yourpassword"&a…

redis如何清空指定缓存和所有缓存

Windows环境下使用命令行进行redis缓存清理 1.访问redis根目录 cd D:\development_tools\redis64-3.0.501 2.登录redis:redis-cli -h 127.0.0.1 -p 6379 3.查看所有key值:keys * 4.删除指定索引的值:del key 5.清空整个 Redis 服务器的数据&…

redis删除缓存

首先下载一个redis可视化管理页面:RedisDesktopManager, 1、点击连接到redis服务器 2、 填写连接Ip地址,端口(默认6379),有密码填写密码,没用密码就不用填写了,然后点击测试连接,连接成功后,可以找到自己…

如何清理Redis中的缓存

首先在cmd模式下进入redis的目录, 然后使用 redis-cli -p 6379(指定进入的端口号,本人的端口号为6379) 进入该端口的redis数据库之后有以下两种清空缓存的命令 1.清空当前redis数据库缓存flushdb flushdb 2.清空整个redis缓存flushall flushall

redis如何清空缓存

前言: 如果你们的项目用到redis啦,虽然设置了过期时间,但有时候修改bug,仍然需要及时清空缓存,去读数据库的数据,所以这篇文章讲解如何在linux下清除redis的缓存。 正文: 1.首先进到redis的安…

redis如何清空当前缓存和所有缓存

Windows环境下使用命令行进行redis缓存清理 1、redis安装目录下输入cmd 2、redis-cli -p 端口号 3、flushdb 清除当前数据库缓存 4、flushall 清除整个redis所有缓存 客户端直接 右键即可:

redis 清理缓存

----windos 方法1,重启redis也能请缓存。 方法2,清缓存前确保redis-server.exe进程已经启动,然后打开redis-cli.exe,跳出的CMD里面输入flushall,显示OK就可以了。 flushall:清空整个redis 服务器的数据(删除…

redis清理缓存

redis如何清空缓存 如果你们的项目用到redis啦,虽然设置了过期时间,但有时候修改bug,仍然需要及时清空缓存,去读数据库的数据,所以这篇文章讲解如何清除redis的缓存。 正文 1.首先进到redis的安装目录,进…

WPS为公式编号

首先打开标尺,看一下中间位置(放公式),和后面位置(放编号)的刻度是多少,我中间位置目测是17个字符,后面位置是38个字符 为你的公式新建一个样式 这里主要注意两个地方,“制表位”和“段落”…

mathtype自动设置公式编号及更新

在写论文的时候难免要用到mathtype,mathtype相对来说还是好用的 使用mathtype的好处之一是可以利用mathtype自动设置公式编号及更新,非常方便,而且可以引用公式,在删除公式时不用手动更新公式编号,mathtype可以自动帮你…

使用MathType为公式自动编号

使用MathType为公式自动编号 公式格式设置修改章节号 公式格式设置 首先设置编号格式,在这里我们先将公式设置成(1-1-1)这样的,后续再讲如何修改成(1-1),看这两者有什么区别; 插入…

Mathtype怎么设置公式编号 ?公式编号怎么自动更新?删除新增公式后编号自动更新?

文章目录 1 公式编号设置2 插入公式3 公式编号怎么自动更新3.1 正常插入公式 编号自动更新3.2 文章中 增加公式 编号自动更新3.3 文章中 删除公式 编号自动更新3.4 文中 引用的公式编号 及 自动更新 以插入下面公式为例, 假如现在在第二单元,插入第一公式…

中值滤波器 C++ 实现

均值滤波是像素周围的33的像素做平均值操作, 那么中值就是在33中的像素中寻找中值 一般来说这个中值滤波是去除椒盐噪声的非常理想的选择。 /** ** method to remove noise from the corrupted image by median value * param corrupted input grayscale binary ar…

基于opencv,C++实现中值滤波器

基于opencv,C实现中值滤波器 目的:去除图像的椒盐噪声 原理:它将每一像素点的灰度值设置为该点某邻域窗口内的所有像素点灰度值的中值 伪代码: 输入:原图像、目标图像、核的半径 (1).判断原图像是否为空,…

python自编中值滤波器

使用python实现图像的中值滤波 椒盐噪声处理的图片: import numpy as np import matplotlib.pyplot as plt from skimage import io def mediafil(img,m): #滤波后会缺失边缘,先对原图进行0 paddingimg1np.zeros((img.shape[0]m-1,img.shape[1]m-1))im…

空间滤波-随机椒盐噪声-高斯噪声-均值滤波器-中值滤波器

文章目录 1 随机椒盐噪声2 高斯噪声3 均值滤波器4 中值滤波器 1 随机椒盐噪声 椒噪声:灰度值为0的噪声点,黑噪声 盐噪声:灰度值为255的噪声点,白噪声 思路:获取图像长、宽、通道数,在每个通道矩阵中随机产…

《数字图像处理》手动实现修正的α均值滤波+手动实现自适应中值滤波器

1 修正的α均值滤波实现 1.1 修正的α均值滤波原理 假设在邻域S_xy内去掉g(x,t)最低灰度值的d/2和最高的灰度值的d/2。令g_r (x,t)代表剩下的mn-d个像素。由这些剩余的像素的平均值形成的滤波器就称为修正的α均值滤波器: 其中,d的取值范围可以为0到m…

3D点云处理:半径滤波器中值滤波器

文章目录 0. 效果1. 半径滤波器1.1. 半径滤波器基本内容1.2 pcl实现2. 中值滤波器2.1.中值滤波器基本内容2.2 pcl实现3. 参考0. 效果 红色点云为处理的点云;白色为滤除的点云。 1. 半径滤波器 1.1. 半径滤波器基本内容 设置目标点半径范围内最少点数,如果少于该点数,则认为…

中值滤波器和双边滤波器(python实现)

文章目录 1.中值滤波(1)函数(2)代码 2.双边滤波(1)函数讲解(2)关于d,sigmaColor和sigmaSpace的值选择(3)代码实现 1.中值滤波 优点:对椒盐噪声处…

自适应中值滤波器(基于OpenCV实现)

转自:http://blog.csdn.net/brookicv/article/details/54931857 本文主要介绍了自适应的中值滤波器,并基于OpenCV实现了该滤波器,并且将自适应的中值滤波器和常规的中值滤波器对不同概率的椒盐噪声的过滤效果进行了对比。最后,对中…