【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 ,即不暴露代理对象。这样我们就可以获取在实现类中获取代理对象了。

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

相关推荐
月光水岸New1 小时前
Ubuntu 中建的mysql数据库使用Navicat for MySQL连接不上
数据库·mysql·ubuntu
狄加山6751 小时前
数据库基础1
数据库
我爱松子鱼1 小时前
mysql之规则优化器RBO
数据库·mysql
chengooooooo2 小时前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
Rverdoser2 小时前
【SQL】多表查询案例
数据库·sql
Galeoto3 小时前
how to export a table in sqlite, and import into another
数据库·sqlite
希忘auto3 小时前
详解Redis在Centos上的安装
redis·centos
人间打气筒(Ada)3 小时前
MySQL主从架构
服务器·数据库·mysql
leegong231113 小时前
学习PostgreSQL专家认证
数据库·学习·postgresql
喝醉酒的小白3 小时前
PostgreSQL:更新字段慢
数据库·postgresql