Spring 事务和事务传播机制
Spring 事务和事务传播机制
一、Spring 事务的基本概念
事务是一组操作,被视为一个不可分割的工作单元,要么全部完成,要么全部失败回滚,以此来确保数据的一致性和完整性。Spring事务管理允许我们在应用程序中声明式地或编程式地管理事务,它提供了一个事务管理抽象层,使得事务的使用和配置更加简单和灵活。
Spring事务管理不直接管理数据库事务,而是通过委托给底层的数据库事务管理器,如JDBC或Hibernate的事务管理器,来实现对数据库事务的控制。Spring事务管理分为编码式和声明式的两种方式。
- 编程式事务:通过编码方式实现事务。
- 声明式事务:基于AOP,即使用@Transactional注解,将具体业务逻辑与事务处理解耦。声明式事务管理使业务代码逻辑不受污染,因此在实际使用中声明式事务用的比较多。
通过简单的配置或注解,Spring允许开发者将事务管理从业务代码中分离出来,提高了代码的可读性和可维护性。使用@Transactional注解,简单配置即可管理事务。例如,在Service层的方法上标注@Transactional注解,Spring将自动处理事务边界。
使用TransactionTemplate或PlatformTransactionManager手动管理事务,虽然不常用,但在某些情况下,可以用于需要更细粒度控制的场景。
值得注意的是,Spring利用AOP和事务拦截器来拦截被@Transactional注解的方法,在方法调用前开启事务,在方法调用后根据方法执行结果决定提交或回滚事务。
此外,在某些情况下,@Transactional注解可能会失效,包括:
-
方法是非public的,因为事务的底层是利用cglib代理实现,cglib是基于父子类来实现的,子类是不能重载父类的private方法,所以无法很好利用代理。
-
使用的数据库引擎不支持事务,因为Spring的事务调用的也是数据库事务的API,如果数据库都不支持事务,那么@Transactional注解也就失效了。
-
添加了@Transactional注解的方法不能在同一个类中调用,否则会使事务失效。这是因为Spring AOP通过代理来管理事务,自调用不会经过代理。
-
@Transactional注解属性propagation设置错误,若是错误的配置以下三种propagation,事务将不会发生回滚:
- TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
-
@Transactional注解属性rollbackFor设置错误,rollbackFor可以指定能够触发事务回滚的异常类型。默认情况下,Spring仅在抛出未检查异常(继承自RuntimeException)时回滚事务。对于受检异常(继承自Exception),事务不会回滚,除非明确配置了rollbackFor属性。
-
异常被捕获了,导致@Transactional失效。当事务方法中抛出一个异常后,应该是需要表示当前事务需要rollback,如果在事务方法中手动捕获了该异常,那么事务方法则会认为当前事务应该正常提交,此时就会出现事务方法中明明有报错信息表示当前事务需要回滚,但是事务方法认为是正常,出现了前后不一致,也会因此抛出UnexpectedRollbackException异常。
二、Spring 事务的传播机制
事务的传播机制是Spring事务管理中的核心概念之一。在多个包含事务的方法里相互调用时,它们之间是如何扩散或者传递的,就是事务的传播机制。这个机制保证了一个事务在多个调用方法之间的可控性(稳定性)。
Spring框架中,事务传播机制是指在一个事务方法调用另一个事务方法时,Spring如何管理这些方法之间的事务边界。Spring提供了七种事务传播行为,以满足不同的业务需求。
-
REQUIRED:这是最常用的事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这种行为确保所有事务操作都在一个统一的事务上下文中执行。适用场景是大多数情况下使用此传播行为,确保多个事务操作在同一个事务中执行,以保持数据一致性。
如果外层方法没有事务管理,那么调用事务方法的外层方法没有被事务注解(如@Transactional)标记,在调用事务方法时,当前就没有事务存在。例如,如果方法A没有事务管理,方法B被标记为@Transactional,当调用方法A时,方法A没有事务管理,因此当前没有事务。当方法A调用方法B时,由于方法A没有事务,所以方法B会创建一个新的事务。如果将方法A也标记为@Transactional,那么方法A开启了一个事务,因此当前有事务。当方法A调用方法B时,方法B会加入方法A的事务,而不是创建新的事务。
-
SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。这种传播行为适用于既能在事务内执行,也能在事务外执行的操作。比如,某些只读操作或性能要求不高的操作。
-
MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。这种传播行为适用于必须在现有事务中执行的操作。通常用于一些关键业务逻辑,要求调用者必须在事务中运行。
-
REQUIRES_NEW:总是启动一个新的事务。如果当前存在事务,则将当前事务挂起。在新的事务执行完成后,恢复先前的事务。这种传播行为适用于需要独立事务的情况,例如记录日志或审计信息,不希望这些操作受到主事务的影响。
-
NOT_SUPPORTED:以非事务方式执行操作。如果当前存在事务,则将当前事务挂起。这种传播行为适用于不需要事务支持的操作,或某些性能要求高、不希望受到事务管理开销影响的操作。
-
NEVER:以非事务方式执行。如果当前存在事务,则抛出异常。这种传播行为适用于明确不希望在事务中执行的操作。比如,某些数据库操作可能不支持事务。
-
NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前不存在事务,则等价于REQUIRED,即创建一个新的事务。嵌套事务依赖于底层数据库对保存点(savepoint)的支持。这种传播行为适用于需要部分提交或回滚的复杂业务操作。比如,复杂的财务操作,某些步骤失败后需要回滚到特定点。
下面通过一个具体的例子来说明事务传播机制的作用:
public class MyService {public void methodA() {// 非事务代码methodB(); // 调用有事务管理的方法// 继续执行非事务代码}@Transactionalpublic void methodB() {// 有事务管理的代码// 执行数据库操作}
}
在这个例子中,methodA开始执行时,此时没有事务。当methodA调用methodB时,由于methodB被标记为@Transactional,Spring会为methodB创建一个新的事务。methodB在事务中执行其代码(如数据库操作)。methodB执行完成后,事务提交或回滚(取决于方法执行的结果和异常情况)。methodB返回到methodA,此时事务已经结束。methodA继续执行剩余代码,但此时已经没有事务。所以,在methodA中,methodB执行的部分是在事务中的,但methodB返回后,methodA剩余的代码是不在事务中的。事务边界由methodB的开始和结束决定,事务结束后事务上下文不再存在。
三、事务传播机制的组合和策略
在实际应用中,可能会遇到多种事务传播行为的组合使用。理解这些组合的行为对于正确设计事务管理策略至关重要。
- REQUIRED-REQUIRED:如果A方法在事务中运行,则B方法将继承A方法的事务。如果A方法没有事务,则B方法将在一个新的事务中运行。
- REQUIRED-SUPPORTS:如果A方法在事务中运行,则B方法将继承A方法的事务。如果A方法没有事务,则B方法将以非事务方式运行。
- REQUIRED-MANDATORY:如果A方法在事务中运行,则B方法将继承A方法的事务。如果A方法没有事务,则B方法将抛出异常。
- REQUIRED-REQUIRES_NEW:无论A方法是否在事务中运行,B方法都会开启一个新的事务并在该事务中运行。
- REQUIRED-NOT_SUPPORTED:如果A方法在事务中运行,则B方法将以非事务方式运行。如果A方法没有事务,则B方法也将以非事务方式运行。
- REQUIRED-NEVER:如果A方法在事务中运行,则B方法将抛出异常。如果A方法没有事务,则B方法将以非事务方式运行。
- REQUIRED-NESTED:如果A方法在事务中运行,则B方法将作为A方法的嵌套事务运行。如果A方法没有事务,则B方法将在一个新的事务中运行。
类似的,还可以根据其他传播行为组合出更多的策略。在实际应用中,需要根据具体的业务需求来选择合适的事务传播行为组合。
四、事务隔离级别
事务隔离级别指的是一个事务对数据的修改与另一个并行的事务的隔离程度。当多个事务同时访问相同数据时,如果没有采取必要的隔离机制,就可能发生以下问题:
- 脏读:一个事务读取了另一个事务未提交的更改。
- 不可重复读:在同一个事务中,两次读取同一数据的结果不一致。
- 幻读:一个事务读取了另一个事务插入的数据。
Spring事务管理框架支持标准的数据库事务隔离级别,这些隔离级别与底层数据库系统所支持的事务隔离级别
为了处理上述的并发问题,数据库系统定义了四种标准的事务隔离级别,Spring事务管理框架也支持这些隔离级别。以下是四种标准的事务隔离级别:
-
READ_UNCOMMITTED(读取未提交):
- 允许读取未提交的数据。
- 可能会发生脏读、不可重复读和幻读。
- 性能较高,但数据一致性最差。
-
READ_COMMITTED(读取已提交):
- 只能读取已提交的数据。
- 避免脏读,但可能会发生不可重复读和幻读。
- 大多数数据库系统的默认隔离级别。
-
REPEATABLE_READ(可重复读):
- 保证在同一个事务中多次读取同一数据时,结果一致。
- 避免脏读和不可重复读,但可能会发生幻读(在某些数据库系统中,如MySQL的InnoDB引擎,通过间隙锁可以避免幻读)。
- MySQL的默认隔离级别(在InnoDB引擎下,实际上通过额外的机制达到了SERIALIZABLE的一部分效果,即避免了幻读)。
-
SERIALIZABLE(串行化):
- 通过强制事务串行执行,避免脏读、不可重复读和幻读。
- 数据一致性最好,但性能最差,因为事务需要等待其他事务完成才能继续执行。
在Spring中,可以通过@Transactional
注解的isolation
属性来设置事务的隔离级别。例如:
@Transactional(isolation = Isolation.SERIALIZABLE)
public void myTransactionalMethod() {// 方法体
}
需要注意的是,事务隔离级别的选择应该根据具体的应用场景和性能需求来决定。较高的隔离级别可以提供更好的数据一致性,但可能会牺牲性能;较低的隔离级别可以提供更好的性能,但可能会牺牲数据一致性。
五、Spring事务的超时设置
除了隔离级别外,Spring事务管理还提供了超时设置,用于控制事务的最大执行时间。如果事务在指定的时间内没有完成,那么事务将被自动回滚。
在Spring中,可以通过@Transactional
注解的timeout
属性来设置事务的超时时间。例如:
@Transactional(timeout = 30) // 设置超时时间为30秒
public void myTransactionalMethod() {// 方法体
}
需要注意的是,超时时间的单位是秒。如果设置为-1,则表示没有超时限制。
超时设置对于长时间运行的事务特别有用,可以避免因为某些原因(如死锁)导致事务无限期地等待下去。通过设置一个合理的超时时间,可以在一定程度上提高系统的健壮性和可用性。
六、Spring事务的回滚规则
在Spring事务管理中,事务的回滚通常是由异常触发的。当事务方法抛出一个运行时异常(RuntimeException)或错误(Error)时,默认情况下,Spring会回滚事务。此外,还可以通过@Transactional
注解的rollbackFor
和noRollbackFor
属性来定制回滚规则。
rollbackFor
:指定哪些异常类型会导致事务回滚。例如:
@Transactional(rollbackFor = {CustomException.class, AnotherException.class})
public void myTransactionalMethod() {// 方法体
}
在这个例子中,如果myTransactionalMethod
方法抛出了CustomException
或AnotherException
异常,那么事务将被回滚。
noRollbackFor
:指定哪些异常类型不会导致事务回滚。例如:
@Transactional(noRollbackFor = {BusinessException.class})
public void myTransactionalMethod() {// 方法体
}
在这个例子中,即使myTransactionalMethod
方法抛出了BusinessException
异常,事务也不会被回滚。
需要注意的是,rollbackFor
和noRollbackFor
属性可以同时使用,以定制更复杂的回滚规则。此外,如果事务方法抛出了一个受检异常(checked exception),并且没有通过rollbackFor
属性指定该异常类型会导致事务回滚,那么默认情况下,事务将不会被回滚。这是因为受检异常通常是由编程错误或业务逻辑错误引起的,而不是由运行时环境或数据问题引起的。因此,在大多数情况下,不需要对受检异常进行回滚处理。
七、Spring事务的最佳实践
在使用Spring事务管理时,有一些最佳实践可以帮助开发者更好地管理事务和提高系统的健壮性和性能:
-
合理设置事务的传播行为:根据具体的业务需求和性能要求来选择合适的事务传播行为。避免不必要的事务嵌套和事务挂起操作。
-
合理设置事务的隔离级别:根据数据一致性和性能需求来选择合适的事务隔离级别。避免使用过高的隔离级别导致性能下降或使用过低的隔离级别导致数据不一致。
-
合理设置事务的超时时间:为长时间运行的事务设置一个合理的超时时间,以避免因为某些原因(如死锁)导致事务无限期地等待下去。
-
定制回滚规则:根据具体的业务需求来定制事务的回滚规则。对于不需要回滚的异常类型,可以使用
noRollbackFor
属性来避免不必要的回滚操作。 -
避免大事务:尽量避免在单个事务中执行过多的数据库操作或处理过多的数据。大事务可能会导致性能下降、死锁等问题。如果必须处理大量数据或执行多个数据库操作,可以考虑将它们拆分成多个小事务来处理。
-
使用合适的数据库连接池:配置一个合适的数据库连接池来管理数据库连接资源。通过合理的连接池配置可以提高数据库操作的性能和可靠性。
-
监控和调优事务性能:定期对事务性能进行监控和调优。通过分析事务的执行时间和资源消耗情况来发现潜在的性能瓶颈并进行优化处理。
-
处理事务回滚后的异常:在事务回滚后,可能需要处理一些额外的逻辑来恢复系统的状态或通知用户。因此,在编写事务方法时应该考虑到这一点并编写相应的异常处理代码来应对这种情况。
-
避免在事务中调用远程服务:在事务中调用远程服务可能会导致分布式事务的问题,增加系统的复杂性和不确定性。如果必须调用远程服务,可以考虑将远程服务调用放在事务之外或使用其他机制来保证数据的一致性。
-
使用Spring的声明式事务管理:尽可能使用Spring的声明式事务管理来简化事务的配置和管理。通过简单的注解或XML配置就可以实现复杂的事务管理功能,而不需要编写大量的代码来手动管理事务。
总之,Spring事务管理是一个强大而灵活的工具,可以帮助开发者更好地管理数据库事务并提高系统的健壮性和性能。但是,在使用Spring事务管理时也需要注意一些最佳实践和潜在的问题,以确保系统的正确性和可靠性。