1.1缓存工具的定位和选择
方法1和方法3配对起来解决一些常见的缓存问题,方法2和方法4配对起来是为了解决热点key的。
1.2设置逻辑过期时间方法和设置TTL过期时间
public class CacheClient { private 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){ //设置逻辑过期,把value封装到redisData对象里面去存,就能诞生逻辑过期时间了 RedisData redisData =new RedisData(); redisData.setData(value); redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time))); //plusSeconds是加上时间的,unit是单位,time是传进来的时间参数,但是不能确定是秒,所以需要用toSeconds去转一下 //写入Redis stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData),time,unit); }1.2.1设置TTL过期时间
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
因为stringRedisTemplate要求的类型是string,而传value的类型是Object,所以需要把对象序列化为json字符串。
1.2.2设置逻辑过期时间方法
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
//设置逻辑过期,把value封装到redisData对象里面去存,就能诞生逻辑过期时间了
RedisData redisData =new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//plusSeconds是加上时间的,unit是单位,time是传进来的时间参数,但是不能确定是秒,所以需要用toSeconds去转一下
//写入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData),time,unit);
}
1.3用缓存空值解决缓存击穿
public <R,ID> R queryWithPassThrough( String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit){ String key =CACHE_SHOP_KEY+ id;//把这个转为字符串,方便使用,前面标明cache shop 方便区分id //1.从redis查缓存 String shopJson = stringRedisTemplate.opsForValue().get(key);//以店铺id做为key,这样就得到店铺的json字符串 //2.判断是否存在 if (StrUtil.isNotBlank(shopJson)) { //3.存在,直接返回 return JSONUtil.toBean(shopJson, type);//R的类型是type,所以反序列化这边放的是type } //判断命中的是否是空值 if(shopJson !=null){ //返回一个错误信息 return null; } //4.不存在,根据id查询数据库 R r= dbFallback.apply(id);//因为范型这边的查数据库语句不好写,不像id,name这种数据库都有的字段,所以要定义一个函数,从外面传 //5.不存在,返回错误 if (r==null) { //将空值写入redis stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES); //返回错误信息 return null; } //6.存在,写入redis this.set(key,r, time, unit); //这个时间,注意,应该是传的时间,而不是直接写死的 //因为用的是opsForValue,所以是value是string类型的,所以后面的shop还得转为string; //7.返回 return r; }因为是封装的缓存工具,所以不确定返回值类型是什么,所以要用泛型,用R来代表返回值, Class<R> type这个是泛型的推荐,方便将来告诉具体是什么类型,R的类型就是type。id的类型不一定是long类型的,不能写死,所以id的类型也不确定,所以也要用泛型。 R r= dbFallback.apply(id);因为范型这边的查数据库语句不好写,不像id,name这种数据库都有的字段,所以要定义一个函数,从外面传,所以这里的代码是一个数据库查询的逻辑。因为是个有参有返回的函数,所以用的是Function,dbFallback是数据库的后备逻辑,在这段代码里面的意思是先查redis,redis查失败了,才走它,所以是后备逻辑。
Shop shop =cacheClient.queryWithPassThrough (CACHE_SHOP_KEY,id,Shop.class, this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);1.4利用逻辑过期时间解决缓存击穿
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;//把这个转为字符串,方便使用,前面标明cache shop 方便区分id //1.从redis查缓存 String json = stringRedisTemplate.opsForValue().get(key);//以店铺id做为key,这样就得到店铺的json字符串 //2.判断是否存在 if (StrUtil.isBlank(json)) { //3.存在,直接返回 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获取互斥锁 String lockKey = LOCK_SHOP_KEY + id; boolean isLock = tryLock(lockKey); //6.2判断是否获取锁成功 if (isLock){ //TODO 6.3成功,开启独立线程,实现缓存重建 //注意:获取锁成功应该再次检测redis缓存是否过期,做DoubleCheck。如果存在则无需重建缓存 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(lockKey); } }); } //6.4.失败,返回过期商铺信息 return r; }Shop shop =cacheClient.queryWithLogicalExpire (CACHE_SHOP_KEY, id,Shop.class,this::getById,20L,TimeUnit.SECONDS);