Redis实战案例及问题分析之单机优惠券秒杀

本文介绍了在分布式环境中如何实现全局唯一ID的生成,通过RedisIdWorker保证ID的唯一性和安全性。接着,详细阐述了优惠券秒杀功能的实现,包括检查秒杀状态、库存校验以及使用乐观锁防止超卖问题。在一人一单的场景下,通过悲观锁确保同一用户只能下单一次,避免并发导致的线程不安全问题。
摘要由CSDN通过智能技术生成

目录

全局唯一ID

全局ID生成器

 实现优惠券秒杀的下单功能

优惠券秒杀超卖问题

 乐观锁解决秒杀券超卖问题

一人一单


全局唯一ID

当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在如下问题:

  • id的规律太明显:容易被用户推测出某些信息
  • 受单表数据量的限制:当优惠券订单数量过多,就要彩标存储,这时不同的数据表中会出现

全局ID生成器

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,已满要满足以下特性:

  • 唯一性
  • 高可用(任何时候来都可用)
  • 高性能
  • 递增性(整体单调增)
  • 安全性

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

@Component
public class RedisIdWorker {
    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 开始时间戳
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /**
     * 序列号位数
     */
    private static final int COUNT_BITS = 32;

    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;
    }
}

 实现优惠券秒杀的下单功能

下单需要判断两点:

  • 秒杀是否开始或结束。如果尚未开始或已经结束则无法下单
  • 库存是否充足 ,不足则无法下单

 

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;
    @Override
    @Transactional
    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("优惠券已经被抢完");
        }
        //5.扣减库存
        boolean success = seckillVoucherService.update().setSql("stock = stock-1").eq("voucher_id", voucherId).update();
        if (!success) {
            //扣减失败
            return Result.fail("优惠券库存不足");
        }
        //6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //6.1订单ID
        long orderId = redisIdWorker.nextID("order:");
        voucherOrder.setId(orderId);
        //6.2用户id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        //6.3代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //7.返回订单
        return Result.ok(orderId);
    }
}

优惠券秒杀超卖问题

为什么会出现超卖问题?高并发的情况下,会出现访问量过大,同时拿到了库存大于1的数据,多个线程交叉执行,就会出现超卖问题。

超卖问题是典型的多线程安全问题,针对这一个问题的常见解决方案是加锁:

 悲观锁性能较低,这里使用乐观锁。

乐观锁的关键是判断之前查询得到的数据是否有被修改过,常见的方式有两种:

版本号发:

 CAS法(先比较后设置),用库存代替版本号

 乐观锁解决秒杀券超卖问题

乐观锁解决超卖问题存在的弊端:成功率低。因为只要判断库存不相等就执行失败,当高并发执行的时候会有大多的线程执行失败。

解决办法:不再是判断库存是否相等,而是去判断库存是否大于1。

//5.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock-1")//set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock",0) //where id = ? and stock >0
                .update();

一人一单

需求:要求同一个优惠券,一个用户只能下一单。

 为了解决并发带来的线程不安全问题,这里采用的是悲观锁,也就是加了synchronized关键字。

 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();
        synchronized(userId.toString().intern()){
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.creatVoucherOrder(voucherId);
        }
    }

    @Transactional
    public  Result creatVoucherOrder(Long voucherId) {
        //5.一人一单
        //5.1查询用户
        Long userId = UserHolder.getUser().getId();
        //5.2查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        //5.3判断用户是否存在
        if (count > 0) {
            //用户已经购买过了
            return Result.fail("用户已经购买过了");
        }
        //6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock-1")//set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock",0) //where id = ? and stock >0
                .update();
        if (!success) {
            //扣减失败
            return Result.fail("优惠券库存不足");
        }
        //7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //7.1订单ID
        long orderId = redisIdWorker.nextID("order:");
        voucherOrder.setId(orderId);

        voucherOrder.setUserId(userId);
        //7.3代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //8.返回订单
        return Result.ok(orderId);
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值