Spring源码解析--事务的详细讲解

Spring为用户封装了JDBC操作,所以就必然会遇到操作数据库的事务问题,而Spring作为强大的全能框架,必然会给用户提供解决方案。

一、Spring中事务的配置方式

Spring中提供了多种配置事务的方式,主要分成两大类:声明式事务和编程式事务

无论使用哪种配置方式,都需要在Spring全局配置文件中先配置数据源和事务管理器,配置如下:

 1 <!-- 配置数据源 -->
 2     <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
 3         <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
 4         <property name="url" value="jdbc:mysql://localhost:3306/test"/>
 5         <property name="username" value="root"/>
 6         <property name="password" value="*****"/>
 7     </bean>
 8 
 9     <!-- 配置事务管理器-->
10     <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
11         <property name="dataSource" ref="dataSource" />
12     </bean>
13     

1.1、声明式事务

声明式事务的实现完全依赖于Spring的AOP机制,其本质就是通过AOP在目标方法执行之前加入事务,在方法执行之后根据方法执行结果选择是执行回滚操作还是执行提交操作。

开始声明式事务配置可以有三种配置方式

1.1.1、使用@Transacation注解

首先在XML配置文件中添加事务注解的声明,如下:

1 <!-- 配置事务声明 -->
2     <tx:annotation-driven transaction-manager="transactionManager"/>

使用时直接在需要添加事务的类或方法上添加@Transaction注解即可,如下示:

1     @Transactional
2     public User addAndGet(User user, Long userId) {
3         userMapper.addUser(user);
4         User result = userMapper.getUserById(userId);
5         return result;
6     }

Tip:使用注解可以修饰在接口上、接口方法上、类上、类的public方法上,而修饰类的非public方法上会无效。另外由于@Transaction注解的实现是通过AOP增强来实现的,所以当内部调用添加了@Transaction修饰的方法时,注解也会失效。只有通过代理来访问方法时才会生效。

1.1.2、使用<tx>标签配置的切面拦截器

配置事务拦截器,通过配置AOP切面的方式,将事务拦截器织入到拦截的目标对象上

 1 <!-- 配置事务拦截器-->
 2     <tx:advice id="txAdvice" transaction-manager="transactionManager">
 3         <tx:attributes>
 4             /** 表示拦截目标中所有add开头的方法都会添加事务*/
 5             <tx:method name="add*" read-only="true" propagation="REQUIRED"/>
 6         </tx:attributes>
 7     </tx:advice>
 8 
 9     <!-- 配置切面, 织入事务增强-->
10     <aop:config>
11         <aop:pointcut id="interceptorPointCuts"
12                       expression="execution(* com.lucky.test.spring.transaction.*.*(..))" />
13         <aop:advisor advice-ref="txAdvice"
14                      pointcut-ref="interceptorPointCuts" />
15     </aop:config>

1.1.3、使用事务代理工厂类TransactionProxyFactoryBean

 1 <!-- 定义代理类 -->
 2     <bean id="transactionBase"
 3           class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
 4           lazy-init="true" abstract="true">
 5         <!-- 配置事务管理器 -->
 6         <property name="transactionManager" ref="transactionManager" />
 7         <!-- 配置事务属性 -->
 8         <property name="transactionAttributes">
 9             <props>
10                 <prop key="add*">PROPAGATION_REQUIRED</prop>
11             </props>
12         </property>
13     </bean>
14 
15     <!-- 配置Service -->
16     <bean id="userService" parent="transactionBase" >
17         <property name="target" ref="userServiceTarget" />
18     </bean>
19     <bean id="userServiceTarget" class="com.lucky.test.spring.transaction.UserServiceImpl">
20     </bean>
21     <bean id="userMapper" class="com.lucky.test.spring.transaction.UserMapper">
22         <constructor-arg index="0" ref="dataSource"/>
23     </bean>

1.2、编程式事务

编程式事务的实现依赖于底层持久化框架提供的API,显示的执行开启事务、提交事务和回滚事务的操作。

使用编程式事务通常有三种方式,分别是使用不同的事务管理器TransactionTemplate、TransactionOpeator或TransactionManager的实现

1.2.1、使用TransactionTemplate方式

TransactionTemplate相当于事务模版,配置文件中需要初始化TransactionTemplate实例,并且注入事务管理器,配置如下:

1     <!-- 配置事务模版 -->
2     <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
3         <property name="transactionManager" ref="transactionManager"/>
4     </bean>

在需要添加使用事务的对象中注入TransactionTemplate即可使用,使用案例如下:

 1 public class UserServiceImpl implements UserService{
 2 
 3     @Autowired
 4     private TransactionTemplate transactionTemplate;
 5 
 6     @Autowired
 7     private UserMapper userMapper;
 8 
 9 
10     @Override
11     public User addAndGet(User user, Long userId) {
12         /**
13          * 通过内部类执行具体的业务逻辑,如果事务不需要返回数据,回调类可以使用TransactionCallWithoutResult
14          * */
15         return transactionTemplate.execute(new TransactionCallback<User>() {
16             @Override
17             public User doInTransaction(TransactionStatus status) {
18                 userMapper.addUser(user);
19                 User result = userMapper.getUserById(userId);
20                 return result;
21             }
22         });
23     }
24 }

1.2.2、使用TransactionOpeator方式

TransactionOpeator是Spring5才提供的一种反应式事务管理方式,使用方式如下:

 1 public class SimpleService implements Service {
 2 
 3     private final TransactionalOperator transactionalOperator;
 4 
 5     public SimpleService(ReactiveTransactionManager transactionManager) {
 6         this.transactionOperator = TransactionalOperator.create(transactionManager);
 7     }
 8 
 9     public Mono<Object> someServiceMethod() {
10         Mono<Object> update = updateOperation1();
11         return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional);
12     }
13 }
1 transactionalOperator.execute(new TransactionCallback<>() {
2     public Mono<Object> doInTransaction(ReactiveTransaction status) {
3         return updateOperation1().then(updateOperation2)
4                     .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly());
5         }
6     }
7 });

1.2.3、使用TransactionManager的实现类PlatformTransactionManager方式

直接使用TransactionManager实现类的方式,相当于不采用任何模版,而直接使用底层的接口来手动开启、提交和回滚事务

涉及到的接口有PlatformTransactionManager(事务管理器)、TransacationStatus(事务状态)、TransactionDefination(一个事务的定义)

使用方法如下:

 1 @Override
 2     public User addAndGet(User user, Long userId) {
 3         User result = null;
 4 
 5         //1.创建一个事务,默认实现类为DefaultTransactionDefinition
 6         DefaultTransactionDefinition transaction = new DefaultTransactionDefinition();
 7         transaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
 8 
 9         //2.获取事务的状态
10         TransactionStatus transactionStatus = transactionManager.getTransaction(transaction);
11 
12         try{
13             //3.执行业务逻辑
14             userMapper.addUser(user);
15             result = userMapper.getUserById(userId);
16         }catch (Exception e){
17             e.printStackTrace();
18             //4.回滚事务
19             transactionManager.rollback(transactionStatus);
20         }
21         //5.提交事务
22         transactionManager.commit(transactionStatus);
23 
24         return result;
25     }

1.3、声明式事务和编程式事务的对比

1.3.1、优缺点对比

声明式事务优点:

1、全局配置,配置和维护成本较小

2、可以和业务代码解偶

声明式事务缺点:

1、事务的粒度最小为方法级(可以通过将大方法拆分成多个小方法来解决)

2、不可以控制指定方法可不可以使用事务,一旦全局配置之后,符合条件的方法都会自动加上事务(需要手动控制哪些方法加事务的场景几乎不会出现)

3、当项目中绝大多数操作都不需要用到事务时,配置全局事务会给不必要的方法加上事务,增加了性能消耗

编程式事务优点:

1、事务的粒度可控

2、可以人为控制哪些方法添加事务哪些方法不加事务

编程式事务缺点:

1、需要在业务代码中手动添加事务配置,和业务代码耦合度比较高,对于代码侵入性大,背离了Spring的非侵入式开发方式。

2、当项目中需要事务的地方比较多时,配置和维护成本较大

1.3.2、使用场景对比

1、当业务代码中使用到的事务场景比较少时,比如项目中绝大多数的操作均是查询操作时而只有很少的更新操作时,建议使用编程式事务

2、当业务代码中存在大量的操作都需要使用到事务,那么使用声明式事务便于管理,且可以和业务代码解偶

3、需要显式的设置事务的名称时只能通过编程式事务来实现

4、正常场景下都是采用声明式事务配置方式,因为这种方式更方便且更符合Spring的非侵入式开发原则

二、Spring中事务的传播机制

当使用事务时,单个方法执行成功则提交事务,执行失败则回滚事务,这个都很好理解,但是如果存储一个方法A调用另一个方法B时,此时两个方法如果配置的事务不一样,就会涉及到事务在多个方法中的传播机制。

比如当方法A调用方法B时,就会存在一下几种情况:

1、方法A和方法B都执行成功

2、方法A执行成功,方法B执行失败

3、方法A执行失败,方法B执行成功

4、方法A和方法B都执行失败

此时是提交还是回滚需要判断方法A和方法B分别是否存在事务来决定最终的执行结果

Spring中定义了六种事务传播机制,主要作用于被调用的方法,通过设置Propagation属性的值来配置,取值范围在Propagation中有定义,分别如下:

 1 public enum Propagation {
 2 
 3         /**
 4          * 默认事务传播机制
 5          * 如果当前存在事务就加入事务;
 6          * 如果不存在事务,则创建一个新事务
 7          */
 8         REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
 9 
10         /**
11          * 如果当前存在事务就加入事务;
12          * 如果当前不存在事务就不使用事务
13          */
14         SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
15 
16         /**
17          * 如果当前存在事务就加入事务;
18          * 如果当前不存在事务则直接报错,也就是当前必须有事务
19          */
20         MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
21 
22         /**
23          * 不管当前是否存在事务,都创建一个新的事务,并且会将当前事务挂起,
24          * 直到新的事务执行成功,才会继续执行旧的事务
25          */
26         REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
27 
28         /**
29          * 不管当前是否存在事务,都不使用事务
30          */
31         NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
32 
33         /**
34          * 不可以在事务中执行,否则直接抛异常
35          */
36         NEVER(TransactionDefinition.PROPAGATION_NEVER),
37 
38         /**
39          * 如果当前存在事务,则在嵌套事务内执行;
40          * 如果当前不存在事务,在按PROPAGATION_REQUIRED操作
41          */
42         NESTED(TransactionDefinition.PROPAGATION_NESTED);
43     }

假设在方法A中调用方法B,那么各种传播机制的事务效果如下:

REQUIRED: 如果方法A有事务,那么方法B加入方法A的事务,两个方法一起提交或回滚;如果方法A没有事务,那么方法B自己创建新事务,和方法A独立,方法B无论提交和回滚都不会影响方法A的执行

SUPPORTS:如果方法A有事务,那么方法B加入方法A的事务,两个方法一起提交或回滚;如果方法A没有事务,那么方法B也以无事务执行

MANDATORY:如果方法A有事务,那么方法B加入方法A的事务,两个方法一起提交或回滚;如果方法A没有事务,那么方法B执行抛不合法事务状态异常,提示当前没有事务存在

REQUIRES_NEW:不管方法A是否有事务,方法B都会创建一个新的事务,方法A和方法B的事务互相独立,所以方法A回滚不会影响方法B,但是方法B回滚如果报错了可能会让方法A回滚,主要的看是否报错让方法A捕获到

NOT_SUPPORTED:不管方法A是否有事务,方法B都不会使用事务执行,所以不管方法A是提交还是回滚,方法B都会以非事务状态执行

NEVER:方法B不可以在事务中执行,如果方法A有事务,那么方法B执行会抛不合法事务状态异常,提示当前有事务存在

NESTZED:如果方法A存在事务,则在嵌套事务执行;如果方法A不存在事务,那么方法B按REQUIRED类型执行

三、Spring中事务的隔离机制

 Spring中事务隔离机制和mysql数据库的隔离机制基本上是一一对应的,Spring事务隔离机制实际也是通过数据库的隔离机制来实现的

在配置事务隔离机制时,需要通过配置事务的isolation属性来设置,如下示:

1 @Transactional(isolation = Isolation.DEFAULT)

mysql中事务隔离机制一共有四种,而Spring中事务隔离机制一共有五种,除了和数据库一样的四种之外还额外添加了一个Default类型。

DEFAUlT:默认隔离机制,相当于在Spring中不配置事务隔离机制,而采用数据库的默认事务隔离机制,数据库采用什么隔离机制Spring中就采用什么隔离机制

READ_UNCOMMITED:未提交读,最不安全事务隔离机制,会存在脏读、不可重复读和幻读等问题

READ_COMMITED:提交读,保证一个事务不会读取到另一个事务中还未提交的数据,解决了脏读问题,但是没有解决不可重复读和幻读的问题

REPEATABLE_READ:可重复读,保证一个事务不会出现脏读和不可重复读问题,但是还是不可以解决幻读问题

SERIALIZATION:串行化,解决了脏读、不可重复读和幻读问题,但是性能较差,各个事务串行化执行

四、Spring中事务的回滚机制

Spring中的事务的回滚,默认只会对unchecked exception进行回滚处理,异常的基类是Throwable,Error和Exception分别继承之Throwable,而Exception又分为RuntimeException和其他异常。所有继承之Exception的非RuntimeException都是checked Exception;

而Error和所有的RuntimeException及其子类的异常都是unchecked Exception。所以Spring事务默认只会对Error和RuntimeExeception进行捕获和回滚处理。如果抛的异常是Exception异常,那么不会进行回滚。

如果想对Checked Exception也进行捕获和回滚操作,那么可以通过配置来实现。比如@Transational(rollbackFor = {Exception.class}) 那么遇到Exception异常同样会进行回滚操作;

如果想对UnChecked Exception也不想进行回滚,同样可以通过配置来实现,比如@Transactional(noRollbackFor = {NullPointerException.class}),那么遇到空指针异常时,虽然是RuntimeException,但是同样不会回滚。

另外,如果配置了事务,并且抛了异常但是事务没有回滚的话,有以下几种情况都会导致事务不会回滚:

Tip:Spring事务不会回滚的情况

1、环境配置问题,比如没有配置开启注解的解析;扫描的包没有包含当前的包等情况(开发时配置错误)

2、数据库没有配置事务或不支持事务(需要保证数据库是支持事务的,否则配置了事务也没用)

3、事务修饰的方法不是public修饰的(只有public修饰的方法才会事务生效)

4、抛出的异常并非事务捕获的异常,默认只有RumtimeException才会被捕获使事务回滚

5、抛出的异常已经被try/catch捕获并且处理了,可以通过在try/catch中再throw new RuntimeException重新抛异常运行时异常来使事务回滚

五、Spring事务超时机制

在配置事务时可以配置事务的超时时间,比如@Transactional(timeout = 5),单位是秒,那么在整个事务中如果执行时间超过了操作时间,此时会进行判断剩余的代码中是否还含有数据库的操作。

如果方法执行超时,但是此时已经没有任何数据库操作,那么即使超时了也不会回滚而继续执行;

如果方法执行超时,而且此时还有没有待执行的数据库操作,那么此时事务回立即回滚,并且抛事务超时异常

六、Spring事务只读机制

在配置事务时可以设置readOnly属性,表示当前事务是否是只读模式。

配置可以根据不同事务配置方式选取以下配置方式:

<tx:method name="query*" read-only="true" /> 
1 @Transactional(readOnly = true)

 Spring本身对于readOnly并没有做额外的处理,而是在底层操作数据库时在设置和数据库的连接为只读模式,具体readOnly的效果如何,完全需要靠不同的数据库存储引擎来实现具体的语义。

所以并不是说配置来@Transaction(readOnly = true)之后,整个事务中就只可以执行读操作,同样也是可以执行写操作的。只是说设置了readOnly之后相当于给事务增加了一个标识,那么数据库看到这个事务标识时就可以对于事务进行相应的优化处理。

数据库看到事务标识了readOnly,那么可能就不会安排写锁了,因为默认会认为事务中的操作都是读操作。所以如果事务中都是读操作时才推荐使用readOnly=true。

那么如果事务中都是读操作的话,还需要添加事务么?答案是:yes

当使用Spring+mybatis来操作数据库时,我们都知道mybatis是有二级缓存中,其中一级缓存是针对于每次SQL会话的,也就是每个SqlSession会对应一个缓存。

而当在一个方法中如果执行两个相同的查询操作,如果没有事务的话,那么会创建两个SqlSession,所以就会执行两次数据库查询操作;而如果添加了事务之后,就会每个事务共享一个SqlSession,此时如果执行了相同的SQL,就会用到Mybatis的一级缓存,

从而不会执行一次SQL语句。如下示例:

1 @Transactional
2     public User getUserById(Long userId) {
3         User user = userMapper.getUserById(userId);
4         //TODO 执行其他业务操作
5         user = userMapper.getUserById(userId);
6         return user;
7     }

在方法中执行了两次UserMapper的getUserById,如果没有添加事务,那么就会执行两次数据库查询操作,而此时可能数据已经被修改,导致出现不可重复读现象;而如果添加了事务,就会使用mybatis缓存,只会执行一次数据库查询,从而解决前后不一致的问题。

本文主要简单介绍Spring中事务的相关理论和用法,下一篇主要对Spring中事务的具体实现原理进行分析,参见文章:Spring源码解析--事务的实现原理和源码解析

原文地址:https://www.cnblogs.com/jackion5/p/13419150.html