分布式锁的实现(秒杀为背景)

本文探讨了在秒杀场景下如何实现和优化分布式锁,包括全局唯一ID生成策略、乐观锁的版本号法和CAS法,以及如何解决乐观锁的缺点。文章详细介绍了如何利用Redis实现分布式锁,解决了集群模式下的超卖问题,并讨论了Redisson的可重入锁原理,以应对线程安全问题。
摘要由CSDN通过智能技术生成

1. 全局唯一ID

在秒杀后生成的订单,订单ID的设计是值得考虑的。是采用数据库的自增?必然是不行的,首先若是一张订单表,其表的容量是有上限的,且订单的数据量巨大,若是采用多库多表进行存储,那么每个表自增ID都是从1开始,会造成订单ID的重复,且自增ID规律性强,容易被猜测,具有安全隐患。

1.1 ID生成策略

  1. 采用UUID
  2. 雪花算法
  3. 采用Redis的自增并且根据业务进行拼接

采用Redis的自增并且根据业务进行拼接:
在这里插入图片描述
第一位是符号位,永远为0,表示正数

2-31位是时间戳,可以定义一个起始时间,然后在获取下单时间,两者相减即可

后32位是序列号位,采用Redis的自增长

那么Redis自增长的key应该如何设计呢?

可以采用业务名:日期进行设计,如果不拼接日期的话,虽然Redis的自增长支持到2^64,但是我们设计的ID的序列号只有32位,所以如果不按照日期进行划分,业务量巨大的情况下,是会导致自增的数值超过序列号的位数。且以日期作为key的拼接,也方便做每天订单量的统计操作。

代码实现

/**
     * 开始时间戳
     */
    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(ZoneOffset.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("icr:" + keyPrefix + ":" + date);

        // 3.拼接并返回
        return timestamp << COUNT_BITS | count;
    }

2. 秒杀

声明,秒杀接口传入秒杀物品的ID,订单流程图如下
在这里插入图片描述

首先实现基本的下单功能

/**
     * 获取秒杀优惠券的service
     */
    @Autowired
    private ISeckillVoucherService iSeckillVoucherService;

    /**
     * 全局ID的生成类
     */
    @Autowired
    private RedisIdWorker redisIdWorker;

    /**
     * 下单的mapper
     */
    @Autowired
    private VoucherOrderMapper voucherOrderMapper;
    /**
     * 秒杀
     * @param voucherId
     * @return
     */
    @Transactional
    @Override
    public Result seckillVoucher(Long voucherId) {
   
        // 1. 查询优惠券
        SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
        // 2. 判断秒杀是否开始
        LocalDateTime beginTime = voucher.getBeginTime();
        if(beginTime.isAfter(LocalDateTime.now())){
   
            return Result.fail("秒杀尚未开始");
        }
        // 3. 判断秒杀是否结束
        LocalDateTime endTime = voucher.getEndTime();
        if (endTime.isBefore(LocalDateTime.now())){
   
            return Result.fail("秒杀已经结束");
        }
        // 4. 库存是否充足
        if (voucher.getStock()<1){
   
            return Result.fail("库存不足");
        }
        // 5.扣减库存
        boolean flag = iSeckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).update();
        if (!flag){
   
            return Result.fail("下单失败");
        }
        // 6. 创建订单
        VoucherOrder order = new VoucherOrder();
        // ID生成类
        long orderId = redisIdWorker.nextId("order");
        // 获取userId
        Long userId = UserHolder.getUser().getId();
        order.setId(orderId);
        order.setVoucherId(voucherId);
        order.setUserId(userId);
        voucherOrderMapper.insert(order);
        // 7. 返回订单ID
        return Result.ok(orderId);
    }

这种代码只是基本的业务代码,在高并发的情况下必然会出现超卖的情况

解决方式:使用锁

  1. 悲观锁
    认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。
    例如Synchronized、Lock都属于悲观锁

  2. 乐观锁
    认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。
    如果没有修改则认为是安全的,自己才更新数据。
    如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常

乐观锁,悲观锁不是锁,它是锁的一种思想

2.1 乐观锁

2.1.1 版本号法

数据库里保存version字段,在修改数据前先读取版本号,在修改数据时加上条件 version = 之前读取的version,如果条件不符合,则说明有线程修改过,则此次修改失败。每次修改成功后,version = version +1。

2.1.2 CAS法

CAS法和版本号法思想一致,只是版本号法使用新的字段version字段来辨别版本是否被修改过,CAS使用已有的字段,在上述场景中,可以使用库存代替版本号,在修改库存时,先读出库存,在修改时,stock = stock -1 where
stock = 读取的stock即可

代码实现CAS法

 /**
     * 获取秒杀优惠券的service
     */
    @Autowired
    private ISeckillVoucherService iSeckillVoucherService;

    /**
     * 全局ID的生成类
     */
    @Autowired
    private RedisIdWorker redisIdWorker;

    /**
     * 下单的mapper
     */
    @Autowired
    private VoucherOrderMapper voucherOrderMapper;
    /**
     * 秒杀
     * @param voucherId
     * @return
     */
    @Transactional
    @Override
    public Result seckillVoucher(Long voucherId) {
   
        // 1. 查询优惠券
        SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
        // 2. 判断秒杀是否开始
        LocalDateTime beginTime = voucher.getBeginTime();
        if(beginTime.isAfter(LocalDateTime.now())){
   
            return Result.fail("秒杀尚未开始");
        }
        // 3. 判断秒杀是否结束
        LocalDateTime endTime = voucher.getEndTime();
        if (endTime
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值