2. API中的事务模型:请求-响应模型中的事务生命周期与边界划分
在微服务与单体架构的API开发阶段,事务管理是保证数据一致性的核心挑战。请求-响应模型作为最普遍的交互模式,其事务生命周期与各层的边界划分直接决定了错误处理的粒度与可靠性。本节将深入剖析事务在Controller、Service、Repository三层的职责边界,并给出清晰的划分原则。
2.1 请求-响应模型中的事务生命周期
一个典型的API请求从进入Controller到返回响应,事务的生命周期可划分为四个关键阶段。每个阶段都有不同的错误风险与处理策略。
| 阶段 | 起始点 | 结束点 | 核心风险 |
|---|---|---|---|
| 开启 | 请求到达Controller边界 | Service方法入口 | 连接池耗尽、超时配置缺失 |
| 执行 | Service开始业务逻辑 | Repository完成数据持久化 | SQL异常、唯一键冲突、死锁 |
| 提交/回滚 | Service方法正常返回或抛出异常 | 事务管理器提交或回滚完成 | 提交失败、部分回滚、连接未释放 |
| 清理 | 事务资源释放 | 响应返回给客户端 | 连接泄漏、资源未关闭 |
实践要点:事务生命周期应始终与请求线程绑定。在异步或事件驱动场景中,事务上下文不能跨线程传递,否则会导致连接混乱与数据不一致。
2.2 Controller/Service/Repository 层的事务边界划分
事务边界划分的核心原则是:事务的开启与提交必须在Service层完成。Controller负责请求解析与响应组装,Repository负责数据访问,两者都不应直接管理事务。下面通过对比表格明确各层职责。
| 层次 | 事务职责 | 允许操作 | 禁止操作 |
|---|---|---|---|
| Controller | 无事务管理权 | 参数校验、调用Service、异常转HTTP状态码 | 开启/提交/回滚事务、直接操作Repository |
| Service | 事务边界定义者 | 声明式事务(@Transactional)、编程式事务控制、跨Repository协调 | 将事务传播到Controller层、在非事务方法中调用Repository |
| Repository | 事务参与者 | 执行SQL、参与当前事务、抛出数据访问异常 | 独立提交/回滚、改变事务传播行为 |
常见反模式:在Controller层使用
@Transactional。这会导致事务范围过大,连接持有时间过长,且异常处理逻辑与HTTP响应耦合,难以进行细粒度的错误恢复。
2.3 事务传播行为与边界嵌套
当Service方法之间存在调用关系时,事务传播行为决定了边界如何嵌套。以下是开发阶段最常用的三种传播行为及其适用场景。
- REQUIRED(默认):加入当前事务,不存在则新建。适用于绝大多数业务方法,保证整个调用链在同一个事务中。
- REQUIRES_NEW:挂起当前事务,新建独立事务。适用于日志记录、审计、异步通知等需要独立提交的场景。
- NESTED:在当前事务中创建保存点,子事务回滚不影响父事务。适用于批量处理中允许部分失败的回退策略。
2.4 边界划分的代码示例
以下代码展示了正确的三层事务边界划分。注意@Transactional仅出现在Service层,Controller只负责调用并处理异常。
// Controller层:无事务管理权
@RestController
public class OrderController {
private final OrderService orderService;
@PostMapping("/orders")
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
try {
OrderResponse response = orderService.createOrder(request);
return ResponseEntity.ok(response);
} catch (InsufficientStockException e) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse(e.getMessage()));
} catch (PaymentException e) {
return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED).body(new ErrorResponse(e.getMessage()));
}
}
}
// Service层:事务边界定义者
@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryRepository inventoryRepository;
public OrderResponse createOrder(OrderRequest request) {
// 1. 扣减库存(参与当前事务)
inventoryRepository.deduct(request.getProductId(), request.getQuantity());
// 2. 创建订单(参与当前事务)
Order order = orderRepository.save(new Order(request));
// 3. 如果后续操作失败,整个事务回滚
return new OrderResponse(order);
}
}
// Repository层:事务参与者
@Repository
public class InventoryRepository {
@PersistenceContext
private EntityManager em;
public void deduct(Long productId, int quantity) {
// 抛出异常时由Service层事务管理器统一回滚
int updated = em.createQuery(
"UPDATE Product p SET p.stock = p.stock - :qty WHERE p.id = :id AND p.stock >= :qty")
.setParameter("qty", quantity)
.setParameter("id", productId)
.executeUpdate();
if (updated == 0) {
throw new InsufficientStockException("库存不足");
}
}
}
边界划分黄金法则:Controller层只做协议转换(HTTP ↔ 业务对象),Service层做事务编排与业务规则,Repository层做数据访问。任何跨越这三层的事务管理行为都会导致代码难以测试和维护。
2.5 事务边界常见错误与避坑清单
- 事务注解放在私有方法上:Spring AOP代理无法拦截私有方法,
@Transactional会失效。 - 在同一个类中调用带事务的方法:自调用不走代理,事务传播行为不生效。
- 在Controller层捕获异常后不重新抛出:事务管理器无法感知异常,导致本该回滚的事务被提交。
- Repository层直接操作事务提交:破坏Service层的事务边界,导致部分提交的数据不一致。
- 事务超时设置过大:长时间持有数据库连接,在高并发下快速耗尽连接池。
| 错误场景 | 后果 | 正确做法 |
|---|---|---|
Controller层使用@Transactional |
事务范围过大,连接长时间占用 | 事务注解仅放在Service层方法上 |
| Service层捕获异常后不抛出 | 事务正常提交,数据不一致 | 捕获后根据业务决定抛出或标记回滚 |
Repository层调用em.flush()后提交 |
部分数据提前持久化,回滚失效 | Repository只做数据操作,不管理事务 |
| 跨Service调用未考虑传播行为 | 事务意外合并或隔离 | 明确声明@Transactional(propagation = ...) |