优惠券秒杀业务分析

文章目录


业务分析

秒杀业务的核心难点在于应对高并发带来的数据一致性问题,主要聚焦两个矛盾:

  • 限定数量,防止超售:库存扣减需要保证原子性,避免并发下卖出超过实际库存。
  • 一人一单:同一个用户 ID 只能抢购一次,保证公平。

问题1:高并发下出现超售

多个线程同时读取库存为 1,都判断"充足",然后各自扣减,结果库存变成负数。

  • 解决方案:CAS 乐观锁,在更新库存时,不依赖锁,而是增加一个版本条件,仅当库存大于 0 时才扣减。
java 复制代码
boolean success = seckillVoucherService.update()
       .setSql("stock = stock - 1")
       .eq("voucher_id", voucherId)
       .gt("stock", 0)   // CAS 条件:库存 > 0
       .update();

问题2:一个用户抢购多张

(1)使用同步锁 synchronized

给"查询是否已购买"和"创建订单"整个逻辑加锁。

java 复制代码
synchronized (userId.toString().intern()) {
    return createVoucherOrder(voucherId);
}

所有用户争夺同一把锁,锁粒度太粗。→ 改为 只对当前用户 ID 加锁,即 synchronized (userId.toString().intern())。

intern() 保证相同字符串对象唯一,避免不同线程使用不同 String 对象导致锁失效。

(2)事务失效

上述代码看似可行,但@Transactional 会失效,原因是 Spring AOP 的代理机制。createVoucherOrder 方法标记了 @Transactional,期望在事务中执行。然而我们通过 this.createVoucherOrder 直接调用,跳过了 Spring 生成的代理对象。

Spring 的事务管理基于 AOP 动态代理(JDK/CGLIB)。容器注入的是代理对象,代理对象会在调用目标方法前后处理事务(开启、提交/回滚)。如果我们拿到的是原始对象(this)并直接调用,代理根本没机会插手,事务注解自然失效。

解决方案:暴露代理对象

  • 在主方法中通过 AopContext.currentProxy() 获取当前类的代理对象。
  • 通过代理对象调用 createVoucherOrder。

必须先在启动类或配置上启用 @EnableAspectJAutoProxy(exposeProxy = true),允许暴露代理。

java 复制代码
// 主方法
public Result seckillVoucher(Long voucherId) {
    // ...
    synchronized (userId.toString().intern()) {
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        return proxy.createVoucherOrder(voucherId);
    }
}

最终代码结构

java 复制代码
@Override
public Result seckillVoucher(Long voucherId) {
    //查询优惠券
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    //判断秒杀是否开始
    if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
        //秒杀尚未开始
        return Result.fail("秒杀尚未开始");
    }
    if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
        //秒杀已经结束
        return Result.fail("秒杀已经结束");
    }
    //判断库存是否充足
    if (voucher.getStock() < 1) {
        //库存不足
        return Result.fail("库存不足");
    }
    Long userId = UserHolder.getUser().getId();
    //仅限单体应用使用
    synchronized (userId.toString().intern()) {
        //实现获取代理对象 比较复杂
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        return createVoucherOrder(voucherId);
    }
java 复制代码
@Transactional
public Result createVoucherOrder(Long voucherId) {
    // 5. 一人一单
    Long userId = UserHolder.getUser().getId();
    // 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);
}
相关推荐
消失的旧时光-19431 小时前
统一并发模型:线程、Reactor、协程本质是一件事(从线程到协程 · 第6篇·终章)
java·python·算法
勿忘初心12211 小时前
Java 国密 SM4 加密工具类实战(Hutool + BouncyCastle)|企业级数据加密 + 兼容 JDK8
java·数据安全·数据加密·后端开发·企业级开发·国密 sm4
庞轩px1 小时前
第8篇:原子类与CAS底层原理——无锁并发的实现
java·cas·乐观锁·aba·无锁编程·自旋
rleS IONS2 小时前
SpringBoot中自定义Starter
java·spring boot·后端
DevilSeagull2 小时前
MySQL(2) 客户端工具和建库
开发语言·数据库·后端·mysql·服务
苍煜2 小时前
慢SQL优化实战教学
java·数据库·sql
MATLAB代码顾问2 小时前
改进遗传算法(IGA)求解作业车间调度问题(JSSP)——附MATLAB代码
开发语言·matlab
AI进化营-智能译站2 小时前
ROS2 C++开发系列16-智能指针管理传感器句柄|告别ROS2节点内存泄漏与野指针
java·c++·算法·ai
syker3 小时前
AIFerric深度学习框架:自研全栈AI基础设施的技术全景
开发语言·c++