封装缓存空对象解决缓存穿透与逻辑过期解决缓存击穿工具类
@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))); //写入Redis stringRedisTemplate.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){ //将空值写入redis stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES); return null; } //6.存在,写入redis this.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); //写入redis this.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);
@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 = 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){ //将空值写入redis stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL,TimeUnit.MINUTES); return null; } //6.存在,写入redis stringRedisTemplate.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); }