封装缓存空对象解决缓存穿透与逻辑过期解决缓存击穿工具类
@Slf4j @Component public class CacheClient {private final StringRedisTemplate stringRedisTemplate;public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}public void set(String key, Object value , Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, 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)));//写入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;//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查询数据库QueryWrapper<Shop> queryWrapper = new QueryWrapper<>();queryWrapper.eq("id", id);R r = dbFallback.apply(id);//5.不存在,返回错误if(r == null){//将空值写入redisstringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);return null;}//6.存在,写入redisthis.set(key, r, time, unit);//7.返回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)) {return null;}//4.命中,先把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();//5.判断缓存是否过期if(expireTime.isAfter(LocalDateTime.now())){//5.1未过期,直接返回店铺信息return r;}//5.2已过期,需要缓存重建//6.缓存重建//6.1获取互斥锁boolean isLock = tryLock(LOCK_SHOP_KEY + id);//6.2判断是否获取互斥锁成功if(isLock){//6.3成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() ->{try {//查询数据库R r1 = dbFallback.apply(id);//写入redisthis.setWithLogicalExpire(key, r1, time, unit);} catch (Exception e) {throw new RuntimeException(e);} finally {//释放锁unlock(LOCK_SHOP_KEY + id);}});}//6.4失败与成功,都返回过期的商铺信息return r;}//获取锁方法private boolean tryLock(String key){Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);//方法的返回值为基本类型,需将b进行拆箱,拆箱过程中可能会出现空指针异常,所以要使用工具类return BooleanUtil.isTrue(b);//会进行自动拆箱(当传入的值是 null 时,它会返回 false。可以避免空指针异常,这里没用到)}//释放锁方法private void unlock(String key){stringRedisTemplate.delete(key);} }
控制层调用
@GetMapping("/{id}")public Result queryShopById(@PathVariable("id") Long id) {return shopService.queryById(id);}
服务层调用
Result queryById(Long id);
@Overridepublic Result queryById(Long id){//缓存穿透 // Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id ,Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);//互斥锁解决缓存击穿//Shop shop = queryWithMutex(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);}
常量工具类
public class RedisConstants {public static final String LOGIN_CODE_KEY = "login:code:";public static final Long LOGIN_CODE_TTL = 2L;public static final String LOGIN_USER_KEY = "login:token:";public static final Long LOGIN_USER_TTL = 36000L;public static final Long CACHE_NULL_TTL = 2L;public static final Long CACHE_SHOP_TTL = 30L;public static final String CACHE_SHOP_KEY = "cache:shop:";public static final String LOCK_SHOP_KEY = "lock:shop:";public static final Long LOCK_SHOP_TTL = 10L; }
数据工具类
@Data public class RedisData {private LocalDateTime expireTime;private Object data; }
互斥锁解决缓存击穿
//互斥所解决缓存击穿public Shop queryWithMutex(Long id) {//1.从redis查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);//2.判断是否存在if (StrUtil.isNotBlank(shopJson)) {//3.存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//判断命中的是否是空值,不为null 就为“” 解决缓存穿透if(shopJson != null){return null;}Shop shop = null;try {//4.实现缓存重建//4.1 获取互斥锁boolean isLock = tryLock(LOCK_SHOP_KEY + id);//4.2 判断是否获取成功if(!isLock){//4.3 失败,则休眠并重试Thread.sleep(50);return queryWithMutex(id);}//4.4 成功,根据id查询数据库QueryWrapper<Shop> queryWrapper = new QueryWrapper<>();queryWrapper.eq("id", id);shop = shopMapper.selectOne(queryWrapper);//模拟重建的延时Thread.sleep(200);//5.不存在,返回错误if(shop == null){//将空值写入redisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);return null;}//6.存在,写入redisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {//7.释放互斥锁unlock(LOCK_SHOP_KEY + id);}//7.返回return shop;}//获取锁方法private boolean tryLock(String key){Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);//方法的返回值为基本类型,需将b进行拆箱,拆箱过程中可能会出现空指针异常,所以要使用工具类return BooleanUtil.isTrue(b);//会进行自动拆箱(当传入的值是 null 时,它会返回 false。可以避免空指针异常,这里没用到)}//释放锁方法private void unlock(String key){stringRedisTemplate.delete(key);}