Skip to content
DAILY QUOTE

“ ”

需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题。

  • 使用逻辑过期改造queryById()方法,解决缓存击穿
java
/**
 * 根据id查询数据。
 *
 * 作用:
 * 1.逻辑过期解决缓存击穿;
 *
 * @param id业务id
 * @return处理结果
 */
@Override
public Result queryById(Long id) {
    //逻辑过期解决缓存击穿
    Shop shop=queryWithLogicalExpire(id);

    if(shop == null){
        return Result.fail("店铺不存在");
    }
    return Result.ok(shop);
}

/**
 * 使用逻辑过期解决缓存击穿查询。
 *
 * 作用:
 * 1.从Redis查询商铺缓存;
 * 2.判断是否存在;
 * 3.不存在,直接返回;
 * 4.命中,需要先把Json反序列化为对象;
 * 5.判断是否过期;
 *
 * @param id业务id
 * @return处理结果
 */
public Shop queryWithLogicalExpire(Long id){
    String key= CACHE_SHOP_KEY+id;
    //1.从Redis查询商铺缓存
    String shopJson=stringRedisTemplate.opsForValue().get(key);
    //2.判断是否存在
    if(StrUtil.isBlank(shopJson)){
        //3.不存在,直接返回
        return null;
    }
    //4.命中,需要先把Json反序列化为对象
    RedisData redisData=JSONUtil.toBean(shopJson,RedisData.class);
    JSONObject data=(JSONObject) redisData.getData();
    Shop shop=JSONUtil.toBean(data,Shop.class);
    LocalDateTime expireTime=redisData.getExpireTime();
    //5.判断是否过期
    if(expireTime.isAfter(LocalDateTime.now())){
        //5.1.未过期,直接返回店铺信息
        return shop;
    }
    //5.2.已过期,需要缓存重建
    //6.缓存重建
    //6.1.获取互斥锁
    String lockKey=LOCK_SHOP_KEY+id;
    boolean isLock=tryLock(lockKey);
    //6.2.判断是否获取锁成功
    if(isLock){
        //6.3.成功,开启独立线程,实现缓存重建
        CACHE_REBUILD_EXECUTOR.submit(()->{
            try {
                //重建缓存
                this.saveShop2Redis(id,20L);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                //释放锁
                unlock(lockKey);
            }
        });
    }
    //6.4.返回过期的商铺信息
    return shop;
}

/**
 * 执行saveShop2Redis相关业务逻辑。
 *
 * 作用:
 * 1.查询店铺数据;
 * 2.封装逻辑过期时间;
 * 3.写入Redis;
 *
 * @param id业务id
 * @param expireSeconds expireSeconds参数
 * @return无返回值
 */
public void saveShop2Redis(Long id,Long expireSeconds) throws InterruptedException {
    //1.查询店铺数据
    Shop shop=getById(id);
    Thread.sleep(200);
    //2.封装逻辑过期时间
    RedisData redisData=new RedisData(); //如果直接把Shop存进Redis,里面是没有“过期时间”这个字段的,不能为了存Redis,去修改数据库表对应的实体类
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds)); //以现在的系统时间为起点,往后推 expireSeconds 秒
    //3.写入Redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}

测试:

先对缓存进行热点数据预热,预热数据的逻辑过期时间设置了10秒,所以等待10秒缓存逻辑过期后,在数据库中修改id为1的店铺数据(热点数据),使用Jmeter进行压力测试(可以将重建缓存的过程延迟1秒钟,让效果更加明显),第一次查询结果还是旧数据,1秒左右以后返回的缓存数据发生了更新(因为此时缓存已经重建好了),并且访问数据库只重建了一次缓存,验证了并发安全问题。