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

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

相关推荐
赵师的工作日24 分钟前
MongoDB-单键索引与复合索引
数据库·mongodb
吴代庄38 分钟前
探秘Redis哨兵模式:原理、运行与风险全解析
java·redis·系统架构
初晴~1 小时前
【Redis】高并发场景下秒杀业务的实现思路(单机模式)
java·数据库·redis·后端·spring·缓存·中间件
KevinAha2 小时前
MySQL迁移SQLite
数据库·mysql·sqlite
简 洁 冬冬2 小时前
限制redis内存
数据库·redis·缓存
9ilk2 小时前
【MySQL】--- 数据库基础
数据库·mysql
信徒_2 小时前
Redis 和 Mysql 中的数据一致性问题
数据库·redis·mysql
weisian1512 小时前
Redis篇-19--运维篇1-主从复制(主从复制,读写分离,配置实现,实战案例)
java·运维·redis
龙少95433 小时前
【深入理解Java线程池】
java·数据库·算法