欢迎光临
我们一直在努力

【AI全职下属】AI 下属翻车现场:AI Agent 写出 `@Transactional`,为什么仍然超卖?

Spring Boot 高并发秒杀实战:AI Agent 写出 @Transactional,为什么仍然超卖?

系列导读:本系列用一个可运行的 Spring Boot 秒杀 Demo,拆解“当 AI 成为我的全职下属”之后,研发工作流应该如何重建。五期内容从一次超卖事故开始,依次讨论并发正确性、上下文裁剪、防作弊 CI、Agent 友好架构和 Human-in-the-loop 上线门禁。主线不是证明 AI 能不能写代码,而是回答一个更实际的问题:当 Agent 已经能高吞吐地产出代码时,工程师如何用边界、验证和审批机制,把它变成可靠的执行者。

文章目录

  • Spring Boot 高并发秒杀实战:AI Agent 写出 `@Transactional`,为什么仍然超卖?
    • 1. 问题现场:事务写了,库存还是卖穿了
    • 2. 原因:先查再改留下了并发窗口
      • 动态演示:为什么 unsafe 会继续写入订单
    • 3. 在 Demo 中复现这个错误
      • 3.1 如何判断实验复现成功
    • 4. 原理:`@Transactional` 解决不了所有并发不变量
    • 5. 解决办法:把判断和扣减合并成一条 SQL
    • 6. 给 AI 下发任务时要写成验收条件
    • 实验环境
    • 小结
      • 适用边界
      • 测试 Demo 仓库
    • **下一篇**:小z疯狂码字ing

1. 问题现场:事务写了,库存还是卖穿了

你把“判断库存、扣减库存、创建订单”交给 AI Agent,它返回了一段看上去很规范的 @Transactional 代码。单测通过,接口能跑,日志也没有异常。

但一压测,500 个请求争抢 100 件库存,数据库里出现了 500 条成功订单。更糟的是库存字段没有变成负数,单看库存还以为系统正常。

这个错误很适合拿来做 AI Agent 工程治理的第一课:代码能编译、事务能提交,不等于业务约束在并发下成立。

本文用配套 Spring Boot Demo 复现这个错误,再解释原因、事务原理和修复办法。但第一期真正想强调的不是“秒杀该怎么写”,而是一个更基础的工作习惯:给 AI 下发任务时,不要只描述功能,要把任务写成可验证的验收条件。

如果 Issue 只写“实现一个高并发秒杀接口,注意并发安全”,Agent 很容易交出一段看起来正确的 @Transactional 代码。它完成了“像一个秒杀接口”的表面目标,却没有被要求证明“订单数不能超过库存”这个业务不变量。

所以在开始写代码前,先把目标约束说清楚:

  • 库存不能小于 0。
  • 成功订单数不能超过初始库存。
  • 库存扣减与订单创建必须在同一事务中完成。
  • 这三句话不是补充说明,而是验收条件。后面的代码、压测和修复,都围绕它们展开。

    2. 原因:先查再改留下了并发窗口

    下面的代码没有语法错误,也加了事务:

    @Transactional
    public SeckillResult execute(Long userId, Long productId) {
    SeckillProduct product = productMapper.selectById(productId);
    if (product == null || product.getStock() <= 0) {
    return SeckillResult.soldOut("unsafe");
    }

    product.setStock(product.getStock() 1);
    productMapper.updateById(product);
    SeckillOrder order = orderCreator.create(userId, productId, "unsafe");
    return SeckillResult.success(order.getId(), "unsafe");
    }

    单线程下,它的执行过程完全正确。问题出现在两个线程同时读到相同库存时:

    线程 A:读取 stock = 100
    线程 B:读取 stock = 100
    线程 A:写入 stock = 99
    线程 B:写入 stock = 99

    两笔订单已经创建,库存却只减少 1。这叫丢失更新。因此超卖不一定表现为负库存;它也可能表现为“订单数远大于售出库存,但库存值仍然看起来正常”。

    动态演示:为什么 unsafe 会继续写入订单

    下面这张动图把两种实现放在同一个并发场景下对比:

    在这里插入图片描述

    • 左侧 unsafe:多个请求并发读到同一个旧库存,后续请求即使已经超过真实库存,也仍然进入“写入订单”区。
    • 右侧 atomic:请求必须通过 UPDATE … WHERE stock > 0,库存归零后数据库返回 affectedRows=0,服务端不再创建订单。

    如果博客平台支持外链页面,也可以打开完整交互动画:

    在这里插入图片描述

    查看完整交互动画

    3. 在 Demo 中复现这个错误

    启动项目:

    cd demo\\AutoEnterprise-Seckill
    mvn.cmd spring-boot:run

    命令要求当前终端中的 java -version 已指向 JDK 17 或更高版本,不在项目中记录任何本机 JDK 安装路径。

    执行压测:

    python pipeline\\run_stress_test.py `
    base-url $env:SECKILL_BASE_URL `
    mode unsafe `
    concurrency 100 `
    requests 500 `
    stock 100

    本机一次实测结果:

    {
    "mode": "unsafe",
    "requests": 500,
    "concurrency": 100,
    "initial_stock": 100,
    "success_responses": 500,
    "database_orders": 500,
    "remaining_stock": 49,
    "oversold": true
    }

    500 个请求全部拿到“秒杀成功”,数据库出现 500 条订单,但初始库存只有 100。更危险的是剩余库存仍为正数,单看库存字段甚至不容易发现事故。

    吞吐量受机器、数据库连接池和后台进程影响。判断正确性的关键不是 QPS,而是 订单数 <= 初始库存 这一业务不变量。

    3.1 如何判断实验复现成功

    • unsafe 模式的 database_orders 大于 100,说明已复现超卖。
    • remaining_stock 不一定为负数;丢失更新时它可能仍为正数。
    • 如果本机并发较低未复现,可增加 –requests,但不要直接把结果当作生产容量数据。

    4. 原理:@Transactional 解决不了所有并发不变量

    事务没有失效。每个线程内部的“更新库存 + 插入订单”仍然一起提交或回滚。真正的问题是多个事务都基于旧库存做出了合法但互相冲突的决定。

    需要区分两个概念:

    • 事务原子性:一个事务中的操作要么全部成功,要么全部失败。
    • 并发业务正确性:多个事务同时执行后,系统仍满足库存不超卖等业务不变量。

    前者不能自动推导出后者。AI Agent 生成 @Transactional 只能说明它知道要包事务,不说明它已经把库存判断做成了并发安全的不变量。

    5. 解决办法:把判断和扣减合并成一条 SQL

    MyBatis-Plus Mapper 中增加条件更新:

    @Update("""
    UPDATE seckill_product
    SET stock = stock – 1, version = version + 1
    WHERE id = #{productId} AND stock > 0
    """
    )
    int deductStockIfAvailable(@Param("productId") Long productId);

    Service 只在影响行数为 1 时创建订单:

    @Transactional
    public SeckillResult execute(Long userId, Long productId) {
    if (productMapper.deductStockIfAvailable(productId) != 1) {
    return SeckillResult.soldOut("atomic");
    }
    SeckillOrder order = orderCreator.create(userId, productId, "atomic");
    return SeckillResult.success(order.getId(), "atomic");
    }

    这条 SQL 将“库存大于 0 的判断”和“库存减 1”交给数据库原子执行。多个线程竞争时,最多只有 100 次更新成功。

    同样参数再次压测:

    {
    "mode": "atomic",
    "requests": 500,
    "concurrency": 100,
    "initial_stock": 100,
    "success_responses": 100,
    "database_orders": 100,
    "remaining_stock": 0,
    "oversold": false
    }

    6. 给 AI 下发任务时要写成验收条件

    这次超卖事故表面上是并发问题,本质上也是任务表达问题。

    很多人给 Agent 下任务时会这样写:

    帮我实现一个秒杀接口,要求并发安全,不要超卖。

    这句话对人类开发者可能勉强够用,因为人会主动追问边界、补测试、确认数据口径。但 Agent 更像一个高吞吐执行者:你没有明确写进任务的约束,它未必会自动变成测试、SQL 条件或审查清单。

    更好的写法是把需求拆成“输入、动作、禁止事项、验收条件”:

    任务:实现商品秒杀接口。

    输入条件:
    1. 商品初始库存为 100。
    2. 使用 100 个并发线程发起 500 个秒杀请求。
    3. 每个请求使用不同 userId,同一个 productId。

    必须满足:
    1. 成功响应数量不能超过 100。
    2. 订单表新增记录数不能超过 100。
    3. 最终库存不能小于 0。
    4. 当订单表新增记录数为 100 时,最终库存必须为 0。
    5. 库存扣减成功后才能创建订单。

    禁止事项:
    1. 禁止通过降低并发度让测试变绿。
    2. 禁止跳过、删除或弱化测试断言。
    3. 禁止只依赖接口返回值判断正确性,必须检查数据库订单数和库存。

    验收方式:
    1. 提供并发压测脚本或测试用例。
    2. 输出 success_responses、database_orders、remaining_stock、oversold 四个指标。
    3. 当 oversold=false 且 database_orders<=initial_stock 时,才算通过。

    这个模板的关键,是把“不要超卖”翻译成机器可以判断的指标:

    业务语言验收指标
    不要超卖 database_orders <= initial_stock
    库存不能卖成负数 remaining_stock >= 0
    成功下单要有真实订单 success_responses == database_orders 或解释差异来源
    库存扣减和订单创建一致 扣减失败时不得创建订单
    并发安全 在指定并发度和请求数下重复验证不变量

    这一步会直接改变 Agent 的实现倾向。 如果任务只写“加事务”,它很可能围绕 @Transactional 做文章;如果任务写成“500 个请求抢 100 件库存,订单数不能超过 100”,它才会更容易走向条件更新、唯一约束、幂等键、锁或补偿机制这些真正服务于不变量的方案。

    因此第一期的核心经验可以写成一句话:

    不要把“实现什么功能”当作任务终点,要把“满足什么验收条件”写成任务本体。

    AI 擅长实现目标,但目标必须被写成测试可以判断的条件。否则它可能完成了代码,却没有完成业务。

    实验环境

    项目版本或参数
    JDK 17.0.15
    Spring Boot 3.5.15
    MyBatis-Plus 3.5.15
    数据库 H2 内存库,MySQL 兼容模式
    初始库存 100
    请求数 500
    并发线程 100

    小结

    这次事故给出了专栏的第一条原则:

    给 AI 下发任务时,先写验收条件,再看代码实现;不要审查它写得像不像人,要审查它是否守住了业务不变量。

    下一期将解决另一个常见问题:项目越来越大后,为什么把整个仓库塞进上下文,反而会让 Agent 更容易改错文件。

    适用边界

    条件更新适合单商品库存这一类简单不变量。涉及多仓、预占库存、超时释放、消息最终一致性时,还需要库存流水、幂等键和补偿机制;本文 Demo 不代表完整电商生产方案。

    测试 Demo 仓库

    配套 Demo 仓库地址:https://github.com/quan020406/xiaozhan-blog-column-demos

    完整交互动画地址:https://quan020406.github.io/xiaozhan-blog-column-demos/下一代工作流-当AI成为我的全职下属/demo/AutoEnterprise-Seckill/visualization/seckill-animation.html


    下一篇:小z疯狂码字ing

    感谢阅读,记得点赞、关注、收藏,欢迎各位评论区交流!!! 在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 【AI全职下属】AI 下属翻车现场:AI Agent 写出 `@Transactional`,为什么仍然超卖?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址