【Redis】一人一单秒杀活动

秒杀一人一单业务流程图如下

实现代码块如下

复制代码
@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.一人一单逻辑
    // 5.1.用户id
    Long userId = UserHolder.getUser().getId();
    // 这里不需要查具体数据了,只需要查count值
    int count = query().eq("user_id", userId)
                       .eq("voucher_id", voucherId)
                       .count();
    // 5.2.判断是否存在
    if (count > 0) {
        // 用户已经购买过了
        return Result.fail("用户已经购买过一次!");
    }

    //6,扣减库存
    boolean success = seckillVoucherService.update()
            .setSql("stock= stock -1")
            .eq("voucher_id", voucherId).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);

    return Result.ok(orderId);
}

如果是单线程那上面的逻辑是没有问题的,问题是在多并发的情况就会出现一人购买多次的情况。为什么呢?这里不是已经加了判断吗?当有两个线程同时去订单表查询数据时,都发现没有数据,所以这两个线程都会扣减库存,创建订单,所以还是会出现一人购买多单的情况。所以需要加锁,由于乐观锁适合更新数据控制版本号,而插入数据就没有办法控制版本号了,所以需要使用悲观锁操作。

优化一

我们可以把实现一人一单的逻辑抽取出来封装成一个方法,然后对这个方法进行加锁。另外,事务的范围是对数据的监控,即扣减库存和创建订单时需要监控,而查询信息是不需要事务的,所以需要将seckillVoucher 方法的事务取消,在抽取的方法上添加事务。

优化二

如果在方法中加锁,那么整个方法就是串行执行。如果多个用户购买商品,需要等待上一个用户购买完才能下单,这效率也太低了。因此,这里加锁不应该是整个service服务,而需要为用户加锁。

复制代码
@Transactional
public  Result createVoucherOrder(Long voucherId) {
	Long userId = UserHolder.getUser().getId();
	synchronized(userId.toString().intern()){
         // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        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);
        // 7.2.用户id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        // 7.返回订单id
        return Result.ok(orderId);
    }
}

我们期望用户id相同的使用同一把锁,但即便是同个用户,每个请求传递过来都是一个全新的对象,那么锁也会改变,没有达到预期效果。此时就需要使用 intern() 函数,intern **它可以去常量池中找与你匹配的值,将常量池中的值返回给你。因此,无论new了多少个对象,只要值一样,返回的结果也是一样的。**这就可以确保用户id一样时,加的锁也是一样的。

优化三

如果你在方法内部加锁,会导致当前方法未提交,锁已经释放了。如果恰好有个线程在锁释放掉后,方法未提交这个空窗期内查询订单,而订单还没写入数据库,也有可能会导致并发问题。只需要将锁加在方法调用处,这样锁会在方法执行完毕后才释放锁。

优化四

事务要生效,spring要对当前的类做动态代理,拿到代理对象,而 this.createVoucherOrder指的是当前类,并不是代理对象 ,所以事务会失效。所以我们要获取原始的事务对象来操作事务,借助AopContext的 currentProxy() 方法拿到当前的代理对象。

复制代码
Object proxy = AopContext.currentProxy();

使用代理对象来调用 createVoucherOrder方法,而不是使用this,这样的话就会被spring进行管理了,因为这个代理对象是由spring创建的,它是带有事务的函数,这个不存在的原因是因为 VoucherOrderService 接口中是不存在这个函数的,因此我们也将这个函数在 VoucherOrderService 中创建一下。

我们还需要添加 aspectj 依赖,它是一种代理的模式

复制代码
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

接着在SpringBoot 启动类添加 @EnableAspectJAutoProxy(exposeProxy = true) 注解,暴露代理对象。默认是false ,即不暴露代理对象。这样我们就可以获取在实现类中获取代理对象了。

至此也完成了一人一单的业务。

相关推荐
阿里云大数据AI技术38 分钟前
云栖实录|MaxCompute全新升级:AI时代的原生数据仓库
大数据·数据库·云原生
不剪发的Tony老师1 小时前
Valentina Studio:一款跨平台的数据库管理工具
数据库·sql
weixin_307779131 小时前
在 Microsoft Azure 上部署 ClickHouse 数据仓库:托管服务与自行部署的全面指南
开发语言·数据库·数据仓库·云计算·azure
六元七角八分2 小时前
pom.xml
xml·数据库
Achou.Wang2 小时前
源码分析 golang bigcache 高性能无 GC 开销的缓存设计实现
开发语言·缓存·golang
虚行2 小时前
Mysql 数据同步中间件 对比
数据库·mysql·中间件
奥尔特星云大使2 小时前
mysql读写分离中间件Atlas安装部署及使用
数据库·mysql·中间件·读写分离·atlas
牛马baby2 小时前
【mysql】in 用到索引了吗?
数据库·mysql·in
杀气丶2 小时前
L2JBR - 修复数据库编码为UTF8
数据库·sql·oracle
-Xie-2 小时前
Mysql杂志(三十)——索引失效情况
数据库·mysql