目录
封装代码
封装一个类似的工具类 CacheClient,该工具类可以处理缓存击穿、缓存雪崩等问题,包含缓存穿透、缓存击穿(逻辑过期)的解决方案。
缓存穿透解决方案
/**
* 缓存穿透解决方案
* @param keyPrefix 缓存键前缀
* @param id 数据 ID
* @param type 数据类型
* @param dbFallback 数据库查询函数
* @param time 缓存时间
* @param unit 时间单位
* @param <R> 返回数据类型
* @param <ID> 数据 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;
// 从 Redis 查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 判断是否存在
if (json != null) {
// 存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否是空值
if ("".equals(json)) {
// 返回错误信息
return null;
}
// 不存在,根据 ID 查询数据库
R r = dbFallback.apply(id);
// 不存在,将空值写入 Redis
if (r == null) {
// 将空值写入 Redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 存在,写入 Redis
this.set(key, r, time, unit);
return r;
}
缓存击穿解决方案
-
逻辑过期策略
/** * 缓存击穿解决方案(逻辑过期) * @param keyPrefix 缓存键前缀 * @param id 数据 ID * @param type 数据类型 * @param dbFallback 数据库查询函数 * @param expireTime 逻辑过期时间 * @param <R> 返回数据类型 * @param <ID> 数据 ID 类型 * @return 数据对象 */ public <R, ID> R queryWithLogicalExpire( String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long expireTime) { String key = keyPrefix + id; // 1. 从 Redis 查询缓存 String json = stringRedisTemplate.opsForValue().get(key); // 2. 判断是否存在 if (json == null) { // 3. 不存在,直接返回空 return null; } // 4. 命中,需要先把 JSON 反序列化为对象 RedisData redisData = JSONUtil.toBean(json, RedisData.class); R r = JSONUtil.toBean((JSONObject) redisData.getData(), type); LocalDateTime expireTime1 = redisData.getExpireTime(); // 5. 判断是否过期 if (expireTime1.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, expireTime); } catch (Exception e) { throw new RuntimeException(e); } finally { // 释放锁 unlock(lockKey); } }); } // 6.4 返回过期的店铺信息 return r; } -
互斥锁策略
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. 查询缓存 String json = stringRedisTemplate.opsForValue().get(key); if (json != null) return JSONUtil.toBean(json, type); // 2. 缓存未命中,尝试获取锁 String lockKey = LOCK_SHOP_KEY + id; R r; try { if (!tryLock(lockKey)) { // 2.1 未获取到锁,休眠后重试 Thread.sleep(50); return queryWithMutex(...); // 递归调用 } // 2.2 获取到锁,二次检查缓存(防止其他线程已重建) json = stringRedisTemplate.opsForValue().get(key); if (json != null) return JSONUtil.toBean(json, type); // 3. 查询数据库 r = dbFallback.apply(id); if (r == null) { // 缓存空值(防止穿透) stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } // 4. 写入缓存 set(key, r, time, unit); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { unlock(lockKey); } return r; }其余代码
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
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 expireTime) {...}
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class<R> type,
Function<ID, R> dbFallback, Long time, TimeUnit unit){...}
/**
* 设置缓存
* @param key 缓存键
* @param value 缓存值
* @param time 缓存时间
* @param unit 时间单位
*/
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
/**
* 设置带有逻辑过期时间的缓存
* @param key 缓存键
* @param value 缓存值
* @param expireSeconds 逻辑过期时间(秒)
*/
public void setWithLogicalExpire(String key, Object value, Long expireSeconds) {
// 设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
// 写入 Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
/**
* 尝试获取锁
* @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 static class RedisData {
private LocalDateTime expireTime;
private Object data;
public LocalDateTime getExpireTime() {
return expireTime;
}
public void setExpireTime(LocalDateTime expireTime) {
this.expireTime = expireTime;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
使用示例
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private CacheClient cacheClient;
@Override
public Result queryById(Long id) {
// 使用缓存穿透解决方案
Shop shop = cacheClient.queryWithPassThrough(
CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 或者使用缓存击穿(逻辑过期)解决方案
// Shop shop = cacheClient.queryWithLogicalExpire(
// CACHE_SHOP_KEY, id, Shop.class, this::getById, 10L);
if (shop == null) {
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
}
代码解释
-
queryWithPassThrough方法:用于解决缓存穿透问题,先从 Redis 查询缓存,若缓存为空则查询数据库,若数据库也不存在则将空值写入 Redis 缓存,避免后续请求重复查询数据库。 -
queryWithLogicalExpire方法:用于解决缓存击穿问题,使用逻辑过期策略,当缓存过期时,尝试获取互斥锁,获取成功则开启独立线程重建缓存,当前请求返回旧数据。 -
queryWithMutex方法:用于解决缓存击穿问题,使用互斥锁策略,当缓存过期时,尝试获取互斥锁,在互斥锁释放前阻塞其他线程,保证数据的强一致性 -
set方法:用于设置普通缓存,包含缓存键、缓存值、缓存时间和时间单位。 -
setWithLogicalExpire方法:用于设置带有逻辑过期时间的缓存,将数据和逻辑过期时间封装到RedisData对象中。 -
tryLock和unlock方法:用于获取和释放互斥锁,保证缓存重建的线程安全。
定义缓存枚举策略
步骤 1:定义缓存策略枚举
枚举中每个常量对应一种策略,并通过抽象方法统一执行逻辑。
public enum CacheStrategy {
PASS_THROUGH {
@Override
public <R, ID> R execute(CacheClient client, CacheQueryParams<ID, R> params) {
// 参数校验
if (params.time == null || params.unit == null) {
throw new IllegalArgumentException("PASS_THROUGH 策略需设置 time 和 unit");
}
return client.queryWithPassThrough(
params.keyPrefix, params.id, params.type,
params.dbFallback, params.time, params.unit
);
}
},
LOGICAL_EXPIRE {
@Override
public <R, ID> R execute(CacheClient client, CacheQueryParams<ID, R> params) {
if (params.expireTime == null) {
throw new IllegalArgumentException("LOGICAL_EXPIRE 策略需设置 expireTime");
}
return client.queryWithLogicalExpire(
params.keyPrefix, params.id, params.type,
params.dbFallback, params.expireTime
);
}
},
MUTEX_LOCK {
@Override
public <R, ID> R execute(CacheClient client, CacheQueryParams<ID, R> params) {
if (params.time == null || params.unit == null || params.lockTimeout == null) {
throw new IllegalArgumentException("MUTEX_LOCK 需设置 time, unit 和 lockTimeout");
}
return client.queryWithMutex(
params.keyPrefix, params.id, params.type,
params.dbFallback, params.time, params.unit, params.lockTimeout
);
}
};
// 抽象方法,由每个枚举实现具体策略
public abstract <R, ID> R execute(CacheClient client, CacheQueryParams<ID, R> params);
}
步骤 2:封装参数对象
使用建造者模式创建参数对象,灵活支持不同策略的参数需求。
public class CacheQueryParams<ID, R> {
private final String keyPrefix;
private final ID id;
private final Class<R> type;
private final Function<ID, R> dbFallback;
private Long time; // 用于 PASS_THROUGH/MUTEX_LOCK
private TimeUnit unit; // 用于 PASS_THROUGH/MUTEX_LOCK
private Long expireTime; // 用于 LOGICAL_EXPIRE
private Long lockTimeout; // 用于 MUTEX_LOCK
// 私有构造方法,通过 Builder 构建
private CacheQueryParams(Builder<ID, R> builder) {
this.keyPrefix = builder.keyPrefix;
this.id = builder.id;
this.type = builder.type;
this.dbFallback = builder.dbFallback;
this.time = builder.time;
this.unit = builder.unit;
this.expireTime = builder.expireTime;
this.lockTimeout = builder.lockTimeout;
}
// 静态内部建造者类
public static class Builder<ID, R> {
private String keyPrefix;
private ID id;
private Class<R> type;
private Function<ID, R> dbFallback;
private Long time;
private TimeUnit unit;
private Long expireTime;
private Long lockTimeout;
public Builder<ID, R> keyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
return this;
}
public Builder<ID, R> id(ID id) {
this.id = id;
return this;
}
public Builder<ID, R> type(Class<R> type) {
this.type = type;
return this;
}
public Builder<ID, R> dbFallback(Function<ID, R> dbFallback) {
this.dbFallback = dbFallback;
return this;
}
public Builder<ID, R> time(Long time) {
this.time = time;
return this;
}
public Builder<ID, R> unit(TimeUnit unit) {
this.unit = unit;
return this;
}
public Builder<ID, R> expireTime(Long expireTime) {
this.expireTime = expireTime;
return this;
}
public Builder<ID, R> lockTimeout(Long lockTimeout) {
this.lockTimeout = lockTimeout;
return this;
}
public CacheQueryParams<ID, R> build() {
return new CacheQueryParams<>(this);
}
}
}
步骤 3:在 CacheClient 中实现统一查询入口
提供 query 方法,根据策略和参数执行对应逻辑。
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
// 其他代码...
public <R, ID> R query(
CacheStrategy strategy,
CacheQueryParams<ID, R> params
) {
return strategy.execute(this, params);
}
// 原有方法保持不变
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 expireTime
) { /* 原有逻辑 */ }
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class<R> type,
Function<ID, R> dbFallback, Long time, TimeUnit unit, Long lockTimeout
) { /* 新增互斥锁方案 */ }
}
步骤 4:调用示例
调用者通过建造者设置参数,并指定策略执行查询。
// 创建参数对象
CacheQueryParams<Long, Shop> params = new CacheQueryParams.Builder<Long, Shop>()
.keyPrefix("shop:")
.id(1L)
.type(Shop.class)
.dbFallback(id -> shopMapper.selectById(id))
.expireTime(30L) // LOGICAL_EXPIRE 所需
.time(30L) // PASS_THROUGH/MUTEX_LOCK 所需
.unit(TimeUnit.MINUTES)
.lockTimeout(10L) // MUTEX_LOCK 所需
.build();
// 执行查询
Shop shop = cacheClient.query(CacheStrategy.LOGICAL_EXPIRE, params);
设计优势
-
解耦策略与参数 通过参数对象封装不同策略的差异化参数,调用者无需关注内部细节。
-
类型安全与可扩展性 新增策略时只需扩展枚举和参数对象,无需修改现有代码。
-
清晰的错误提示 每个策略在执行前校验参数,明确提示缺失的必要参数。
-
友好的调用接口 建造者模式让参数设置更直观,避免长参数列表的混乱。
关键问题
在 Spring 框架里,RedisClient 有一个接收 StringRedisTemplate 参数的构造函数,即便你使用 @Autowired 注入 RedisClient 时没显式传入 StringRedisTemplate,它仍能完成实例化,这得益于 Spring 的依赖注入机制。下面详细解释其实现原理和可能的情况。
原理分析
Spring 在创建 Bean 实例时,若发现该类只有一个有参构造函数,会自动查找构造函数参数类型对应的 Bean 并注入。对于 RedisClient 类,Spring 会在容器中查找 StringRedisTemplate 类型的 Bean,然后将其作为参数传入构造函数来创建 RedisClient 实例。

8339

被折叠的 条评论
为什么被折叠?



