1.封装Redis工具类
基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
2.CacheClient工具类的实现
2.1各个方法的代码实现
@Slf4j
@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;
}
方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时
-
set方法:- 用于普通的缓存设置。
- 可以指定缓存的过期时间和时间单位(如秒、分钟、小时等)。
- 用途: 适用于普通的数据缓存,满足基本的缓存需求。
/**
* 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
* @param value
* @param key
* @param time
* @param unit
*/
public void set(Object value, String key, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间
-
setWithLogicalExpire方法:- 用于设置带逻辑过期时间的缓存数据。
- 在缓存中存储数据时同时包含一个逻辑过期时间字段。
- 用途: 适用于防止缓存击穿,确保在缓存过期时能够返回旧数据并异步刷新。
/**
* 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓
* @param value
* @param key
* @param time
* @param unit
*/
public void setWithLogicalExpire(Object value, String key, 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));
}
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
-
queryWithPassThrough方法:- 实现了缓存穿透的解决方案。
- 当缓存中没有数据时,从数据库中查询并缓存结果;如果数据库中也没有数据,则将空值写入缓存,避免缓存穿透。
- 用途: 防止无效请求频繁穿透缓存访问数据库。
/**
* 缓存穿透
* @return
*/
public <R,ID> R queryWithPassThrough(String pre, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){
String key = pre + id;
//1.从redis中查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
//2.判断是否命中
if (StrUtil.isNotBlank(json)) {
//3.命中 返回商铺信息
return JSONUtil.toBean(json,type);
}
//判断是否为null值
if(json != null){
return null;
}
//4.未命中 根据id查询数据库
R r = dbFallback.apply(id);
if (r == null) {
//5.将空值存入redis
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
//5.存在 返回
return null;
}
//6.存在 写入redis 返回商品信息
this.set(r,pre,time,unit);
return r;
}
方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
-
queryWithLogicalExpire方法:- 使用逻辑过期解决缓存击穿问题。
- 当缓存数据过期时,通过异步线程更新缓存,同时仍然返回旧数据,保证系统的高可用性。
- 用途: 适用于热点数据,防止在缓存失效时大量请求同时涌向数据库。
/**
* 利用逻辑过期时间解决缓存击穿
* @return
*/
public <R,ID> R queryWithLogicalExpire(String pre,ID id,Class<R> type,Function<ID,R> dbFallback,Long time, TimeUnit unit){
String key = pre + id;
//1.从redis中查询缓存
String Json = stringRedisTemplate.opsForValue().get(key);
//2.不存在 直接返回空(不是热点数据)
if(StrUtil.isBlank(Json)){
return null;
}
//3.存在 需要先把Json反序列化为对象
RedisData redisData = JSONUtil.toBean(Json, RedisData.class);
JSONObject data =(JSONObject) redisData.getData();
R r = JSONUtil.toBean(data,type);
//4.判断缓存是否过期
LocalDateTime expireTime = redisData.getExpireTime();
if(expireTime.isAfter(LocalDateTime.now())){
//5.未过期 返回商铺信息
return r;
}
//6.已过期 缓存重建
//6.1获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isTryLock = tryLock(lockKey);
//6.2判断是否获取成功
if (isTryLock) {
//6.3成功
//再次检查缓存中的数据是否过期,如果没有过期,无需重建
//1.从redis中查询缓存
Json = stringRedisTemplate.opsForValue().get(key);
//2.不存在 直接返回空(不是热点数据)
if(StrUtil.isBlank(Json)){
return null;
}
//3.存在 需要先把Json反序列化为对象
redisData = JSONUtil.toBean(Json, RedisData.class);
data =(JSONObject) redisData.getData();
r = JSONUtil.toBean(data, type);
//4.判断缓存是否过期
expireTime = redisData.getExpireTime();
if(expireTime.isAfter(LocalDateTime.now())){
//5.未过期 返回商铺信息
return r;
}
//还过期 开启独立线程 缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//先查数据库
R r1 = dbFallback.apply(id);
//再写入Redis
this.setWithLogicalExpire(r1,key,time,unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unLock(lockKey);
}
});
}
//6.4返回商铺信息
return r;
}
/**
* 获取锁
* @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);
}
2.2完整代码
@Slf4j
@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;
}
/**
* 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
* @param value
* @param key
* @param time
* @param unit
*/
public void set(Object value, String key, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
/**
* 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓
* @param value
* @param key
* @param time
* @param unit
*/
public void setWithLogicalExpire(Object value, String key, 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));
}
/**
* 缓存穿透
* @return
*/
public <R,ID> R queryWithPassThrough(String pre, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){
String key = pre + id;
//1.从redis中查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
//2.判断是否命中
if (StrUtil.isNotBlank(json)) {
//3.命中 返回商铺信息
return JSONUtil.toBean(json,type);
}
//判断是否为null值
if(json != null){
return null;
}
//4.未命中 根据id查询数据库
R r = dbFallback.apply(id);
if (r == null) {
//5.将空值存入redis
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
//5.存在 返回
return null;
}
//6.存在 写入redis 返回商品信息
this.set(r,pre,time,unit);
return r;
}
/**
* 获取锁
* @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);
}
/**
* 利用逻辑过期时间解决缓存击穿
* @return
*/
public <R,ID> R queryWithLogicalExpire(String pre,ID id,Class<R> type,Function<ID,R> dbFallback,Long time, TimeUnit unit){
String key = pre + id;
//1.从redis中查询缓存
String Json = stringRedisTemplate.opsForValue().get(key);
//2.不存在 直接返回空(不是热点数据)
if(StrUtil.isBlank(Json)){
return null;
}
//3.存在 需要先把Json反序列化为对象
RedisData redisData = JSONUtil.toBean(Json, RedisData.class);
JSONObject data =(JSONObject) redisData.getData();
R r = JSONUtil.toBean(data,type);
//4.判断缓存是否过期
LocalDateTime expireTime = redisData.getExpireTime();
if(expireTime.isAfter(LocalDateTime.now())){
//5.未过期 返回商铺信息
return r;
}
//6.已过期 缓存重建
//6.1获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isTryLock = tryLock(lockKey);
//6.2判断是否获取成功
if (isTryLock) {
//6.3成功
//再次检查缓存中的数据是否过期,如果没有过期,无需重建
//1.从redis中查询缓存
Json = stringRedisTemplate.opsForValue().get(key);
//2.不存在 直接返回空(不是热点数据)
if(StrUtil.isBlank(Json)){
return null;
}
//3.存在 需要先把Json反序列化为对象
redisData = JSONUtil.toBean(Json, RedisData.class);
data =(JSONObject) redisData.getData();
r = JSONUtil.toBean(data, type);
//4.判断缓存是否过期
expireTime = redisData.getExpireTime();
if(expireTime.isAfter(LocalDateTime.now())){
//5.未过期 返回商铺信息
return r;
}
//还过期 开启独立线程 缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//先查数据库
R r1 = dbFallback.apply(id);
//再写入Redis
this.setWithLogicalExpire(r1,key,time,unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unLock(lockKey);
}
});
}
//6.4返回商铺信息
return r;
}
}
3.ShopServiceImpl
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private CacheClient cacheClient;
@Override
public Result queryById(Long id) {
//缓存穿透
//Shop shop = queryWithPassThrough(id);
//this::getById == id->getById(id)
//Shop shop
// = cacheClient
// .queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_NULL_TTL, TimeUnit.MINUTES);
//互斥锁解决缓存击穿
//Shop shop = queryWithMutex(id);
//逻辑过期解决缓存击穿
//Shop shop = queryWithLogicalExpire(id)
//逻辑过期解决缓存击穿
Shop shop
= cacheClient
.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);
if (shop == null) {
return Result.fail("店铺信息不存在");
}
return Result.ok(shop);
}
}
4.测试
TODO:使用逻辑过期解决缓存击穿问题,需要自行进行数据预热。
运行其中test2()即可
class HmDianPingApplicationTests {
@Autowired
private ShopServiceImpl shopService;
@Resource
private CacheClient cacheClient;
@Test
public void test() throws InterruptedException {
shopService.saveShop2Redis(1L,10L);
}
@Test
public void test2() throws InterruptedException {
Shop shop = shopService.getById(1L);
cacheClient.setWithLogicalExpire(shop, CACHE_SHOP_KEY + 1L,10L, TimeUnit.SECONDS);
}
}
&spm=1001.2101.3001.5002&articleId=144409265&d=1&t=3&u=b921d6bf1e574be1b924c54dc1803a15)
8339

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



