Spring 为事务管理提供了丰富的支持, 对于底层不同的事务 ( 如 Java Transaction API (JTA), JDBC, Hibernate, Java Persistence API (JPA), and Java Data Objects (JDO)) 管理提供了统一的抽象编程模型, 而且它的 API 也非常易于开发者理解和使用.
Spring 事务管理分为编程式和声明式的两种. 编程式事务指的是通过编码方式实现事务; 声明式事务基于 AOP, 将具体业务逻辑与事务处理解耦. 声明式事务对我们的业务代码无侵入, 一般更倾向于选择它. 而编程式的事务一般使用 TransactionTemplate . 本文主要介绍常用的声明式事务.
1.Transactional 注解基本用法
1.1 配置数据源和事务管理器
配置数据源可以采用 xml 配置, 也可以在代码中. 下面是一段代码配置示例.
- ...
- @Configuration
- @MapperScan(basePackages = "com.xxx.dao",
- sqlSessionFactoryRef = DataSourceConfiguration.SQL_SESSION_FACTORY_NAME)
- public class DataSourceConfiguration{
- public static final String DATASOURCE_NAME = "xxxDataSource";
- public static final String SQL_SESSION_FACTORY_NAME = "xxxProductSessionFactory";
- public static final String TRANSACTION_MANAGER_NAME = "xxxProductTransaction";
- @Bean(name = DataSourceConfiguration.DATASOURCE_NAME)
- public DataSource getDataSource(){
- DataSourceProperties dataSourceProperties = getDataSourceProperties();
- return dataSourceProperties.initializeDataSourceBuilder().build();
- }
- @Bean
- @ConfigurationProperties("spring.datasource.xxx")
- // 数据源配置写在 properties 配置文件中, 以 spring.datasource 开头, 借助 ConfigurationProperties 注解来读取
- public DataSourceProperties getDataSourceProperties(){
- return new DataSourceProperties();
- }
- @Bean(name = DataSourceConfiguration.TRANSACTION_MANAGER_NAME)
- public PlatformTransactionManager transactionManager(
- @Named(DataSourceConfiguration.DATASOURCE_NAME)final DataSource dataSource) {
- return new DataSourceTransactionManager(dataSource);
- }
- }
1.2 给需要使用事务的地方添加注解
- @Service
- public class XXXServiceImplimplements XXXService{
- ...
- @Override
- @Transactional(transactionManager = DataSourceConfiguration.TRANSACTION_MANAGER_NAME)
- public void foo(){
- }
如果项目中只有一个事务管理器, 可以不用设置注解的 transactionManager 属性, 关于注解属性的详细介绍在第 3 节.
虽然使用注解方式来实现事务管理看起来非常简单, 但是如果对 Spring 注解实现事务的原理不够理解的话, 在使用事务出现错误的时候, 很难理解, 排查问题.
2. Transactional 注解实现的原理
对于使用了 Transactional 注解的方法的类, Spring AOP 代理会在运行时生成这个类的代理对象.
当这个对象运行这个注解方法时, 会读取 @Transactional 注解里面配置的信息, 决定该方法是否要使用 TransactionInterceptor 拦截器来拦截.
当拦截器拦截该方法时, 会在调用该方法之前, 创建事务, 并在这个事务中执行该方法, 最后根据执行是否有异常使用 抽象事务管理器(AbstractPlatformTransactionManager) 操作数据源提交或者回滚这个事务. 下面是一张常见的 Spring 注解事务实现机制图.
Spring AOP 代理有两种实现, Cglib 和 JDK Proxy . 对于实现了接口的类, 默认走 JDK 代理 , 对于未实现接口的类, 默认走 Cglib .Spring 中事务管理的框架由 AbstractPlatformTransactionManager 提供, 具体 Manager 的实现取决于操作的数据源, 比如数据源是数据库时, 就会使用 DataSourceTransactionManager 管理 JDBC 的 Connection. 下图是 AbstractPlatformTransactionManager 具体实现类关系.
3. Transactional 注解提供的属性
除了基本用法之外, 还应该知道它提供了哪些属性供开发者设置, 以及这些属性如何在事务管理时发挥作用.
- @Target({ElementType.METHOD, ElementType.TYPE})
- @Retention(RetentionPolicy.RUNTIME)
- @Inherited
- @Documented
- public @interface Transactional {
- @AliasFor("transactionManager")
- Stringvalue()default "";
- @AliasFor("value")
- StringtransactionManager()default "";
- Propagationpropagation()default Propagation.REQUIRED;
- Isolationisolation()default Isolation.DEFAULT;
- int timeout()default TransactionDefinition.TIMEOUT_DEFAULT;
- boolean readOnly()default false;
- Class<? extends Throwable>[] rollbackFor() default {};
- String[] rollbackForClassName() default {};
- Class<? extends Throwable>[] noRollbackFor() default {};
- String[] noRollbackForClassName() default {};
- }
name , transactionManager : 当项目中有多个事务管理器时, 选择使用哪一个
propagation : 事务的传播行为, 默认为 REQUIRED. 这里需要开发者有更多了解, 参考 官方 API 文档 .
isolation : 事务的隔离级别, 默认 DEFAULT, 即采用数据服务器设置的隔离级别.
timeout : 超时时间, 默认 - 1
readOnly : 是否为只读事务, 默认 false; 对于一些不需要事务的只读操作, 可以设置为 true
rollbackFor , rollbackForClassName : 指定触发回滚的异常类型
noRollbackFor , noRollbackForClassName : 指定不触发混滚的异常类型
@Transactional 注解也可以添加到类级别上. 当把 @Transactional 注解放在类级别时, 表示所有该类的公共方法都配置相同的事务属性信息. 此时如果方法级别也加了注解, 会覆盖类级别的注解.
4. Spring 事务失效的常见原因
了解 Spring 注解实现事务的基本原理, 看下一些比较容易出现的事务失败的原因.
4.1 内部方法调用失效
Spring 是通过 AOP 代理的方式来识别方法上面的注解, 如果是同一个类中一个方法 A 调用另一个注解方法 B, 将无法被代理从而导致事务失效. 因此方法 A 上面没有注解, 代理对象在执行这个方法时, 不会给 A 方法添加事务, 所以被调用的 B 方法也不会运行在事务里面了.
4.2 private 方法失效
这个意思是说只有给一个 public 方法加 Transactional 注解. Spring 才会给它进行事务管理. 原因是 TransactionInterceptor 拦截器时, 会间接调用到一个 AbstractFallbackTransactionAttributeSource 里面的 computeTransactionAttribute 方法, 通过这个方法来回去注解上的各个属性, 同时会判断该方法是否为 public , 若不是 public 则不会读取注解属性, 返回 null.
- protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass){
- // Don't allow no-public methods as required.
- if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
- return null;
- }
- ...
- }
4.3 多线程切换导致事务失效
Spring 会把很多一个事务中使用的很多变量放到很多 ThreadLocal 中, 并保存在一个 org.springframework.transaction.support.TransactionSynchronizationManager 类中. 如果在一个线程中启动了一个事务, Spring 首先会初始化设置这些 TheadLocal 的值( actualTransactionActive=true ), 并把事务里这些 ThreadLocal 通过 TransactionSynchronizationManager 与这个线程绑定了. 那么假设此时切换到另一个线程中, 另一个线程将无法获得这些 ThreadLocal , 也无法知道当前是不是在一个事务里面, 更无法知道这个事务做了哪些配置. 所以切换线程之后, 事务就无法管理了.
- public abstract class TransactionSynchronizationManager{
- private stati final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
- private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
- private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations");
- private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal("Current transaction name");
- private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal("Current transaction read-only status");
- private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal("Current transaction isolation level");
- private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal("Actual transaction active");
- }
了解了切换线程导致的事务失效原因后, 是不是觉得可以做点什么, 让事务延续呢? 当然是可以的, 如果我们手动地给另一个线程也设置这些 ThreadLocal, 就可以将事务延续了.
4.4 单元测试中注解的方法自动回滚失效
有时候我们会给单元测试里面的某个 testFunction 加上 @Transactional 注解, 希望这个方法运行完成之后, 可以自动回滚, 不污染数据源. 但是有时回滚会失效.
先看下官方文档中, 测试章节 提到的问题.
If your test is @Transactional , it rolls back the transaction at the end of each test method by default. However, as using this arrangement with either RANDOM_PORT or DEFINED_PORT implicitly provides a real servlet environment, the HTTP client and server run in separate threads and, thus, in separate transactions. Any transaction initiated on the server does not roll back in this case.
通常情况下, @SpringBootTest 注解里面的 webEnvironment 默认值是 MOCK , 它在运行单测时, 不会真正启动一个 Server 来进行测试. 但是如果我们手动将这个值改为 RANDOM_PORT/DEFINED_PORT 后, 会启动一个真实的 Web 环境, 内置的 Server 会启动起来. 然后此时发出 HTTP 请求的 Client 和处理请求的 Server 试运行在不同的线程里面的, 按照 3.3 的分析, 这里的事务肯定就无法自动生效了, 单元测试方法上面的 @Transactional 注解就是失效了.
所以如果你不需要启动真实的 Server 时, 建议使用 @SpringBootTest 默认的 webEnvironment , 此时请求的 client 只能用 MockMvc 来发, 而不是 TestRestTemplate , 这样就可以让单测跑完之后自动回滚了.
来源: http://www.tuicool.com/articles/z2YvieV