前言:
阅读本文需要了解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、预防缓存穿透的方法
- ① 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封装;
public interface ShopMapper extends BaseMapper<Shop> {
}
- ② IShopService接口
其中,IService是业务逻辑层的CRUD封装,多了批量增、删、改的操作封装;
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、预防缓存击穿的方法
- ① 准备线程池
//线程池
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缓存实践的部分内容