Spring Framework 提供依赖注入容器,以及面向 AOP、数据访问、事务、消息传递和 Web 应用的专用模块。事务管理体现了该框架的集成模型:应用程序代码声明边界,拦截器则协调特定资源的 PlatformTransactionManager 或响应式事务管理器。
基于代理的声明式事务 #
在默认的通知模式下,Spring 会使用 AOP 代理包装符合条件的 Bean。进入与 @Transactional 匹配的方法的调用会经过 TransactionInterceptor,后者读取事务元数据,启动事务或加入现有事务,调用目标对象,然后提交或回滚。
@Service
class OrderService {
private final OrderRepository orders;
private final PaymentGateway payments;
@Transactional
public Order place(CreateOrder command) {
Order order = orders.insert(command);
payments.reserve(order);
return order;
}
}代理存在实际限制。调用必须经过代理进入,并且方法必须能够被所选代理机制拦截。基于接口的 JDK 代理会公开接口方法;基于类的代理会继承目标类,因此无法为 final 方法提供通知。
自调用 #
this.reprice() 之类的调用会停留在目标对象内部,不会重新进入其代理。因此,如果只能通过自调用到达 reprice(),该方法上的事务元数据会被忽略:
public void importOrders(List<Order> batch) {
batch.forEach(this::storeOne); // 绕过 storeOne 上的事务通知
}如果事务操作代表一个真实边界,应将其移动到单独的注入式服务中。编程式事务管理或 AspectJ 编织也可以作为替代方案;注入 Bean 自身的代理会使领域代码耦合到容器机制,并增加测试难度。
边界粒度 #
一个事务应包含一个应用层面的完整性操作。控制器级边界往往会包含序列化和无关的网络操作,而仓储级边界可能会将一个业务操作拆分到多个提交中。服务方法通常是有用的单元,因为它能够围绕领域不变量协调多个仓储。
在分析锁持有时间、重试和故障恢复之前,不要让数据库事务跨越不可控的远程调用而持续打开。本地事务无法使外部服务具备原子性;可能需要使用 outbox、幂等性或 Saga 风格的协调机制。
传播、隔离与回滚 #
传播定义方法与现有事务之间的关系:
REQUIRED是默认选项,会加入现有事务,或创建新事务。REQUIRES_NEW会挂起现有事务并启动独立事务,因此可能需要另一个连接池连接。SUPPORTS在存在事务时加入,否则不使用事务运行。MANDATORY要求存在现有事务;NEVER禁止事务存在。NOT_SUPPORTED会挂起现有事务。- 当事务管理器和资源支持保存点时,
NESTED会在一个物理事务中使用保存点。
隔离级别请求会映射到资源的能力。DEFAULT 使用其配置的默认值;其他值对应标准 JDBC 隔离级别。声明某个隔离级别并不能证明每个数据库都实现了相同的锁定或快照语义,因此应针对选定的数据库和驱动程序验证实际行为。
默认情况下,Spring 会在 RuntimeException 和 Error 上将事务标记为回滚,但不会对受检异常执行此操作。当异常分类的语义不同时,应配置 rollbackFor 或 noRollbackFor。如果内部参与者将共享事务标记为仅回滚,外层看似提交的操作可能会抛出 UnexpectedRollbackException。
通知顺序 #
安全、重试、缓存、指标、锁定和事务等关注点都可以包装同一个调用。嵌套顺序会改变语义:在一个事务内进行重试,并不等价于每次尝试都开启新事务;在授权之前执行缓存还可能暴露数据。在支持的情况下使用 Ordered/@Order 指定顺序,测试最终的调用顺序;当所需顺序不明确时,避免堆叠注解。
声明式代理的替代方案 #
根据所需边界,在以下三种方法中进行选择:
- 将操作重构到另一个服务中,并保留声明式通知。
- 使用
TransactionTemplate创建显式的本地代码块,使控制流和异常处理清晰可见。 - 当通过编织实现自调用通知是有意采用的系统级选择时,启用
@TransactionalAspectJ 模式。
Order saved = transactionTemplate.execute(status -> orders.insert(command));
评论
在 GitHub 上阅读或评论(发表评论需要登录)