Bootstrap

Redis缓存技术(二)

缓存击穿

概念:缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

两种解决方案:(1)互斥锁;(2)逻辑过期

根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题

业务流程:

//获取锁
private boolean tryLock(string key){
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, value: "1", timeout: 10, TimeUnit.SEcONDs);
    return BooleanUtil.isTrue(flag);
}
//释放锁
private void unlock(string key){
    stringRedisTemplate.delete(key);
}
//互斥锁
public shop queryWithMutex(Long id){
    String key = CACHE_SHOP_KEY + id;
    //1.从redis查询商铺缓存
    String shopJson =stringRedisTemplate.opsForValue().get(key);
    //2.判断是否存在
    if(strUtil.isNotBlank(shopJson)){
        //3.存在,直接返回
        return JSONUtil.toBean(shopJson,Shop.class);
    }
    //判断命中的是否是空值
    if(shopJson != null){
        // 返回一个错误信息
        return null;
    }
    // 4.实现缓存重建
    // 4.1.获取互斥锁
    String lockKey = "lock:shop:" + id;
    Shop shop = null;
    try{
        boolean isLock = tryLock(lockKey);
        //4.2.判断是否获取成功
        if(!isLock){
            //4.3.失败,则休眠并重试
            Thread.sleep(50);
            return queryWithMutex(id);
        }
        // 4.4.成功,根据id查询数据库
        Shop shop = getById(id),
        // 5.不存在,返回错误
        if(shop == null){
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key,value:"",CACHE_NULL TTL, TimeUnit.MINUTES);
            //返回错误信息
            return null;
        }
        //6.存在,写入redis
        stringRedisTemplate.opsForValue().set(key, JsoNutil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
    }catch(InterruptedException e){
        throw new RuntimeException(e);
    }finally{
        //7.释放互斥锁
        unlock(lockKey);
    }
    //8.返回
    return shop;
}
    

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

public void saveShop2Redis(Long id, Long expireseconds){
    //1.查询店铺数据
    Shop shop = getById(id);
    //2.封装逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    //3.写入Redis
    stringRedisTemplate.opsForValue().set(cACHE SHOP_KEY + id, JsONUtil.toJsonstr(redisData));
}

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newfixedThreadPool( nthreads: 10);

public shop queryWithLogicExpire(Long id){
    String key = CACHE_SHOP_KEY + id;
    //1.从redis查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    //2.判断是否存在
    if(strUtil.isNotBlank(shopJson)){
        //3.存在,直接返回
        return null;
    }
    //4.命中,需要先把json反序列化为对象
    RedisData redisData = JSONUtil,toBean(shopJson, RedisData.class);
    Shop shop = JsoNutil.toBean((JsoNobject)redisData.getData(),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:" + id;
    boolean isLock = tryLock(lockKey);
    //6.2.判断是否获取锁成功
    if(isLock){
        //6.3.成功,开启独立线程,实现缓存重建
        CACHE_REBUILD_EXECUTOR.submit((){
        try {
            // 重建缓存
            this.saveShop2Redis(id,expireSeconds: 20L);
        }catch(Exceptione){
            throw new RuntimeException(e);
        }finally {
            // 释放锁
            unlock(lockKey);
        });
    //6.4 返回过期的商铺信息
    return shop;
}

优惠券秒杀

每个店铺都可以发布优惠券:

当用户抢购时,就会生成订单并保存到tb_voucher(优惠券)_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:

  • id的规律性太明显
  • 受单表数据量的限制

解决方法:全局ID生成器,一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

为了增加ID的安全性,可以不直接使用Redis自增的数值,而是拼接一些其它信息:

ID的组成部分:

  • 符号位:1bit,永远为0
  • 时间戳:31bit,以秒为单位,可以使用69年
  • 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

Redis实现全局唯一ID:

private static final lOng BEGIN_TIMESTAMP = 1640995200L;
private static final int COUNT_BITS = 32;
private stringRedisTemplate stringRedisTemplate;
public RedisIdWorker(stringRedisTemplate stringRedisTemplate){
    this.stringRedisTemplate=stringRedisTemplate;
}
public long nextId(string keyPrefix){
    // 1.生成时间戳
    LocalDateTime now = LocalDateTime.now();
    long nowSecond = now.toEpochSecond(Zone0ffset.UTC):
    long timestamp = nowSecond-BEGIN_TIMESTAMP;
    //2.生成序列号
    //2.1.获取当前日期,精确到天
    String date = now,format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
    // 2.2.自增长
    long count = stringRedisTemplate.opsForValue().increment( key: "icr:" + keyPrefix + ":" + date);
    // 3.拼接并返回
    return timestamp << COUNT_BITS | count;
}

全局唯一ID生成策略:

  • UUID
  • Redis自增
  • snowflake算法
  • 数据库自增

Redis自增ID策略:

  • 每天一个key,方便统计订单量
  • ID构造是时间戳+计数器

;