前言
用Redis完成登录界面后,我们实现了点评项目的店铺查询功能为并基于Redia解决了缓存穿透,缓存雪崩,缓存击穿等一系列Java并发 问题,可以发现我们的代码会不会太长太麻烦了,每一个查询功能都要有这么长的代码真的很麻烦,那我们可以封装起来,用的时候直接调用。
🤪先来看看要封装的方法是什么?

🙌🙌🙌方法1与方法3配合起来,负责常见的普通的缓存,用于解决缓存穿透 ;方法2与方法4结合,负责热点缓存,用于解决缓存击穿
了解封装的方法,有利于我们读懂Java底层的源代码,其实逻辑都是一样的,只是传参,其中用到了Java泛型
来看看什么是 Java 泛型🙌🙌🙌🙌🙌🙌?
✅ 定义
泛型(Generics) 是 Java 5 引入的一种机制,允许在定义类、接口或方法时使用类型参数(Type Parameter),从而在编译期提供类型安全检查,避免运行时类型转换错误。
✅ 基本语法
泛型类:class Box { ... }
泛型方法: T get(T item) { ... }
通配符:List<?>、List<? extends Number>、List<? super Integer>
来看看封装好的工具类的方法:
java
package com.hmdp.utils;
import...
@Slf4j
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate ;
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 将数据加入Redis,并设置有效期
*
* @param key
* @param value
* @param timeout
* @param unit
*/
public void set(String key, Object value, Long timeout, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), timeout, unit);
}
/**
* 设置缓存和逻辑过期时间
*
* @param key key前缀
* @param value
* @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));
}
/**
* 根据id从缓存中查找数据(缓存参透)
* @param keyPrefix Key前缀
* @param id
* @param type 返回数据类型
* @param dbFallback 数据库查询方法
* @param time 有效期
* @param unit 有效期的时间单位
* @return
* @param <R>
* @param <ID>
*/
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)) {
return JSONUtil.toBean(json, type);
}
//判断是否是空值
if (json == null) {
//返回错误
return null;
}
//3.不存在,根据id查询数据库
R r = dbFallback.apply(id);
//4.数据库不存在,返回错误
if (r == null) {
//将空值写入Redis
stringRedisTemplate.opsForValue().set(key, "",CACHE_NULL_TTL , TimeUnit.MINUTES);
return null;
}
//5.存在,写入Redis
this.set(key, r, time, unit);
//6.返回
return r;
}
/*
创建线程池
*/
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/*
缓存重建(逻辑过期)
*/
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 data = (JSONObject) redisData.getData();
R r = JSONUtil.toBean(data, type);
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
//5.1 未过期,直接返回
return r;
}
//5.2 处于过期时间,需要缓存重建
//6.缓存重建
//6.1 获取互斥锁
String lock = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lock);
//6.2 判断是否获取成功
if (isLock) {
//6.3 成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//重建缓存
//1.查询数据库
R r1 = dbFallback.apply(id);
//2.写入Redis
this.setWithLogicalExpire(key, r1, time , unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//释放锁
unLock(lock);
}
});
}
//6.3 成功,返回结果
//6.4 失败,返回错误
//7.返回
return r;
}
/*
尝试获取锁
*/
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/*
释放锁
*/
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
}
java
/**
* 根据id查询商铺数据
*
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
// 调用解决缓存穿透的方法
// Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class,
// this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
// if (Objects.isNull(shop)){
// return Result.fail("店铺不存在");
// }
// 调用解决缓存击穿的方法
Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class,
this::getById, CACHE_SHOP_TTL, TimeUnit.SECONDS);
if (Objects.isNull(shop)) {
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
在 CacheClient 类中,泛型主要出现在两个核心方法中:
java
public <R, ID> R queryWithPassThrough(
String keyPrefix,
ID id,
Class<R> type,
Function<ID, R> dbFallback,
Long time,
TimeUnit unit
)
public <R, ID> R queryWithLogicalExpire(
String keyPrefix,
ID id,
Class<R> type,
Function<ID, R> dbFallback,
Long time,
TimeUnit unit
)
1. <R, ID> 是什么?
这是方法级别的泛型声明,表示:
R:返回值的类型(Result),比如 Shop、User 等业务实体。
ID:主键的类型(Identifier),比如 Long、String、Integer。
2. 参数 Class type 的作用
-
用于 反序列化时指定目标类型。
-
因为 Java 的泛型存在 类型擦除(Type Erasure),运行时无法直接知道 R 是什么类型。
-
所以需要显式传入 Class(如 Shop.class)来告诉 JSONUtil.toBean():请把 JSON 转成 Shop 对象。
✅ ShopService类中的示例调用:
java
// 调用解决缓存击穿的方法
Shop shop = cacheClient.queryWithLogicalExpire(
CACHE_SHOP_KEY,
id,
Shop.class,
this::getById,
CACHE_SHOP_TTL,
TimeUnit.SECONDS
);
3. 参数 Function<ID, R> dbFallback 的含义
- 这是一个 函数式接口(Java 8+),表示"根据 ID 查询数据库"的逻辑。
- Function<ID, R>:输入 ID,输出 R。
- 使用 Lambda 表达式传入数据库查询逻辑,解耦缓存逻辑与具体业务。
本文是学习黑马程序员---黑马点评项目的课程笔记,小白啊!!!写的不好轻喷啊🤯如果觉得写的不好,点个赞吧🤪(批评是我写作的动力)
...。。。。。。。。。。。...
...。。。。。。。。。。。...