spring 第一期:@Transactional 下的事务管理以及该注解失效的常见原因

目录

1.回顾一下事务

2.几种实现事务的方式

3.@Transactional的使用

3.1.DataSourceTransactionManager 配置

3.2.开启事务管理

3.3.@Transactional

3.3.1.value、transactionManager属性

3.3.2.propagation属性

3.3.3.isolation属性

3.3.4.timeout属性

扫描二维码关注公众号,回复: 11452193 查看本文章

3.3.5.read-only属性

3.3.6. rollbackFor、rollbackForClassName、noRollbackFor、noRollbackForClassName

4.@Transactional注解使用须知

4.1.@Transactional注解可以作用于哪些地方?

4.2.@Transactional失效的场景

4.2.1@Transactional 应用在非 public 修饰的方法上

4.2.2.@Transactional 注解属性 propagation 设置错误

4.2.3.@Transactional 注解属性 rollbackFor 设置错误

4.2.4.异常被你的 catch“吃了”导致@Transactional失效

4.2.5.同一个类中方法调用,导致@Transactional失效

4.2.5.数据库引擎不支持事务管理


1.回顾一下事务

事务Transaction,它是一些严密操作的集合,要么都操作完成,要么都回滚撤销,事务具备ACID四种特性,ACID是Atomic(原子性)、Consistency(一致性)、Isolation(隔离性)和Durability(持久性)的英文缩写。 

  (1)原子性(Atomicity)
    事务最基本的操作单元,要么全部成功,要么全部失败,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
  (2)一致性(Consistency)
    事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。
  (3)隔离性(Isolation)
    指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
  (4)持久性(Durability)
    指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

使用 SQL 我们可以简单的演示这个过程

#事务
START TRANSACTION;#开始事务
UPDATE course
SET course_class_hour=100,
    course_credit=8
WHERE course_name = '程序语言设计';
UPDATE course
SET course_class_hour=98,
    course_credit=7
WHERE course_name = '离散数学';
COMMIT;#提交(只有提交后的数据,在数据库中才真正更行,否则回滚就前功尽弃)
ROLLBACK;#回滚

不过,这种方式显然不适合当下的高效率的开发模式,于是便有了Spring 对事务管理的支持

2.几种实现事务的方式

  • 编程式事务管理对基于 POJO 的应用来说是唯一选择。我们需要在代码中调用beginTransaction()、commit()、rollback()等事务管理相关的方法,这就是编程式事务管理
  • 基于 TransactionProxyFactoryBean的声明式事务管理
  • 基于 @Transactional 的声明式事务管理
  • 基于Aspectj AOP配置事务
  • 编程式事务:允许用户在代码中精确定义事务的边界。编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。

  • 声明式事务: 基于AOP,有助于用户将操作与事务规则进行解耦。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务管理也有两种常用的方式,一种是在配置文件(xml)中做相关的事务规则声明,另一种是基于@Transactional注解的方式。显然基于注解的方式更简单易用,更清爽。@Transactional注解的使用也是我们本文着重要理解的部分。

       显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。

3.@Transactional的使用

3.1.DataSourceTransactionManager 配置

顾名思义,这是数据源事务管理器,它需要一个DataSource作为参数,如下配置:

/**
     * 多数据源事务管理器配置
     *
     * @param dynamicDataSource 数据源
     * @return 事务管理器
     */
    @Bean
    public DataSourceTransactionManager dataSourceTransactionManager(DataSource dynamicDataSource) {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dynamicDataSource);
        return dataSourceTransactionManager;
    }

注意:在SpringBoot中,DataSourceTransactionManager 是默认配置好的,但当有多个数据源时(例如我这里使用的是动态数据源),则需要手动配置,指定数据源)

3.2.开启事务管理

我这里使用注解开启事务管理:

@Configuration
@EnableTransactionManagement
public class SkyConfig

3.3.@Transactional

配置好事务环境后,我们只需要在需要事务管理的方法上加上@Transactional注解即可,由于Dao 层的方法通常是一条语句执行的,本生就具有原子性,所以一般我们习惯在Service层上使用该注解

如下是一个简单的实例,由于这两个字段我设置了组合唯一索引,所以第二次插入操作会报错,于是整个方法执行事务回滚,结果就是:第一条(看似执行过了)也无效

    @Transactional
    public int batchInsert() {
        List<SysUserRoleLinkDO> list = new ArrayList<>();
        list.add(new SysUserRoleLinkDO(1L, 2L));
        list.add(new SysUserRoleLinkDO(2L, 7L));
        int i = sysUserRoleLinkDAO.batchInsert(list);
        List<SysUserRoleLinkDO> list2 = new ArrayList<>();
        list2.add(new SysUserRoleLinkDO(2L, 7L));
        list2.add(new SysUserRoleLinkDO(5L, 1L));
        int j = sysUserRoleLinkDAO.batchInsert(list2);
        return i + j;
    }

接下来我们来分析一下该注解的几个属性

3.3.1.value、transactionManager属性

它们两个是一样的意思。当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器。大多数项目只需要一个事务管理器。然而,有些项目为了提高效率、或者有多个完全不同又不相干的数据源,从而使用了多个事务管理器。

3.3.2.propagation属性

propagation用于指定事务的传播行为,这里先来说一下事务传播行为:当事务方法被另一个事务方法调用时(例如一个Service去调用另一个Service),必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring定义了七种传播行为:

propagation属性 事务属性-传播行为 含义
REQUIRED TransactionDefinition.PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务,则加入到这个事务中。这是最常见的选择。
SUPPORTS TransactionDefinition.PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
MANDATORY TransactionDefinition.PROPAGATION_MANDATORY 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常。
REQUIRES_NEW TransactionDefinition.PROPAGATION_REQUIRES_NEW 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。
NOT_SUPPORTED TransactionDefinition.PROPAGATION_NOT_SUPPORTED 表示该方法不应该运行在事务中。如果当前存在事务,就把当前事务挂起。
NEVER TransactionDefinition.PROPAGATION_NEVER 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常。
NESTED TransactionDefinition.PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

3.3.3.isolation属性

定义事务的隔离级别,前面说到事务具有隔离性,在实际开发过程中,我们绝大部分的事务都是有并发情况。下多个事务并发运行,经常会操作相同的数据来完成各自的任务。在这种情况下可能会导致以下的问题:

  • 脏读(Dirty reads)—— 事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。
  • 不可重复读(Nonrepeatable read)—— 事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
  • 幻读(Phantom read)—— 系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

Spring定义了5种隔离规则,如下所示:

@isolation属性 事务属性-隔离规则 含义 脏读 不可重复读 幻读
DEFAULT TransactionDefinition.ISOLATION_DEFAULT 使用后端数据库默认的隔离级别    
READ_UNCOMMITTED TransactionDefinition.ISOLATION_READ_UNCOMMITTED 允许读取尚未提交的数据变更(最低的隔离级别)
READ_COMMITTED TransactionDefinition.ISOLATION_READ_COMMITTED 允许读取并发事务已经提交的数据
REPEATABLE_READ TransactionDefinition.ISOLATION_REPEATABLE_READ 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改
SERIALIZABLE TransactionDefinition.ISOLATION_SERIALIZABLE 最高的隔离级别,完全服从ACID的隔离级别,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的

3.3.4.timeout属性

事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。

3.3.5.read-only属性

指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。

3.3.6. rollbackFor、rollbackForClassName、noRollbackFor、noRollbackForClassName

ollbackFor、rollbackForClassName用于设置那些异常需要回滚;noRollbackFor、noRollbackForClassName用于设置那些异常不需要回滚。他们就是在设置事务的回滚规则

(注:在阿里巴巴规范中,需要在Transactional注解指定rollbackFor或者在方法中显式的rollback)

4.@Transactional注解使用须知

4.1.@Transactional注解可以作用于哪些地方?

  • 作用于类:当把@Transactional 注解放在类上时,表示所有该类的public方法都配置相同的事务属性信息
  • 作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。
  • 作用于接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效

4.2.@Transactional失效的场景

此注解有很多失效的场景,但前提是,上面的基本环境你已经配置好了,再接着如下分析,希望对你有帮助

4.2.1@Transactional 应用在非 public 修饰的方法上

如果Transactional注解应用在非public修饰的方法上,Transactional将会失效。

之所以会失效是因为在Spring AOP 代理时,如上图所示 TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute

方法,获取Transactional 注解的事务配置信息。

此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。

注意:protected、private修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。

4.2.2.@Transactional 注解属性 propagation 设置错误

这种失效是由于配置错误,若是错误的配置以下三种 propagation,事务将不会发生回滚。

TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。

TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

4.2.3.@Transactional 注解属性 rollbackFor 设置错误

rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。

4.2.4.异常被你的 catch“吃了”导致@Transactional失效

这种情况是最常见的一种@Transactional注解失效场景,在A方法中调用了B方法,如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,这时候@Transacational就无效了

4.2.5.同一个类中方法调用,导致@Transactional失效

开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。

那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。

4.2.5.数据库引擎不支持事务管理

这种情况不太常见,MySQL是支持的

猜你喜欢

转载自blog.csdn.net/qq_42013035/article/details/106908688
今日推荐