首页 > 数据库 >【Redis-缓存工具类:缓存穿透,缓存击穿,缓存雪崩】

【Redis-缓存工具类:缓存穿透,缓存击穿,缓存雪崩】

时间:2023-03-13 13:31:49浏览次数:57  
标签:缓存 String 过期 Redis param 雪崩 key id

前言:

阅读本文需要了解Java泛型以及lambda表达式的基础使用,会微量包含这些内容,但这些又是代码的一些关键。

  • 目录:
  • 一、Redis缓存相关工具类
  • 二、缓存穿透相关方法
  • 缓存穿透相关概念
  • 三、缓存击穿相关方法
  • 缓存击穿相关概念
  • 四、缓存雪崩(补充)
  • 缓存雪崩相关概念


一、Redis缓存相关工具类

1、基础依赖

  • ① redis相关依赖
    ​spring-boot-starter-data-redis​​:redis基础依赖;
    ​commons-pool2​​:redis连接池;
  • ② web相关依赖
    ​spring-boot-starter-web​​:SpringBoot的Web依赖;
  • ③ 数据库连接依赖
    ​mysql-connector-java​​:mysql连接依赖;
  • ④ mybatis-plus相关依赖
    ​mybatis-plus-boot-starter​​:MyBatis Plus的相关依赖;
  • ⑤ lombok相关依赖
    ​lombok​​:方便编写实体类;
  • ⑥ hutool相关依赖
    ​hutool-all​​:各种工具类的依赖;
  • ⑦ test相关依赖
    ​spring-boot-starter-test​​:测试相关依赖;


<dependencies>
<!--spring_data_redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--redis_pool-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--spring_boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mysql_connector-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>5.1.47</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--spring_boot_test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--mybatis_plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
</dependencies>

2、编写缓存工具类

  • ① @Slf4j注解
    方便打日志,省去​​private final Logger logger = LoggerFactory.getLogger(当前类名.class);​​;
  • ② @Component
    将工具类注册到Spring容器中,方便使用;
  • ③ StringRedisTemplate类
    Redis的模板类,其​​key​​和​​value​​的形式均为​​String类型​​。


@Slf4j
@Component
public class CacheClient {

private final StringRedisTemplate stringRedisTemplate;

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

... ...

}


二、缓存穿透相关方法

0、缓存穿透相关概念

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


常见的两种解决方案:Ⅰ、缓存空对象;Ⅱ、布隆过滤器。

注意:以下案例以缓存空对象为例。

1、保存任意Java类型对象到缓存,并设置过期时间

  • ① JSONUtil#toJsonStr方法
    ​toJsonStr方法​​可以将Java对象类型转换为String类型;
  • ② stringRedisTemplate#opsForValue#set方法
    ​​​set(K key, V value, final long timeout, final TimeUnit unit)方法​​​对应redis指令中的​​set key value ex time​​​。
    ​​​​​​
/**
* 设置任意Java对象的缓存过期时间
*
* @param key 缓存key
* @param value 缓存Java对象
* @param time 过期时间
* @param unit 过期时间单位
*/
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}

2、预防缓存穿透的方法

【Redis-缓存工具类:缓存穿透,缓存击穿,缓存雪崩】_redis


  • ① keyPrefix参数
    ​String key = keyPrefix + id;​​用来保证某一资源key的唯一性;
  • ② ID id
    由于id的数据类型可能是Long、Integer、String,所以此处使用泛型;
  • ③ R
    结果类型,查询缓存的数据类型不确定,也使用泛型;
  • ④ JSONUtil#toBean方法
    ​toBean(String jsonString, Class<T> beanClass)方法​​可以将JSON字符串转为实体类对象;
    由于参数中需要实体类对象的类型,所以我们传入​​Class<R> type​​;
  • ⑤ Function<ID, R> dbFallback参数
    从数据库中查询所需对象,这个函数会因结果对象不同而不同,所以我们也通过函数参数化传入函数参数;
  • ⑥ StrUtil#isNotBlank方法
    StrUtil.isNotBlank(null) // false
    StrUtil.isNotBlank("") // false
    StrUtil.isNotBlank(" \t\n") // false
    StrUtil.isNotBlank("abc") // true


/**
* 预防缓存穿透的方法
*
* @param keyPrefix key前缀
* @param id 查询id
* @param type 结果类型
* @param dbFallback 查询函数
* @param time 过期时间
* @param unit 过期时间单位
* @param <R> 结果类型泛型
* @param <ID> 时间类型泛型
* @return 结果
*/
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. 存在,直接返回
R r = JSONUtil.toBean(json, type);
return r;
}
//判断json是等于空值
if (json != null){ //即json等于""的情形
//结果不存在
return null;
}
//4. 从数据库中查询
R r = dbFallback.apply(id);
//4.1 在数据库中也不存在
if (r == null){
//将空值写入Redis中
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
//返回错误信息
return null;
}
//4.2 在数据库中存在,写入redis,返回信息
this.set(key, r, time, unit);
return r;
}

3、service类

  • ① ShopMapper接口
    其中,BaseMapper是DAO层的CRUD封装;

【Redis-缓存工具类:缓存穿透,缓存击穿,缓存雪崩】_缓存_02

public interface ShopMapper extends BaseMapper<Shop> {

}
  • ② IShopService接口
    其中,IService是业务逻辑层的CRUD封装,多了批量增、删、改的操作封装;

【Redis-缓存工具类:缓存穿透,缓存击穿,缓存雪崩】_json_03

public interface IShopService extends IService<Shop> {

Result queryShopById(Long id);

}
  • ③ ShopServiceImpl实现类
    其中,ServiceImpl 针对业务逻辑层的实现;


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

@Resource
private CacheClient cacheClient;

/**
* 根据id查询商铺信息
*
* @param id 商铺id
* @return 结果
*/
@Override
public Result queryShopById(Long id) {
//1. 通过缓存工具类调用预防缓存穿透的查询方法
Shop shop = cacheClient.queryWithPassThrough(
RedisConstants.CACHE_SHOP_KEY,
id,
Shop.class,
this::getById,
RedisConstants.CACHE_SHOP_TTL,
TimeUnit.MINUTES);
//2. 判断查询结果是否为null
if (shop == null){
return Result.fail("店铺不存在!");
}
//3. 返回结果
return Result.ok(shop);
}

}


三、缓存击穿相关方法

0、缓存击穿相关概念

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

常见的两种解决方案:Ⅰ、互斥锁;Ⅱ、逻辑过期。

注意:以下方案以逻辑过期为例。

1、设置任意Java对象的逻辑过期时间

  • ① RedisData类
    用于存储时间数据​​expireTime​​,以及对象数据​​data​​;


@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
  • ② LocalDateTime.now().plusSeconds方法
    给当前时间加上设置的过期时间;
  • ③ unit.toSeconds方法
    ​TimeUnit#toSeconds​​方法,将单位换算成秒;
/**
* 设置任意Java对象的逻辑过期时间
*
* @param key 缓存key
* @param value 缓存的Java对象
* @param time 过期时间
* @param 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))); //将对应单位转换成秒
// 写入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}

注意:设置逻辑过期,并没有真正的给缓存设置过期时间。


2、预防缓存击穿的方法

【Redis-缓存工具类:缓存穿透,缓存击穿,缓存雪崩】_redis_04


  • ① 准备线程池


//线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
  • ② 互斥锁准备
    ​BooleanUtil#isTrue方法​​处理Boolean结果拆箱为null的问题,返回boolean类型;
    stringRedisTemplate.opsForValue().setIfAbsent方法对应SETNX指令


/**
* 预防缓存击穿查询方法(互斥锁方案) 获取互斥锁
*
* @param key id
* @return 结果
*/
public boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_CACHE_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}

/**
* 预防缓存击穿查询方法(互斥锁方案) 释放互斥锁
*
* @param key id
*/
public void unlock(String key){
stringRedisTemplate.delete(key);
}
  • ③ queryWithLogicalExpire方法主体
  • Ⅰ keyPrefix参数
    String key = keyPrefix + id;用来保证某一资源key的唯一性;
  • Ⅱ ID id
    由于id的数据类型可能是Long、Integer、String,所以此处使用泛型;
  • Ⅲ R
    结果类型,查询缓存的数据类型不确定,也使用泛型;
  • Ⅳ JSONUtil#toBean方法
    toBean(String jsonString, Class<T> beanClass)方法可以将JSON字符串转为实体类对象;
    由于参数中需要实体类对象的类型,所以我们传入Class<R> type
    toBean(JSONObject json, Class<T> beanClass)方法可以将JSONObject对象转换成我们指定的对象类型;
  • Ⅴ Function<ID, R> dbFallback参数
    从数据库中查询所需对象,这个函数会因结果对象不同而不同,所以我们也通过函数参数化传入函数参数;
  • Ⅵ LocalDateTime#isAfter方法
    该方法用于判断当前时间(此处我们使用逻辑过期时间为此时间)是否晚于比较时间(此处我们获取当前时间为被对比的时间)
    即逻辑过期时间晚于当前时间,则不算过期;

注意:步骤3中判断为空即返回不存在,是因为业务中保存店铺信息时就会将店铺信息保存到Redis中。


if (StrUtil.isBlank(json)){
//3. 不存在,直接返回
return null;
}

完整方法如下:

/**
* 预防缓存击穿的方法(逻辑过期方案)
*
* @param keyPrefix key前缀
* @param id 查询id
* @param type 结果类型
* @param dbFallback 查询函数
* @param time 过期时间
* @param unit 过期时间单位
* @param <R> 返回结果类型泛型
* @param <ID> 查询id类型泛型
* @return 结果
*/
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. 判断缓存中是否存在数据
if (StrUtil.isBlank(json)){
//3. 不存在,直接返回
return null;
}
//4. 命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
JSONObject jsonObject = (JSONObject) redisData.getData();
R r = JSONUtil.toBean(jsonObject, type);
LocalDateTime expireTime = redisData.getExpireTime();
//5. 判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1 未过期,直接返回缓存信息
return r;
}
//5.2 已过期,需要缓存重建
//6. 缓存重建
String lockKey = keyPrefix + id;
//6.1 获取互斥锁
boolean isLock = tryLock(lockKey);
//6.2 判断是否获取锁成功
if (isLock){
//6.3 成功,开启独立线程,实现缓存重建
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.4 返回过期的商铺信息
return r;
}

可以看到,我们将重建缓存数据的任务交由线程池中的线程来完成了,单独看如下:

//6. 缓存重建
String lockKey = keyPrefix + id;
//6.1 获取互斥锁
boolean isLock = tryLock(lockKey);
//6.2 判断是否获取锁成功
if (isLock){
//6.3 成功,开启独立线程,实现缓存重建
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);
}
});
}

3、service类

注意:此处service接口、mapper接口省略了,具体可以看上面的缓存穿透方案。

/**
* 根据id查询商铺信息
*
* @param id 商铺id
* @return 结果
*/
@Override
public Result queryShopById(Long id) {
//5. 通过缓存工具类调用预防缓存击穿的方法(逻辑过期方案)
Shop shop = cacheClient.queryWithLogicalExpire(
RedisConstants.CACHE_SHOP_KEY,
id,
Shop.class,
this::getById,
RedisConstants.CACHE_SHOP_TTL,
TimeUnit.MINUTES);

if (shop == null){
return Result.fail("店铺不存在!");
}
//6. 返回结果
return Result.ok(shop);
}


四、缓存雪崩(补充)

0、缓存雪崩相关概念

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

常见的解决方案:

Ⅰ、 给不同的Key的TTL添加随机值;

Ⅱ、 利用Redis集群提高服务的可用性;

Ⅲ、 给缓存业务添加降级限流策略;

Ⅳ、 给业务添加多级缓存。


五、结尾

以上即为Redis缓存实践的部分内容



标签:缓存,String,过期,Redis,param,雪崩,key,id
From: https://blog.51cto.com/u_15874356/6117656

相关文章

  • 【MyBatis】关联映射和缓存机制
    实体类IdCard和Person自行创建:       ......
  • Redis(一)
    基础篇1.简介1.1NoSQLNoSql可以翻译做NotOnlySql(不仅仅是SQL),或者是NoSql(非Sql的)数据库。是相对于传统关系型数据库而言,有很大差异的一种特殊的数据库,因此也称之为非......
  • Redis Stream Commands 命令学习-1 XADD XRANGE XREVRANGE
    概况ARedisstreamisadatastructurethatactslikeanappend-onlylog.Youcanusestreamstorecordandsimultaneouslysyndicateeventsinrealtime.Exam......
  • keydb redis 兼容协议服务
    keydb是完全兼容redis协议的服务,同时支持了不少其他特性,比如多主,多复制,对于我们的集群环境部署简化了不少而且还有一个不错的优势是性能(利用了多线程提供了不错的性能)官......
  • redis之内存淘汰策略和过期删除策略
    前言redis是基于内存的,如果内存超过限定值(redis配置文件的maxmemory参数决定redis最大内存使用量),导致新的数据存不进去,此时redis会根据淘汰策略删除一些数据 一内存......
  • 复盘-记一次慢SQL导致的雪崩
    一.背景交代某NFT数藏平台于3月11日开启抽奖系统,做了社群推广等市场营销行为,期间市场负责人有联系技术负责人,询问是否需要升级服务等,技术负责人回复先观望;该......
  • NopCommerce中缓存学习
    最近把后台管理程序换成nop方式。在使用_productService.Update(M);时碰到问题,更新不成功。刚开始还以为是EF的问题,因为是先_productService.GetProductById(id),再upd......
  • redis
    为什么要选择redis因为redis快Redis为什么速度很快1.数据存放在内存中-------内存的读写速度是磁盘(数据库)的一百倍左右。2.用C语言实现------C语言更底层,执行速度相对......
  • 路飞:redis之列表(List)类型、redis之hash(字典)类型、redis其他方法(所有类型通用的方法)、r
    目录一、redis之列表(List)类型二、redis之hash(字典)类型三、redis其他方法(所有类型通用的方法)四、redis管道五、django中使用redis方式一方式二方案一方案二六、celery介绍......
  • 路飞:celery 执行异步任务,延迟任务,定时任务、django中使用celery、轮播图接口加缓存、
    目录一、celery执行异步任务,延迟任务,定时任务异步任务延迟任务定时任务二、django中使用celery2.1定时任务推荐使用的框架(了解)2.2秒杀功能2.2.1秒杀功能逻辑分析2.1.2......