对黑马点评中Redis缓存穿透与击穿解决方案的小理解
一.首先是封装后的代码Component Slf4j public class CacheClient { private final StringRedisTemplate stringRedisTemplate; public CacheClient(RedisTemplate redisTemplate, StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate stringRedisTemplate; } public void set(String key, Object value, long time, TimeUnit timeUnit) { stringRedisTemplate.opsForValue().set(key, value.toString(), time, timeUnit); } public R, ID R queryWithPassThrough(String keyPrefix, ID id, ClassR type, FunctionID,R dbFallback,Long time, TimeUnit timeUnit) { //先从reids看看有没有 String key keyPrefix id; String json stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(json)) { //有,直接返回 return BeanUtil.toBean(json, type); } //判断是不是空值 //要知道知道空值与空字符串不是一个,这里说的如果是后面再reddis中存的空字符串就直接报错,不可数据库上压力的可能 if (json ! null) { return null; } R r dbFallback.apply(id); if (r null) { //还要将空值写入redis,为了防止缓存穿透 stringRedisTemplate.opsForValue().set(key, , CACHE_NULL_TTL, TimeUnit.MINUTES); //正常返回 return null; } this.set(key, r, time, timeUnit); return r; } private static final ExecutorService CACHE_THREAD_POOL Executors.newFixedThreadPool(10); 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))); stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData)); } public R,ID R queryWithLogicalExpire(String keyPrefix, ID id, ClassR type,FunctionID,R dbFallback,Long time, TimeUnit timeUnit) { //先从reids看看有没有 String key keyPrefix id; String shopJson stringRedisTemplate.opsForValue().get(key); if (StrUtil.isBlank(shopJson)) { //有,直接返回 return null; } //由于下面设置的是属性的过期时间,所以要麻烦的取一下 //在Java中不方便操作JSON格式语句,先转换为通过toBean方法转换为RedisData对象,后面的字节码文件就是告诉toBean方法,按照RedisData类的属性模板来转换 RedisData redisData JSONUtil.toBean(shopJson, RedisData.class); //随后又通过get方法得到一个object的对象,但是object没有get,set方法,所以还要继续转成JSONObject类型的 JSONObject data (JSONObject) redisData.getData(); //随后又将JSONObject类型的对象,转换为Shop类型的对象,方便传回 R r JSONUtil.toBean(data, type); //时间的类型是LocalDateTime,直接与当前时间比较,判断是否过期即可 LocalDateTime expirTime redisData.getExpireTime(); //如果过期时间在当前时间之后,说明没有过期 if (expirTime.isAfter(LocalDateTime.now())) { //没有过期,直接返回 return r; } //只有过期了,才需要重建缓存 String lockKey LOCK_SHOP_KEY id; boolean islock tryLock(lockKey); if (islock) { CACHE_THREAD_POOL.submit(() - { //saveShop2Redis是当前类的方法,所以要使用this调用 try { R r1 dbFallback.apply(id); this.set(key, r1, time, timeUnit); } catch (Exception e) { throw new RuntimeException(e); } finally { unLock(lockKey); } }); } return r; } private boolean tryLock(String key) { Boolean flag stringRedisTemplate.opsForValue().setIfAbsent(key, 1, 10, TimeUnit.SECONDS); //防止空指针,如果是空指针的话会直接返回 false而不是抛异常 return BooleanUtil.isTrue(flag); } private void unLock(String key) { stringRedisTemplate.delete(key); } }1.关于开头的构造注入首先要明确StringRedisTemplate stringRedisTemplate是我通过Java程序与redis交流的工具类,但他光导包是没有用的,他的创建需要大量的配置,例如redis的密码等等,这些由spring boot来自动完成后创建完整可用的bean存在容器里,而我们要拿到spring管理的bean对象用import是没有用的,他只可以找到类而非最后的对象那我们要如何拿到这个bean对象呢?首先类上面的Component注解会让spring启动时吧当前类实例化成对象,存入容器中,在创建bean的时候spring也读构造函数,随后识别到StringRedisTemplate后将它对应的bean对象传入,为什么非要这个对象?一是需要其中已经配置好的连接设置,二是RedisTemplate的方法都是实例方法,只可以通过对象调用2.关于set方法的设置就是为了省事原来要写 stringRedisTemplate.opsForValue().set(user:1, userInfo.toString(), 30, TimeUnit.MINUTES);但是现在只要写cacheClient.set(user:1, userInfo, 30, TimeUnit.MINUTES);3.关于setWithLogicalExpire方法这个其实就是为了后面解做准备,缓存击穿就是同一个热点 key 过期的瞬间大量请求直接打到了数据库public void setWithLogicalExpire(String key, Object value, long time, TimeUnit unit) { //创建RedisData,这个类只有两个属性Object data真正要缓存的业务对象,LocalDateTime expireTime自定义的逻辑过期时间 RedisData redisData new RedisData(); //将你的数据放进包装对象 redisData.setData(value); //不直接设置TTL过期时间,而是直接设置一个逻辑过期时间点的属性 redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time))); //随后将整个RedisData转成JSON,存入redis stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData)); }4.与3相连的queryWithLogicalExpire方法(解决热点key缓存击穿)首先为了提高复用性,需要对返回值做一些变动//R,ID这个是声明了一个自定义的类型,R就是后面R的类型 public R,ID R queryWithLogicalExpire( String keyPrefix, ID id, // ID用作入参类型 ClassR type, // R代表要转换的实体类 FunctionID,R dbFallback, // 函数入参ID返回R Long time, TimeUnit timeUnit )解析双层 JSON 结构//第一层 JSON 转RedisData对象分出两块内容业务数据、逻辑过期时间 RedisData redisData JSONUtil.toBean(shopJson, RedisData.class); //redisData.getData()返回 Object,但是object无法解析内部字段,所以接着转JSONObject JSONObject data (JSONObject) redisData.getData(); //随后就是用方法toBean就是反序列化成需要的对象 R r JSONUtil.toBean(data, type); //同时取出预先存入的逻辑过期时间expirTime用于判断缓存是否失效 LocalDateTime expirTime redisData.getExpireTime();在判断缓存已经过期后就要进入异步更新的逻辑,首先是获取锁String lockKey LOCK_SHOP_KEY id; boolean islock tryLock(lockKey);随后开始异步更新//只有当前线程拿到锁才有下一步 if (islock) { //submit把括号里的任务交给新的子线程去跑主线程不会等待这段代码执行完毕,会直接跳出if,执行最后的return,返回旧数据,下面的子线程只进行大括号里面的 CACHE_THREAD_POOL.submit(() - { try{ //apply就是传入参数t后执行传入的逻辑 R r1 dbFallback.apply(id); //this就是当前对象实例,将最新的数据存进去 this.set(key, r1, time, timeUnit); } catch (Exception e) { throw new RuntimeException(e); //不论try里面的代码是不是对的,一定会执行内部代码 } finally { unLock(lockKey); } }); } //最后return r,但检测到过期的这一次还是旧数据5.关于queryWithMutex(同步互斥锁解决穿透)public Shop queryWithMutex(Long id) { //1. 查询缓存 String shopJson stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY id); if (StrUtil.isNotBlank(shopJson)) { //缓存命中直接返回 return BeanUtil.toBean(shopJson, Shop.class); } //2. 缓存是空字符串之前存的无效id标记防穿透 if (shopJson ! null) { return null; } //走到这里shopJson null缓存彻底过期/无缓存需要重建 String lockKey LOCK_SHOP_KEY id; Shop shop null; try { //3. 尝试获取分布式锁 boolean flag tryLock(lockKey); if (!flag) { //没抢到锁休眠50ms递归重新执行本方法再次查缓存 Thread.sleep(50); return queryWithMutex(id); } //4. 抢到锁查询数据库 shop getById(id); if (shop null) { //数据库无数据存入空字符串防穿透 stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY id, , CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } //5. 数据库有数据同步写入Redis缓存 stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { //6. 无论正常/异常强制释放锁防止死锁 unLock(lockKey); } return shop; }与queryWithLogicalExpire的区别互斥锁 缓存没了才上锁抢到锁才查库写缓存没抢到就等着重试能拦截不存在 id防穿透用户会卡顿 逻辑过期 缓存一直都在过期直接返回旧数据后台悄悄更新不用等用户无延迟没法拦截无效 id6.关于queryWithPassThrough(解决缓存穿透)还是用自定义泛型,方便复用主要注意空字符串与null