# Spring 事务管理

Spring


# 1. 事务的四个特性

  • 原子性 Atomicity

    事务包含的所有操作要么全部成功,要么全部失败回滚;成功必须要完全应用到数据库,失败则不能对数据库产生影响。

  • 一致性 Consistency

    事务执行前和执行后必须处于一致性状态。例如:转账事务执行前后,两账户余额的总和不变。

  • 隔离性 Isolation

    多个并发的事务之间要相互隔离。

  • 持久性 Durability

    事务一旦提交,对数据库的改变是永久性的.

# 2. 声明式事务管理

Spring 进行声明式事务管理,底层使用 AOP 原理。

# 2.1 重要的接口 PlatformTransactionManager

Spring 提供一个接口 PlatformTransactionManager,代表事务管理器,这个接口针对不同的框架提供不同的实现类:

image-20200916154339343

# 2.2 基于 XML 配置

# 2.2.1 引入事务管理命名空间 tx

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

# 2.2.2 配置事务管理器

<!--配置事务管理-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--注入数据源-->
    <property name="dataSource" ref="dataSource"></property>
</bean>

# 2.2.3 配置通知

<!--2 配置通知-->
<tx:advice id="txadvice">
    <!--配置事务参数-->
    <tx:attributes>
        <!--指定在符合哪种规则的方法上添加事务-->
        <!--这个表示在 transfer 方法上添加事务-->
        <tx:method name="transfer" propagation="REQUIRED"
                                    isolation="REPEATABLE_READ"
                                    timeout="-1"
                                    read-only="false"/>
        <!-- tx:method name="transfer*"></tx:method>-->
    </tx:attributes>
</tx:advice>

# 2.2.4 配置切入点和切面

<!--3 配置切入点和切面-->
<aop:config>
    <!--配置切入点-->
    <aop:pointcut id="pointcut" expression="execution(* com.hedon.service.AccountService.*(..))"/>
    <!--配置切面-->
    <aop:advisor advice-ref="txadvice" pointcut-ref="pointcut"></aop:advisor>
</aop:config>

# 2.2.5 开启事务注解

  • 执行前:

    image-20200916164119234
  • 执行后报错:

    image-20200916164152562
  • 数据库还是没有变,说明事务管理起到了作用:

    image-20200916164211343

# 2.3 基于注解

# 2.3.1 创建配置类 SpringConfig

  • @Configuration 配置类

  • @ComponentScan 组件扫描

  • @EnableTransactionManagement 开启事务支持

    这里相当于 xml 文件中的:

    <!--开启事务管理-->
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
    
@Configuration                                //配置类
@ComponentScan(basePackages = {"com.hedon"})  //组件扫描
@EnableTransactionManagement                  //开启事务支持
public class SpringConfig {

# 2.3.2 配置数据源 DruidDataSource

@Bean
public DruidDataSource druidDataSource(){
    DruidDataSource druidDataSource = new DruidDataSource();
    druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
    druidDataSource.setUrl("jdbc:mysql://localhost:3306/user_db");
    druidDataSource.setUsername("root");
    druidDataSource.setPassword("root");
    return druidDataSource;
}

# 2.3.3 配置 JdbcTemplate

/**
 * 配置 JdbcTemplate
 *
 * 参数 dataSource : 去 IoC 容器中默认根据类型注入 dataSource
 */
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
    JdbcTemplate jdbcTemplate = new JdbcTemplate();
    //注入数据源
    jdbcTemplate.setDataSource(dataSource);
    return jdbcTemplate;
}

# 2.3.4 配置事务管理器 DataSourceTransactionManager

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

# 2.3.5 添加事务注解 @Transactional

  • 添加到类上:这个类里面所有的方法都添加事务
  • 添加到方法上:为这个方法添加事务
@Service
@Transactional
public class AccountService {

# 2.3.6 测试

@Test
public void test1(){
    ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
    AccountService accountService = context.getBean("accountService", AccountService.class);
    accountService.transfer();
}

结果是一样没有问题的。

# 2.4 @Transactional 参数说明

@Service
@Transactional(propagation = Propagation.REQUIRED,
                isolation = Isolation.REPEATABLE_READ,
                timeout = 10,
                readOnly = false,
                rollbackFor = NullPointerException.class,
                noRollbackFor = FileNotFoundException.class)
public class AccountService {

# 2.4.1 propagation 事务传播行为

参考博客,讲得是真滴好! (opens new window)

指明当多事务方法直接进行调用时,这个过程中的事务是如何进行管理的。

如下:add() 方法有进行事务管理,而 update() 方法没有进行事务管理,一个有事务管理的方法调用一个没有事务管理的方法的时候,事务该如何传播呢?

image-20200916155045171 image-20200916155951782
# 2.4.1.1 REQUIRED
image-20200930193239783
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
   methodB();
   // do something
}
 
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
    // do something
}

单独调用methodB方法时,因为当前上下文不存在事务,所以会开启一个新的事务。

调用methodA方法时,因为当前上下文不存在事务,所以会开启一个新的事务。当执行到methodB时,methodB发现当前上下文有事务,因此就加入到当前事务中来。

# 2.4.1.2 REQUIRES_NEW
  • 使用 add() 方法调用 update() 方法,无论 add() 是否有事务,都创建新事务。

image-20201001002017105

使用 PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager 作为事务管理器。 它会开启一个新的事务。如果一个事务已经存在,则先将这个存在的事务挂起。

如下代码:

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
  doSomeThingA();
  methodB();
  doSomeThingB();
  // do something else
}


// 事务属性为REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
    // do something
}

当单独调用 methodA 时,即:

main{  
  methodA();
} 

相当于:

main(){
    TransactionManager tm = null;
    try{
        //获得一个JTA事务管理器
        tm = getTransactionManager();
        tm.begin();//开启一个新的事务
        Transaction ts1 = tm.getTransaction();
        doSomeThing();
        tm.suspend();//挂起当前事务
        try{
            tm.begin();//重新开启第二个事务
            Transaction ts2 = tm.getTransaction();
            methodB();
            ts2.commit();//提交第二个事务
        } Catch(RunTimeException ex) {
            ts2.rollback();//回滚第二个事务
        } finally {
            //释放资源
        }
        //methodB执行完后,恢复第一个事务
        tm.resume(ts1);
        doSomeThingB();
        ts1.commit();//提交第一个事务
    } catch(RunTimeException ex) {
        ts1.rollback();//回滚第一个事务
    } finally {
        //释放资源
    }
}

在这里,我把 ts1 称为外层事务,ts2称为内层事务。从上面的代码可以看出,ts2 与 ts1是两个独立的事务,互不相干。Ts2 是否成功并不依赖于 ts1。如果 methodA 方法在调用 methodB 方法后的 doSomeThingB 方法失败了,而 methodB 方法所做的结果依然被提交。而除了 methodB 之外的其它代码导致的结果却被回滚了

# 2.4.1.3 MANDATORY
  • 支持当前事务,如果当前没有事务,就抛出异常。
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
 methodB();
// do something
}
 
// 事务属性为MANDATORY
@Transactional(propagation = Propagation.MANDATORY)
public void methodB() {
    // do something
}

当单独调用 methodB 时,因为当前没有一个活动的事务,则会抛出异常 throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”);

当调用 methodA 时,methodB 则加入到 methodA 的事务中,事务地执行。

# 2.4.1.4 SUPPORTS
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
 methodB();
// do something
}
 
// 事务属性为SUPPORTS
@Transactional(propagation = Propagation.SUPPORTS)
public void methodB() {
    // do something
}

单纯的调用 methodB 时,methodB 方法是非事务的执行的。当调用 methdA 时,methodB 则加入了 methodA 的事务中,事务地执行。

# 2.4.1.5 NOT_SUPPORTED

image-20201001101403661

  • 使用 add() 方法调用 update() 方法,无论 add() 是否有事务,update() 都不进行事务管理。
  • PROPAGATION_NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务。使用 PROPAGATION_NOT_SUPPORTED,也需要使用JtaTransactionManager 作为事务管理器。
# 2.4.1.6 NEVER
  • 以非事务方式执行,如果当前存在事务,则抛出异常。
# 2.4.1.7 NESTED

image-20201001101438238

如果一个活动的事务存在,则运行在一个嵌套的事务中。 如果没有活动事务,则按 TransactionDefinition.PROPAGATION_REQUIRED 属性执行。

这是一个嵌套事务,使用 JDBC 3.0 驱动时,仅仅支持 DataSourceTransactionManager 作为事务管理器。需要 JDBC 驱动的java.sql.Savepoint 类。使用 PROPAGATION_NESTED,还需要把 PlatformTransactionManagernestedTransactionAllowed属性设为 true (属性值默认为false)。

这里的关键是嵌套事务,如下代码:

@Transactional(propagation = Propagation.REQUIRED)
methodA(){
  doSomeThingA();
  methodB();
  doSomeThingB();
}

@Transactional(propagation = Propagation.NEWSTED)
methodB(){
  ……
}

如果单独调用 methodB 方法,则按 REQUIRED 属性执行。如果调用 methodA 方法,相当于下面的效果:

main(){
    Connection con = null;
    Savepoint savepoint = null;
    try{
        con = getConnection();
        con.setAutoCommit(false);
        doSomeThingA();
        savepoint = con2.setSavepoint();
        try{
            methodB();
        } catch(RuntimeException ex) {
            con.rollback(savepoint);
        } finally {
            //释放资源
        }
        doSomeThingB();
        con.commit();
    } catch(RuntimeException ex) {
        con.rollback();
    } finally {
        //释放资源
    }
}

当 methodB 方法调用之前,调用 setSavepoint 方法,保存当前的状态到 savepoint。如果 methodB 方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码 (doSomeThingB()方法) 调用失败,则回滚包括 methodB 方法的所有操作。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。

# △ 比较PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别
# 相似点
都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。
# 不同点
① 使用 PROPAGATION_REQUIRES_NEW 时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。
② 使用PROPAGATION_NESTED 时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager 使用 savepoint 支持 PROPAGATION_NESTED 时,需要 JDBC 3.0以上驱动及 1.4 以上的JDK版本支持。其它的 JTATrasactionManager 实现可能有不同的支持方式。
③ PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围,自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。
④ 另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 嵌套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。
# 最大区别
PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back。

# 2.4.2 isolation 事务隔离级别

# 2.4.2.1 事务的隔离性

多事务操作之间不会产生影响。

# 2.4.2.2 三个读问题
  • 脏读

    一个未提交的事务读取到另一个未提交的事务的数据。

    image-20200916161119710

    如上:东方不败想从5000改到100,而岳不群想从5000改到60000。这个时候岳不群先改了,然后东方不败读取到数据已经改成60000了,所以东方不败就会继续在60000的基础上进行修改。但是这个时候,岳不群的事务并没有进行提交,而且进行了事务回滚,所以真实的数据现在还是5000,而东方不败操作的数据是60000。这就叫脏读。

  • 不可重复读

    一个未提交事务读取到另一提交事务修改数据。

    image-20200916161326921

    如上:东方不败先读取到数据是5000,想对数据进行操作,但是这个时候岳不群已经将数据改成900了。而东方不败又检测到了数据已经改成900了,读两次,数据不一致,这就是不可重复读(因为不知道再读的话是不是又会不一样了)。

  • 虚(幻)读

    一个未提交事务读取到另一提交事务增加的数据。

# 2.4.2.3 解决读问题 —— 设置事务的隔离性
isolation 属性值 意思 脏读 不可重复读 虚读 作用
READ UNCOMMITTED 读未提交 效率高,但是啥也避免不了
READ COMMITTED 读已提交 常用,可避免脏读
REPEATABLE READ 可重复读 可以用在非 insert 方法上
SERIALIZABLE 串行化 三个问题都解决了,但效率低
DEFAULT 使用数据库默认 MySQL 的话就是 REPEATABLE READ

# 2.4.3 timeout 超时时间

事务需要在一定时间内进行提交,如果不提交进行回滚。

默认时间是 -1(永不超时),设置时间以秒单位进行计算。

# 2.4.4 readOnly 是否只读

readOnly 默认值 false,表示可以查询,可以添加修改删除操作。

设置 readOnly 值是 true,设置成 true 之后,只能查询。

# 2.4.5 rollbackFor 回滚

设置出现哪些异常进行事务回滚。

# 2.4.6 noRollbackFor 不回滚

设置出现哪些异常不进行事务回滚。

上次更新: 8/29/2022, 12:17:25 AM