很多新手第一次把 AI 用到 Spring 项目里时会遇到一个很常见的需求用户下单后先保存订单再异步发送通知、写操作日志、刷新统计数据。主流程不要被这些非核心操作拖慢。于是AI 很容易给出类似写法ServicepublicclassOrderService{TransactionalpublicvoidcreateOrder(CreateOrderCommandcommand){OrderorderorderRepository.save(Order.create(command));notificationService.sendOrderCreated(order.getId());orderRepository.markCreated(order.getId());}}通知服务写成ServicepublicclassNotificationService{AsyncpublicvoidsendOrderCreated(LongorderId){OrderorderorderRepository.findById(orderId);messageSender.send(订单已创建order.getOrderNo());}}从代码表面看这个方案好像很合理创建订单放在事务里发通知放在异步线程里主线程不用等待Async看起来已经处理了异步问题。但线上跑一段时间后你可能会遇到非常奇怪的现象异步通知偶尔查不到刚创建的订单订单事务最终回滚了但通知已经发出异步任务执行失败主流程完全不知道某些订单被正常创建但统计数据没有更新本地测试正常测试环境偶尔出错同一批数据在高并发下出现“已通知但订单不存在”的短暂状态。这些问题的根源通常不是Async写错了。而是开发者误以为异步线程会自动继承调用它的事务。实际上它通常不会。一、先理解一件事事务和线程不是同一个东西在常见的 Spring 使用方式里事务上下文通常与当前执行线程绑定。简化理解可以写成主线程 ↓ 开启事务 ↓ 执行数据库操作 ↓ 提交或回滚事务而Async的逻辑会进入线程池中的另一个线程主线程 开启事务 ↓ 保存订单 ↓ 提交事务 异步线程 获取线程池线程 ↓ 执行通知逻辑 ↓ 查询订单 / 写日志 / 调用外部服务它们不是同一个线程。也就意味着主线程里的事务不会自动搬到异步线程异步线程看到的数据取决于主事务是否已经提交异步线程抛出的异常不会自动让主事务回滚异步线程中的数据库操作也不会自动纳入主事务。如果把时序展开问题会更直观。T1主线程开始事务 T2主线程保存订单记录 T3主线程提交异步任务 T4异步线程开始执行 T5异步线程查询订单 T6主线程事务提交如果 T5 发生在 T6 之前异步线程就可能查询不到这条订单。因为从数据库可见性角度看主事务还没有提交。二、最常见的误区只要加了Async逻辑就会自动可靠很多 AI 生成的代码会默认做下面这件事TransactionalpublicvoidcreateOrder(CreateOrderCommandcommand){OrderorderorderRepository.save(Order.create(command));asyncService.sendOrderCreated(order.getId());}而异步方法则直接读取数据库AsyncpublicvoidsendOrderCreated(LongorderId){OrderorderorderRepository.findById(orderId);messageSender.send(order.getOrderNo());}问题在于这里存在三个独立风险。风险为什么会发生表现形式事务未提交异步线程先于主线程执行查不到订单或读到旧数据主事务回滚异步任务已经启动通知已发但订单不存在异步执行失败异常不会自动回传主线程主流程成功后续动作丢失很多开发者会想那我就在异步方法里加Transactional。例如AsyncTransactionalpublicvoidsendOrderCreated(LongorderId){OrderorderorderRepository.findById(orderId);notificationRepository.save(NotificationRecord.of(orderId));}这样做会开启一个新的事务但它并不能“继承主事务”。它只代表异步线程里的数据库操作有自己独立的事务它和订单创建事务不是同一个原子单元两边谁先提交、谁后失败依旧需要被单独设计。三、另一个常见坑同类内部调用时Async可能根本没生效还有一种更隐蔽的情况。开发者把方法写在同一个类里ServicepublicclassOrderService{TransactionalpublicvoidcreateOrder(CreateOrderCommandcommand){OrderorderorderRepository.save(Order.create(command));sendOrderCreatedAsync(order.getId());}AsyncpublicvoidsendOrderCreatedAsync(LongorderId){messageSender.send(orderorderId);}}你可能以为sendOrderCreatedAsync()会进入异步线程。但在很多基于代理的实现中同类内部直接调用不会经过 Spring 代理。结果就是createOrder() ↓ 直接调用 sendOrderCreatedAsync() ↓ 仍然运行在主线程这时不但没有异步甚至可能出现主线程被发送通知阻塞事务持续时间变长外部调用耗时把数据库连接占住通知失败导致主事务是否回滚变得模糊代码和实际运行行为完全不一致。所以看到Async不代表异步一定生效。先要确认这个方法是否经过 Spring 代理调用 它是否在另一个 Bean 中 是否真的进入了线程池 日志里的线程名是否发生变化四、正确思路把“事务完成”与“异步动作”显式连接起来对于“订单创建成功后再发送通知”这类场景比较清晰的方式是主事务只负责完成核心数据写入事务成功提交后再触发后续动作后续动作失败时有独立的记录、重试和监控外部通知不能默认和数据库事务天然一致。例如先定义事件publicrecordOrderCreatedEvent(LongorderId){}主事务内只保存订单并发布事件ServicepublicclassOrderService{privatefinalApplicationEventPublishereventPublisher;TransactionalpublicvoidcreateOrder(CreateOrderCommandcommand){OrderorderorderRepository.save(Order.create(command));eventPublisher.publishEvent(newOrderCreatedEvent(order.getId()));}}然后监听事务提交后的事件ComponentpublicclassOrderCreatedListener{TransactionalEventListener(phaseTransactionPhase.AFTER_COMMIT)publicvoidonOrderCreated(OrderCreatedEventevent){notificationService.sendAsync(event.orderId());}}异步执行放到另一个 Bean 中ServicepublicclassNotificationService{AsyncpublicvoidsendAsync(LongorderId){OrderorderorderRepository.findById(orderId);if(ordernull){thrownewIllegalStateException(order not found: orderId);}messageSender.send(订单已创建order.getOrderNo());}}这样至少保证了一点只有订单事务已经提交成功后异步通知才会被触发。但这里仍然有工程边界。如果通知发送失败订单并不会自动回滚。因此关键不是强行让所有动作塞进一个事务而是明确哪些动作属于核心一致性哪些动作属于可重试的后续处理。五、不要把外部调用塞进数据库事务里很多新手为了“保证一致性”会把外部通知放进事务中TransactionalpublicvoidcreateOrder(CreateOrderCommandcommand){OrderorderorderRepository.save(Order.create(command));messageSender.send(订单已创建order.getOrderNo());}这看似解决了“异步任务太早执行”的问题。但会引入另一组风险外部服务超时事务一直不提交数据库连接长期占用通知服务抖动会拖慢订单主流程外部消息已经发送成功但后续数据库事务失败事务重试时外部通知可能被重复发送。这说明外部调用和数据库事务不是简单地“放在一起就一致”。更稳妥的方向通常是把后续动作变成可追踪事件。例如使用 Outbox 模式订单表写入 事件表写入 ↓ 同一个数据库事务提交 ↓ 独立任务读取事件表 ↓ 发送消息或通知 ↓ 记录发送结果 ↓ 失败后重试或人工处理简化后的事件表可以这样定义CREATETABLEoutbox_event(idBIGINTPRIMARYKEYAUTO_INCREMENT,event_typeVARCHAR(64)NOTNULL,aggregate_idBIGINTNOTNULL,payloadTEXTNOTNULL,statusVARCHAR(32)NOTNULL,retry_countINTNOTNULLDEFAULT0,created_atDATETIMENOTNULL,sent_atDATETIMENULL);这不是说每一个异步动作都必须引入完整 Outbox。而是要先判断通知丢失是否可以接受通知重复是否可以接受失败后是否可以人工补发是否需要记录完整发送历史是否属于资金、库存、权益等关键链路。六、让 AI 先区分线程边界、事务边界和业务边界如果只问 AI订单创建后怎么异步发通知它很可能给你一段Async代码。这段代码可能能运行但未必覆盖你真正需要的边界。更有效的问法是你是 Spring 事务与异步任务评审助手。 场景 订单创建成功后需要异步发送通知并写入操作日志。 订单创建必须保证数据库事务一致 通知允许延迟但不能无记录丢失 通知失败后需要可重试 通知不能在订单事务回滚时提前发送。 请不要直接只给 Async 代码。 请完成 1. 区分主事务、异步线程、外部通知之间的边界 2. 说明 Async 是否会继承调用方事务 3. 设计事务提交后触发后续动作的方式 4. 判断是否需要事件表或 Outbox 5. 列出异常、重试、重复发送和人工补发的处理方式 6. 给出至少 6 个测试场景 7. 标出需要由业务方确认的风险。这类 Prompt 的价值不是让 AI 生成更多注解。而是让它先把问题拆成线程问题 事务问题 消息可靠性问题 业务一致性问题对刚开始使用 ChatGPT Plus 做代码解释、事务排查和测试设计的开发者来说工具接入准备不只是会不会复制一段异步代码还包括能否明确线程边界、保留异常记录、验证失败路径和回看执行结果。第一次把 AI 工具纳入开发工作流时建议把使用说明、异常处理和信息留存方式一起整理相关准备项可按实际需要参考gpt328com七、至少要补齐这些测试场景事务和异步问题最怕只验证“通知是否发送成功”。更应该覆盖这些场景测试场景预期结果订单事务成功提交异步通知在提交后触发订单事务回滚不触发通知异步线程抢先执行不会在提交前读取订单通知发送失败失败可记录、可重试同一订单重复触发不重复发送或有幂等控制同类内部调用Async能识别异步是否未生效线程池拒绝任务有明确异常与补偿路径外部通知超时不阻塞核心订单事务消息重试成功状态和审计记录一致例如可以验证事务回滚后不会触发监听TestvoidshouldNotSendNotificationWhenOrderTransactionRollsBack(){CreateOrderCommandcommandinvalidOrderCommand();assertThrows(BusinessException.class,()-orderService.createOrder(command));verify(notificationService,never()).sendAsync(anyLong());}再验证事务提交后才触发TestvoidshouldSendNotificationAfterOrderTransactionCommitted(){CreateOrderCommandcommandvalidOrderCommand();orderService.createOrder(command);await().atMost(Duration.ofSeconds(3)).untilAsserted(()-verify(notificationService).sendAsync(anyLong()));}测试里不能只验证方法调用次数。还要确认订单是否已真实提交通知记录是否可追踪失败后有没有进入补偿链路重试是否造成重复副作用线程池满载时系统如何表现。八、上线后必须让异步状态可观察异步任务最危险的状态是主流程成功了但后续动作悄悄失败了。因此至少应记录async_task_submitted_total async_task_rejected_total async_task_success_total async_task_failed_total async_task_retry_total async_task_pending_count outbox_event_pending_count outbox_event_oldest_age_seconds需要重点关注提交了多少异步任务有多少任务被线程池拒绝有多少任务失败后没有重试待处理事件积压了多久是否存在订单已创建但通知长期未发送重试数量是否突然上升。不要只看服务是否正常启动。异步链路的问题往往发生在高峰期、线程池繁忙、外部依赖抖动或重启恢复之后。九、结语Transactional和Async都是很有用的工具。但它们解决的问题并不一样Transactional负责当前线程中的数据库一致性Async负责把任务交给另一个线程执行它们不会自动拼成一个跨线程、跨服务、绝对一致的执行单元。AI 可以快速帮你生成异步代码、补齐监听器、写测试案例。但真正要由开发者确认的是哪些动作必须与主事务一起成功哪些动作可以延迟、重试或人工补偿异步失败后谁来发现重复发送是否可接受运行时线程池满了会发生什么是否需要事件表、Outbox 或更明确的状态记录。异步不是“丢到后台就结束”。真正可靠的异步是即使任务晚到、失败、重试或重启后恢复系统也知道它应该怎么继续。