采用Redis分布式锁限制食客

Java中,我们通常使用的synchronized或者Lock都是线程锁,对同一个JVM进程内的多个线程有效。因为锁的本质 是内存中存放一个标记,记录获取锁的线程是谁,这个标记对每个线程都可见。比如我们启动的多个秒杀服务,就是多个JVM,内存中的锁显然是不共享的,每个JVM进程都有自己的锁,自然无法保证线程的互斥了,这个时候我们就需要使用到分布式锁了

分布式锁条件

实现分布式锁要满足3点:多进程可见,互斥,可重入。

1、多进程可见

Redis本身就是基于JVM之外的,因此满足多进程可见的要求

2、互斥

同一时间只能有一个进程获取锁标记,我们可以通过redis的setnx实现(SETNX是SET if Not eXists的简写。),只有第一次执行的才会成功并返回1,其它情况返回0。setnx key value将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做

但是使用setnx命令设置锁会出现死锁情况,比如当我get lock以后出现了异常以后并没有将锁删除,而且这把锁也没有过期时间,因此其他请求就再也获取不到这把锁了,这就是死锁。于是,后来Redis对set指令进行了改进,可以添加过期时间。当然有人会使用expire指令将key进行过期,但这样就不能保证setnx和expire的原子操作了。

有些场景是不满足的,例如一个方法获取到锁之后,可能在方法内调这个方法此时就获取不到锁了。这个时候我们就需要把锁改进成可重入锁了。

3、可重入锁

实现原理是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1

引入Redisson分布式锁

添加依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.13.6</version>
</dependency>

引入对象

@Resource
private RedissonClient redissonClient;

完成改造

@Resource
private RedissonClient redissonClient;

/**
 * 抢购代金券
 *
 * @param voucherId   代金券 ID
 * @param accessToken 登录token
 * @Para path 访问路径
 */
@Transactional(rollbackFor = Exception.class)
public ResultInfo doSeckill(Integer voucherId, String accessToken, String path) {
    // 基本参数校验
    AssertUtil.isTrue(voucherId == null || voucherId < 0, "请选择需要抢购的代金券");
    AssertUtil.isNotEmpty(accessToken, "请登录");

    // ----------注释原始的走 关系型数据库 的流程----------
    // 判断此代金券是否加入抢购
    // SeckillVouchers seckillVouchers = seckillVouchersMapper.selectVoucher(voucherId);
    // AssertUtil.isTrue(seckillVouchers == null, "该代金券并未有抢购活动");

    // ----------采用 Redis 解问题----------
    String redisKey = RedisKeyConstant.seckill_vouchers.getKey() + voucherId;
    Map<String, Object> seckillVoucherMaps = redisTemplate.opsForHash().entries(redisKey);
    SeckillVouchers seckillVouchers = BeanUtil.mapToBean(seckillVoucherMaps, SeckillVouchers.class, true, null);

    // 判断是否有效
    AssertUtil.isTrue(seckillVouchers.getIsValid() == 0, "该活动已结束");
    // 判断是否开始、结束
    Date now = new Date();
    AssertUtil.isTrue(now.before(seckillVouchers.getStartTime()), "该抢购还未开始");
    AssertUtil.isTrue(now.after(seckillVouchers.getEndTime()), "该抢购已结束");

    // 判断是否卖完通过 Lua 脚本扣库存时判断
    //AssertUtil.isTrue(seckillVouchers.getAmount() < 1, "该券已经卖完了");

    // 获取登录用户信息
    String url = oauthServerName + "user/me?access_token={accessToken}";
    ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);
    if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
        resultInfo.setPath(path);
        return resultInfo;
    }
    // 这里的data是一个LinkedHashMap,SignInDinerInfo
    SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),
            new SignInDinerInfo(), false);
    // 判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
    VoucherOrders order = voucherOrdersMapper.findDinerOrder(dinerInfo.getId(),
            seckillVouchers.getFkVoucherId());
    AssertUtil.isTrue(order != null, "该用户已抢到该代金券,无需再抢");

    // ----------注释原始的走 关系型数据库 的流程----------
    // 扣库存
    // int count = seckillVouchersMapper.stockDecrease(seckillVouchers.getId());
    // AssertUtil.isTrue(count == 0, "该券已经卖完了");

    // 使用 Redis 锁一个账号只能购买一次
    String lockName = RedisKeyConstant.lock_key.getKey() + dinerInfo.getId() + ":" + voucherId;
    // 加锁
    long expireTime = seckillVouchers.getEndTime().getTime() - now.getTime();
   

    // Redisson 分布式锁
    RLock lock = redissonClient.getLock(lockName);

    try {
         {

        // Redisson 分布式锁处理
        boolean isLocked = lock.tryLock(expireTime, TimeUnit.MILLISECONDS);
        if (isLocked) {
            // 下单
            VoucherOrders voucherOrders = new VoucherOrders();
            voucherOrders.setFkDinerId(dinerInfo.getId());
            // Redis 中不需要维护外键信息
           
            voucherOrders.setFkVoucherId(seckillVouchers.getFkVoucherId());
            String orderNo = IdUtil.getSnowflake(1, 1).nextIdStr();
            voucherOrders.setOrderNo(orderNo);
            voucherOrders.setOrderType(1);
            voucherOrders.setStatus(0);
            long count = voucherOrdersMapper.save(voucherOrders);
            AssertUtil.isTrue(count == 0, "用户抢购失败");

            

            // ----------采用 Redis + Lua 解问题----------
            // 扣库存
            List<String> keys = new ArrayList<>();
            keys.add(redisKey);
            keys.add("amount");
            Long amount = (Long) redisTemplate.execute(redisScript, keys);
            AssertUtil.isTrue(amount == null || amount < 1, "该券已经卖完了");
        }
    } catch (Exception e) {
        // 手动回滚事物
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        
       

        // Redisson 解锁
        lock.unlock();
        if (e instanceof ParameterException) {
            return ResultInfoUtil.buildError(0, "该券已经卖完了", path);
        }
    }

    return ResultInfoUtil.buildSuccess(path, "抢购成功");
}

Natcret
1 声望0 粉丝