Skip to content
DAILY QUOTE

“ ”

由于本项目是专门学习Redis的,所以在这里将会使用Redis的setnx指令实现分布式锁解决超卖问题。

  • 获取分布式锁
bash
# 添加锁,利用setnx互斥的特性
SETNX key value
# 为锁设置过期时间,超时释放,避免死锁
EXPIRE key time

# 防止在执行完setnx后Redis服务宕机,还没来得及执行expire设置过期时间的情况,保证了分布式锁的安全性。
SET key value EX seconds NX
  • 释放分布式锁
bash
# 手动释放(还可设置过期时间,超时剔除释放锁)
DEL key

获取锁失败后,重试获取锁有两种机制,阻塞式获取和非阻塞式获取:

  • 阻塞锁:没有获取到锁,则继续等待获取锁。浪费CPU,线程等待时间较长,实现较麻烦。
  • 非阻塞锁:尝试一次,没有获取到锁后,不继续等待,直接返回锁失败。(本次采用非阻塞机制)

  • 创建分布式锁
java
/**
 * 锁接口
 */
public interface ILock {
    /**
     * 尝试获取锁
     * @param timeoutSec锁持有的超时时间,过期后自动释放
     * @return true代表获取锁成功; false代表获取锁失败
     */
    boolean tryLock(long timeoutSec);

    /**
     * 释放锁
     */
    void unlock();
}

public class SimpleRedisLock implements ILock{

        private String name;
        private StringRedisTemplate stringRedisTemplate;

        /**
         * 执行SimpleRedisLock相关业务逻辑。
         *
         * @param name名称
         * @param stringRedisTemplate stringRedisTemplate参数
         * @return处理结果
         */
        public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
            this.stringRedisTemplate = stringRedisTemplate;
            this.name = name;
        }

        private static final String KEY_PREFIX = "lock:";

        /**
         * 尝试获取Redis互斥锁。
         *
         * 作用:
         * 1.获取线程标识;
         * 2.获取锁;
         *
         * @param timeoutSec timeoutSec参数
         * @return处理结果
         */
        public boolean tryLock(long timeoutSec) {
            //获取线程标识
            long threadId=Thread.currentThread().getId();
            //获取锁
            Boolean success=stringRedisTemplate.opsForValue()
                    .setIfAbsent(KEY_PREFIX+name,threadId+"",timeoutSec, TimeUnit.SECONDS);
            return Boolean.TRUE.equals(success);
        }

        /**
         * 释放Redis互斥锁。
         *
         * @return无返回值
         */
        public void unlock() {
            stringRedisTemplate.delete(KEY_PREFIX+name);
        }
}
  • 改造VoucherOrderServiceImpl
java
/**
 * 处理优惠券秒杀下单请求。
 *
 * 作用:
 * 1.查询优惠卷;
 * 2.判断秒杀是否开始;
 * 3.尚未开始;
 * 4.判断秒杀是否已经结束;
 * 5.已经结束;
 *
 * @param voucherId优惠券id
 * @return处理结果
 */
@Override
public Result seckillVoucher(Long voucherId) {
    //1.查询优惠卷
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    //2.判断秒杀是否开始
    if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
        //尚未开始
        return Result.fail("秒杀尚未开始");
    }
    //3.判断秒杀是否已经结束
    if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
        //已经结束
        return Result.fail("秒杀已结束");
    }
    //4.判断库存是否充足
    if (voucher.getStock() < 1) {
        //库存不足
        return Result.fail("库存不足");
    }
    Long userId=UserHolder.getUser().getId();
    //创建锁对象
    SimpleRedisLock lock = new SimpleRedisLock("order" + userId, stringRedisTemplate);
    //获取锁
    boolean isLock = lock.tryLock(1200);
    //判断是否获取锁成功
    if (!isLock) {
        //获取锁失败,返回错误或重试
        return Result.fail("不允许重复下单");
    }
    try {
        //获取代理对象
        IVoucherOrderService proxy= (IVoucherOrderService) AopContext.currentProxy();
        return proxy.createVoucherOrder(voucherId);
    } catch (IllegalStateException e) {
        throw new RuntimeException(e);
    } finally {
        lock.unlock();
    }
}

因为锁的是用户id,即同一个用户不能同时下单多次,所以相同用户再次下单获取锁会失败,并且不会再次重试,直接返回错误信息。

  • 测试效果 同一个用户只能成功获取一次锁,实现了Redis分布式锁的互斥效果,解决了一人一单集群超卖问题。