一、前置内容

1.1 单机秒杀实现

基于 Redis 的秒杀单机系统迭代实现(附详细流程图)-CSDN博客

1.2 前置流程图

1.4 前置问题

不同服务器的锁对象是不同的,仍然出现一人多单问题,所以要用到分布式锁,不同的服务器获得同一把锁。

二、分布式锁解决集群模式下一人多单问题

2.1 流程图

由于多个服务器只有一个redis,所以采用redis分布式锁可以实现集群模式下的秒杀

这里采用了redis分布式锁替代了原来的用户悲观锁,并且不管用户有没有抢到,都会在最后删除这个分布式锁,还设置了过期时间防止发生死锁。

2.2 代码实现

    @Autowired
    private ISeckillVoucherService seckillVoucherService;
    @Autowired
    private RedisIdWorker redisIdWorker;
    @Autowired
    private CacheClient cacheClient;

    @Override
    @Transactional
    public Long seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            throw new BaseException(ExceptionConstants.NOT_START);
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            throw new BaseException(ExceptionConstants.IS_END);
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            throw new BaseException(ExceptionConstants.NOT_ENOUGH);
        }
        Long userId = UserHolder.getUser().getId();
        String key = RedisConstants.getLockOrderKey(userId);
        boolean isLock = cacheClient.tryLock(key , RedisConstants.LOCK_ORDER_TTL);
        if (!isLock) {
            // 获取锁失败,返回错误或重试
            throw new BaseException(ExceptionConstants.EXIST_BUY);
        }
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } catch (IllegalStateException e) {
            throw new RuntimeException(e);
        } finally {
            cacheClient.unlock(key);
        }
    }

    @Transactional
    @Override
    public Long createVoucherOrder(Long voucherId) {
        // 5.一人一单逻辑
        // 5.1.用户id
        Long userId = UserHolder.getUser().getId();
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 用户已经购买过了
            throw new BaseException(ExceptionConstants.EXIST_BUY);
        }
        //5,扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0)
                .update();
        if (!success) {
            //扣减库存
            throw new BaseException(ExceptionConstants.NOT_ENOUGH);
        }
        //6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 6.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 6.2.用户id
        voucherOrder.setUserId(userId);
        // 6.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        return orderId;
    }
}
    public boolean tryLock(String key, Long time) {
        // 获取线程标示
        long threadId = Thread.currentThread().getId();
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, threadId+"", time, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    public void unlock(String  key) {
        stringRedisTemplate.delete(key);
    }

2.3 问题分析

由于我们给分布式锁加了过期时间,就可能导致时间过长业务还没有结束但是锁已经被释放了,当这个用户再发送了一个请求加上锁时,这个锁又被原来的线程释放了,出现了误删问题。

所以我们在释放锁的时候要注意是不是原来的锁,不能删除别人的锁。

三、解决误删问题

3.1 流程图

线程id是由jvm内部递增获得的,如果有多个服务器,线程的id就会重复,所以要采用更唯一的存储value

3.2 代码实现

    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    @Override
    public boolean tryLock(String key, Long time) {
        String value = ID_PREFIX + Thread.currentThread().getId();
        Boolean flag = stringRedisTemplate.opsForValue()
                .setIfAbsent(key , value , time, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    @Override
    public void unlock(String  key) {
        String value = ID_PREFIX + Thread.currentThread().getId();
        String currentValue = stringRedisTemplate.opsForValue().get(key);
        if (value.equals(currentValue)) {
            stringRedisTemplate.delete(key);
        }
    }

3.3 问题分析

因为查询和删除没有写在一条语句,没有保证原子性,所以还是有误删的可能

四、lua脚本解决redis原子性问题

4.1 lua脚本

Redis 本身是单线程执行命令,而Lua 脚本在 Redis 中执行时是原子性

Lua 脚本中操作 Redis,必须通过 Redis 提供的内置全局函数 redis.call() 或 redis.pcall(),二者功能一致,唯一区别:

  • redis.call():执行 Redis 命令出错时,直接抛出异常,脚本终止执行,Redis 向客户端返回错误信息;
  • redis.pcall():执行 Redis 命令出错时,捕获异常并返回错误信息,脚本继续执行(推荐生产使用,容错性更高)。

语法:redis.call('Redis命令', '参数1', '参数2', ...)

示例:redis.call('SET', 'name', 'zhangsan')redis.call('GET', 'name')

4.2 redis中调用lua脚本

EVAL lua_script numkeys [key1 key2 ...] [arg1 arg2 ...]

参数说明(必须严格按顺序传参,Redis 通过numkeys区分 keys 和 args):

  • lua_script:Lua 脚本的字符串内容;
  • numkeys:后续传入的key 的数量(整数,无 key 则传 0);
  • [key1 key2 ...]:需要在 Lua 脚本中操作的 Redis 键名(可选,通过KEYS表访问);
  • [arg1 arg2 ...]:向 Lua 脚本传递的普通参数(可选,通过ARGV表访问)。
  • Redis 为 Lua 脚本内置了两个全局只读表,用于接收外部传入的参数,必须大写

    • KEYS[i]:访问第 i 个传入的 key(i 从 1 开始,不是 0!Lua 表支持 1 开始索引)

    • ARGV[i]:访问第 i 个传入的普通参数(i 从 1 开始)。

4.3 编写lua脚本,确保锁判断和释放一起操作

-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

4.4 代码实现

下载Emmylua插件

    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    @Override
    public void unlock(String  key) {
        // 调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(key),
                ID_PREFIX + Thread.currentThread().getId());
    }

脚本在初始化的时候读取,这样就不会用一次读取一次了。

4.5 问题分析

  • 当前锁可能会出现不可重入问题和死锁问题,并且获取锁失败了就没有重试机会。
  • 如果锁是超时释放的,那么业务也会受到影响。
  • 当我们执行写操作时,访问主节点,执行读操作,访问从节点,主节点要把数据同步给从节点,如果还没有同步的时候,主节点出现了问题,导致数据未同步,选取的新的从节点作为主节点,数据发生了丢失(概率低)

五、后续内容

5.1 基于Redisson锁实现

基于 Redis 的秒杀集群系统迭代实现【Redisson源码分析】(附详细流程图)-CSDN博客

5.2 基于消息队列异步优化

基于 Redis 的秒杀集群系统迭代实现【异步优化】(附详细流程图)-CSDN博客

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐