基于StringRedisTemplate
封装一个缓存工具类,满足以下需求:
- 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
- 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间,用于处理缓存击穿问题
- 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方法解决缓存穿透问题
- 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
方法1,方法3对应是普通的缓存,解决缓存穿透,设置TTL
方法2,方法4结合起来是解决热点key,解决击穿问题.
package com.hmdp.utils;import lombok.Data;import java.time.LocalDateTime;@Data
public class RedisData {private LocalDateTime expireTime;//逻辑过期时间private Object data;//万能存储数据的对象 对原来实体不需要修改 也可以使用继承
}
package com.hmdp.utils;import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;@Slf4j
@Component //将来这个bean由Spring维护
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}//方法1public void set(String key, Object value, Long time, TimeUnit unit){stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);}//方法2public 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)));//写入redisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}//方法三public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R>type, Function<ID,R> dbFallback,Long time, TimeUnit unit){String key = keyPrefix + id;//查询缓存String json = stringRedisTemplate.opsForValue().get(key);//判断是否存在if(StrUtil.isNotBlank(json)){//存在,直接返回return JSONUtil.toBean(json,type);}//判断是否为空值if(json!=null){//返回一个错误信息return null;}//不存在,根据id查询数据库//传入函数有参有返回值的函数Function 参数类型ID 返回值类型R//函数式编程 就是定义的时候参数传了个接口,用到时传入接口的实现类R r = dbFallback.apply(id);//不存在,返回错误if(r==null){//将空值写入redisstringRedisTemplate.opsForValue().set(key,"",time,unit);//返回错误信息return null;}this.set(key,r,time,unit);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查询商铺缓存 也可以用springcacheString 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 lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);//6.2 判断是否获取锁成功//注意: 获取锁成功应该再次检测redis缓存是否过期,做DoubleCheck.如果存在则无需重建缓存if(isLock){//6.3 成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() ->{//查询数据库R apply = dbFallback.apply(id);//写入redisthis.setWithLogicalExpire(key,apply,time,unit);});}//6.4 返回过期商铺信息return r;}private boolean tryLock(String key){Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);//需要转化为基本类型返回 如果直接return flag是有可能发生拆箱的,可能出现空指针return BooleanUtil.isTrue(flag);}private void unlock(String key){stringRedisTemplate.delete(key);}}
应用:
@Resourceprivate CacheClient cacheClient;@Overridepublic Result queryById(Long id) {//缓存穿透//Shop shop = queryWIthPassThrough(id);
// id2 -> getById(id2) == this::getById
// Shop shop = cacheClient.
// queryWithPassThrough(CACHE_SHOP_KEY,id,Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);//互斥锁解决缓存击穿
// Shop shop = queryWithMutex(id);//逻辑过期解决缓存击穿
// Shop shop = queryWithLogicalExpire(id);Shop shop =cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY,id, Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);if (shop==null) {return Result.fail("店铺不存在!");}return Result.ok(shop);}