关于java:长文捋明白-Spring-事务隔离性传播性一网打尽

8次阅读

共计 26440 个字符,预计需要花费 67 分钟才能阅读完成。

@[toc]
事务的重要性显而易见,Spring 对事务也提供了十分丰盛的反对,各种反对的属性包罗万象。

然而很多小伙伴晓得,这里有两个属性特地绕:

  • 隔离性
  • 流传性

有多绕呢?松哥都始终懒得写文章去总结。不过最近有小伙伴问到这个问题,刚好有空,就抽空总结一下,我不会水灵灵的和大家讲概念,接下来的所有内容,松哥都会通过具体的案例来和大家演示。

好啦,不废话啦,请看大屏幕。

1. 什么是事务

数据库事务是指作为单个逻辑工作单元执行的一系列操作,这些操作要么一起胜利,要么一起失败,是一个不可分割的工作单元。

在咱们日常工作中,波及到事务的场景十分多,一个 service 中往往须要调用不同的 dao 层办法,这些办法要么同时胜利要么同时失败,咱们须要在 service 层确保这一点。

说到事务最典型的案例就是转账了:

张三要给李四转账 500 块钱,这里波及到两个操作,从张三的账户上减去 500 块钱,给李四的账户增加 500 块钱,这两个操作要么同时胜利要么同时失败,如何确保他们同时胜利或者同时失败呢?答案就是事务。

事务有四大个性(ACID):

  • 原子性(Atomicity): 一个事务(transaction)中的所有操作,要么全副实现,要么全副不实现,不会完结在两头某个环节。事务在执行过程中产生谬误,会被回滚(Rollback)到事务开始前的状态,就像这个事务素来没有执行过一样。即,事务不可分割、不可约简。
  • 一致性(Consistency): 在事务开始之前和事务完结当前,数据库的完整性没有被毁坏。这示意写入的材料必须完全符合所有的预设束缚、触发器、级联回滚等。
  • 隔离性(Isolation): 数据库容许多个并发事务同时对其数据进行读写和批改,隔离性能够避免多个事务并发执行时因为穿插执行而导致数据的不统一。事务隔离分为不同级别,包含未提交读(Read Uncommitted)、提交读(Read Committed)、可反复读(Repeatable Read)和串行化(Serializable)。
  • 持久性(Durability): 事务处理完结后,对数据的批改就是永恒的,即使系统故障也不会失落。

这就是事务的四大个性。

2. Spring 中的事务

2.1 两种用法

Spring 作为 Java 开发中的基础设施,对于事务也提供了很好的反对,总体上来说,Spring 反对两种类型的事务,申明式事务和编程式事务。

编程式事务相似于 Jdbc 事务的写法,须要将事务的代码嵌入到业务逻辑中,这样代码的耦合度较高,而申明式事务通过 AOP 的思维可能无效的将事务和业务逻辑代码解耦,因而在理论开发中,申明式事务失去了宽泛的利用,而编程式事务则较少应用,思考到文章内容的残缺,本文对两种事务形式都会介绍。

2.2 三大基础设施

Spring 中对事务的反对提供了三大基础设施,咱们先来理解下。

  1. PlatformTransactionManager
  2. TransactionDefinition
  3. TransactionStatus

这三个外围类是 Spring 处理事务的外围类。

2.2.1 PlatformTransactionManager

PlatformTransactionManager 是事务处理的外围,它有诸多的实现类,如下:

PlatformTransactionManager 的定义如下:

public interface PlatformTransactionManager {TransactionStatus getTransaction(@Nullable TransactionDefinition definition);
    void commit(TransactionStatus status) throws TransactionException;
    void rollback(TransactionStatus status) throws TransactionException;
}

能够看到 PlatformTransactionManager 中定义了根本的事务操作方法,这些事务操作方法都是平台无关的,具体的实现都是由不同的子类来实现的。

这就像 JDBC 一样,SUN 公司制订规范,其余数据库厂商提供具体的实现。这么做的益处就是咱们 Java 程序员只须要把握好这套规范即可,不必去管接口的具体实现。以 PlatformTransactionManager 为例,它有泛滥实现,如果你应用的是 JDBC 那么能够将 DataSourceTransactionManager 作为事务管理器;如果你应用的是 Hibernate,那么能够将 HibernateTransactionManager 作为事务管理器;如果你应用的是 JPA,那么能够将 JpaTransactionManager 作为事务管理器。DataSourceTransactionManagerHibernateTransactionManager 以及 JpaTransactionManager 都是 PlatformTransactionManager 的具体实现,然而咱们并不需要把握这些具体实现类的用法,咱们只须要把握好 PlatformTransactionManager 的用法即可。

PlatformTransactionManager 中次要有如下三个办法:

1.getTransaction()

getTransaction() 是依据传入的 TransactionDefinition 获取一个事务对象,TransactionDefinition 中定义了一些事务的根本规定,例如流传性、隔离级别等。

2.commit()

commit() 办法用来提交事务。

3.rollback()

rollback() 办法用来回滚事务。

2.2.2 TransactionDefinition

TransactionDefinition 用来形容事务的具体规定,也称作事务的属性。事务有哪些属性呢?看下图:

能够看到,次要是五种属性:

  1. 隔离性
  2. 流传性
  3. 回滚规定
  4. 超时工夫
  5. 是否只读

这五种属性接下来松哥会和大家具体介绍。

TransactionDefinition 类中的办法如下:

能够看到一共有五个办法:

  1. getIsolationLevel(),获取事务的隔离级别
  2. getName(),获取事务的名称
  3. getPropagationBehavior(),获取事务的流传性
  4. getTimeout(),获取事务的超时工夫
  5. isReadOnly(),获取事务是否是只读事务

TransactionDefinition 也有诸多的实现类,如下:

如果开发者应用了编程式事务的话,间接应用 DefaultTransactionDefinition 即可。

2.2.3 TransactionStatus

TransactionStatus 能够间接了解为事务自身,该接口源码如下:

public interface TransactionStatus extends SavepointManager, Flushable {boolean isNewTransaction();
    boolean hasSavepoint();
    void setRollbackOnly();
    boolean isRollbackOnly();
    void flush();
    boolean isCompleted();}
  1. isNewTransaction() 办法获取以后事务是否是一个新事务。
  2. hasSavepoint() 办法判断是否存在 savePoint()。
  3. setRollbackOnly() 办法设置事务必须回滚。
  4. isRollbackOnly() 办法获取事务只能回滚。
  5. flush() 办法将底层会话中的批改刷新到数据库,个别用于 Hibernate/JPA 的会话,对如 JDBC 类型的事务无任何影响。
  6. isCompleted() 办法用来获取是一个事务是否完结。

这就是 Spring 中反对事务的三大基础设施。

3. 编程式事务

咱们先来看看编程式事务怎么玩。

通过 PlatformTransactionManager 或者 TransactionTemplate 能够实现编程式事务。如果是在 Spring Boot 我的项目中,这两个对象 Spring Boot 会主动提供,咱们间接应用即可。然而如果是在传统的 SSM 我的项目中,则须要咱们通过配置来提供这两个对象,松哥给一个简略的配置参考,如下(简略起见,数据库操作咱们应用 JdbcTemplate):

<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql:///spring_tran?serverTimezone=Asia/Shanghai"/>
    <property name="username" value="root"/>
    <property name="password" value="123"/>
</bean>
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<bean class="org.springframework.transaction.support.TransactionTemplate" id="transactionTemplate">
    <property name="transactionManager" ref="transactionManager"/>
</bean>
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
</bean>

有了这两个对象,接下来的代码就简略了:

@Service
public class TransferService {
    @Autowired
    JdbcTemplate jdbcTemplate;
    @Autowired
    PlatformTransactionManager txManager;

    public void transfer() {DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        TransactionStatus status = txManager.getTransaction(definition);
        try {jdbcTemplate.update("update user set account=account+100 where username='zhangsan'");
            int i = 1 / 0;
            jdbcTemplate.update("update user set account=account-100 where username='lisi'");
            txManager.commit(status);
        } catch (DataAccessException e) {e.printStackTrace();
            txManager.rollback(status);
        }
    }
}

这段代码很简略,没啥好解释的,在 try...catch... 中进行业务操作,没问题就 commit,有问题就 rollback。如果咱们须要配置事务的隔离性、流传性等,能够在 DefaultTransactionDefinition 对象中进行配置。

下面的代码是通过 PlatformTransactionManager 实现的编程式事务,咱们也能够通过 TransactionTemplate 来实现编程式事务,如下:

@Service
public class TransferService {
    @Autowired
    JdbcTemplate jdbcTemplate;
    @Autowired
    TransactionTemplate tranTemplate;
    public void transfer() {tranTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                try {jdbcTemplate.update("update user set account=account+100 where username='zhangsan'");
                    int i = 1 / 0;
                    jdbcTemplate.update("update user set account=account-100 where username='lisi'");
                } catch (DataAccessException e) {status.setRollbackOnly();
                    e.printStackTrace();}
            }
        });
    }
}

间接注入 TransactionTemplate,而后在 execute 办法中增加回调写外围的业务即可,当抛出异样时,将以后事务标注为只能回滚即可。留神,execute 办法中,如果不须要获取事务执行的后果,则间接应用 TransactionCallbackWithoutResult 类即可,如果要获取事务执行后果,则应用 TransactionCallback 即可。

这就是两种编程式事务的玩法。

编程式事务因为代码入侵太重大了,因为在理论开发中应用的很少,咱们在我的项目中更多的是应用申明式事务。

4. 申明式事务

申明式事务如果应用 XML 配置,能够做到无侵入;如果应用 Java 配置,也只有一个 @Transactional 注解侵入而已,相对来说非常容易。

以下配置针对传统 SSM 我的项目(因为在 Spring Boot 我的项目中,事务相干的组件曾经配置好了):

4.1 XML 配置

XML 配置申明式事务大抵上能够分为三个步骤,如下:

  1. 配置事务管理器
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql:///spring_tran?serverTimezone=Asia/Shanghai"/>
    <property name="username" value="root"/>
    <property name="password" value="123"/>
</bean>
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
  1. 配置事务告诉
<tx:advice transaction-manager="transactionManager" id="txAdvice">
    <tx:attributes>
        <tx:method name="m3"/>
        <tx:method name="m4"/>
    </tx:attributes>
</tx:advice>
  1. 配置 AOP
<aop:config>
    <aop:pointcut id="pc1" expression="execution(* org.javaboy.demo.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="pc1"/>
</aop:config>

第二步和第三步中定义进去的办法交加,就是咱们要增加事务的办法。

配置实现后,如下一些办法就主动具备事务了:

public class UserService {public void m3(){jdbcTemplate.update("update user set money=997 where username=?", "zhangsan");
    }
}

4.2 Java 配置

咱们也能够应用 Java 配置来实现申明式事务:

@Configuration
@ComponentScan
// 开启事务注解反对
@EnableTransactionManagement
public class JavaConfig {
    @Bean
    DataSource dataSource() {DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setPassword("123");
        ds.setUsername("root");
        ds.setUrl("jdbc:mysql:///test01?serverTimezone=Asia/Shanghai");
        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return ds;
    }

    @Bean
    JdbcTemplate jdbcTemplate(DataSource dataSource) {return new JdbcTemplate(dataSource);
    }

    @Bean
    PlatformTransactionManager transactionManager() {return new DataSourceTransactionManager(dataSource());
    }
}

这里要配置的货色其实和 XML 中配置的都差不多,最最要害的就两个:

  • 事务管理器 PlatformTransactionManager。
  • @EnableTransactionManagement 注解开启事务反对。

配置实现后,接下来,哪个办法须要事务就在哪个办法上增加 @Transactional 注解即可,向上面这样:

@Transactional(noRollbackFor = ArithmeticException.class)
public void update4() {jdbcTemplate.update("update account set money = ? where username=?;", 998, "lisi");
    int i = 1 / 0;
}

当然这个略微有点代码入侵,不过问题不大,日常开发中这种形式应用较多。当@Transactional 注解加在类下面的时候,示意该类的所有办法都有事务,该注解加在办法下面的时候,示意该办法有事务。

4.3 混合配置

也能够 Java 代码和 XML 混合配置来实现申明式事务,就是一部分配置用 XML 来实现,一部分配置用 Java 代码来实现:

假如 XML 配置如下:

<?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: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/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--
    开启事务的注解配置,增加了这个配置,就能够间接在代码中通过 @Transactional 注解来开启事务了
    -->
    <tx:annotation-driven />

</beans>

那么 Java 代码中的配置如下:

@Configuration
@ComponentScan
@ImportResource(locations = "classpath:applicationContext3.xml")
public class JavaConfig {
    @Bean
    DataSource dataSource() {DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setPassword("123");
        ds.setUsername("root");
        ds.setUrl("jdbc:mysql:///test01?serverTimezone=Asia/Shanghai");
        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return ds;
    }

    @Bean
    JdbcTemplate jdbcTemplate(DataSource dataSource) {return new JdbcTemplate(dataSource);
    }

    @Bean
    PlatformTransactionManager transactionManager() {return new DataSourceTransactionManager(dataSource());
    }
}

Java 配置中通过 @ImportResource 注解导入了 XML 配置,XML 配置中的内容就是开启 @Transactional 注解的反对,所以 Java 配置中省略了 @EnableTransactionManagement 注解。

这就是申明式事务的几种配置形式。好玩吧!

5. 事务属性

在后面的配置中,咱们只是简略说了事务的用法,并没有和大家具体聊一聊事务的一些属性细节,那么接下来咱们就来认真捋一捋事务中的五大属性。

5.1 隔离性

首先就是事务的隔离性,也就是事务的隔离级别。

MySQL 中有四种不同的隔离级别,这四种不同的隔离级别在 Spring 中都失去了很好的反对。Spring 中默认的事务隔离级别是 default,即数据库自身的隔离级别是啥就是啥,default 就能满足咱们日常开发中的大部分场景。

不过如果我的项目有须要,咱们也能够调整事务的隔离级别。

调整形式如下:

5.1.1 编程式事务隔离级别

如果是编程式事务,通过如下形式批改事务的隔离级别:

TransactionTemplate

transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);

TransactionDefinition 中定义了各种隔离级别。

PlatformTransactionManager

public void update2() {
    // 创立事务的默认配置
    DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
    definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
    TransactionStatus status = platformTransactionManager.getTransaction(definition);
    try {jdbcTemplate.update("update account set money = ? where username=?;", 999, "zhangsan");
        int i = 1 / 0;
        // 提交事务
        platformTransactionManager.commit(status);
    } catch (DataAccessException e) {e.printStackTrace();
        // 回滚
        platformTransactionManager.rollback(status);
    }
}

这里是在 DefaultTransactionDefinition 对象中设置事务的隔离级别。

5.1.2 申明式事务隔离级别

如果是申明式事务通过如下形式批改隔离级别:

XML:

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!-- 以 add 开始的办法,增加事务 -->
        <tx:method name="add*"/>
        <tx:method name="insert*" isolation="SERIALIZABLE"/>
    </tx:attributes>
</tx:advice>

Java:

@Transactional(isolation = Isolation.SERIALIZABLE)
public void update4() {jdbcTemplate.update("update account set money = ? where username=?;", 998, "lisi");
    int i = 1 / 0;
}

对于事务的隔离级别,如果大家还不相熟,能够参考松哥之前的文章:四个案例看懂 MySQL 事务隔离级别。

5.2 流传性

先来说说何谓事务的流传性:

事务流传行为是为了解决业务层办法之间相互调用的事务问题,当一个事务办法被另一个事务办法调用时,事务该以何种状态存在?例如新办法可能持续在现有事务中运行,也可能开启一个新事务,并在本人的事务中运行,等等,这些规定就波及到事务的流传性。

对于事务的流传性,Spring 次要定义了如下几种:

public enum 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);
    private final int value;
    Propagation(int value) {this.value = value;}
    public int value() { return this.value;}
}

具体含意如下:

流传性 形容
REQUIRED 如果以后存在事务,则退出该事务;如果以后没有事务,则创立一个新的事务
SUPPORTS 如果以后存在事务,则退出该事务;如果以后没有事务,则以非事务的形式持续运行
MANDATORY 如果以后存在事务,则退出该事务;如果以后没有事务,则抛出异样
REQUIRES_NEW 创立一个新的事务,如果以后存在事务,则把以后事务挂起
NOT_SUPPORTED 以非事务形式运行,如果以后存在事务,则把以后事务挂起
NEVER 以非事务形式运行,如果以后存在事务,则抛出异样
NESTED 如果以后存在事务,则创立一个事务作为以后事务的嵌套事务来运行;如果以后没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED

一共是七种流传性,具体配置也简略:

TransactionTemplate 中的配置

transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

PlatformTransactionManager 中的配置

public void update2() {
    // 创立事务的默认配置
    DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
    definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
    definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    TransactionStatus status = platformTransactionManager.getTransaction(definition);
    try {jdbcTemplate.update("update account set money = ? where username=?;", 999, "zhangsan");
        int i = 1 / 0;
        // 提交事务
        platformTransactionManager.commit(status);
    } catch (DataAccessException e) {e.printStackTrace();
        // 回滚
        platformTransactionManager.rollback(status);
    }
}

申明式事务的配置(XML)

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!-- 以 add 开始的办法,增加事务 -->
        <tx:method name="add*"/>
        <tx:method name="insert*" isolation="SERIALIZABLE" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

申明式事务的配置(Java)

@Transactional(noRollbackFor = ArithmeticException.class,propagation = Propagation.REQUIRED)
public void update4() {jdbcTemplate.update("update account set money = ? where username=?;", 998, "lisi");
    int i = 1 / 0;
}

用就是这么来用,至于七种流传的具体含意,松哥来和大家一个一个说。

5.2.1 REQUIRED

REQUIRED 示意如果以后存在事务,则退出该事务;如果以后没有事务,则创立一个新的事务。

例如我有如下一段代码:

@Service
public class AccountService {
    @Autowired
    JdbcTemplate jdbcTemplate;
    @Transactional
    public void handle1() {jdbcTemplate.update("update user set money = ? where id=?;", 1, 2);
    }
}
@Service
public class AccountService2 {
    @Autowired
    JdbcTemplate jdbcTemplate;
    @Autowired
    AccountService accountService;
    public void handle2() {jdbcTemplate.update("update user set money = ? where username=?;", 1, "zhangsan");
        accountService.handle1();}
}

我在 handle2 办法中调用 handle1。

那么:

  1. 如果 handle2 办法自身是有事务的,则 handle1 办法就会退出到 handle2 办法所在的事务中,这样两个办法将处于同一个事务中,一起胜利或者一起失败(不论是 handle2 还是 handle1 谁抛异样,都会导致整体回滚)。
  2. 如果 handle2 办法自身是没有事务的,则 handle1 办法就会本人开启一个新的事务,本人玩。

举一个简略的例子:handle2 办法有事务,handle1 办法也有事务(小伙伴们依据后面的解说自行配置事务),我的项目打印进去的事务日志如下:

o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [org.javaboy.spring_tran02.AccountService2.handle2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [HikariProxyConnection@875256468 wrapping com.mysql.cj.jdbc.ConnectionImpl@9753d50] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [HikariProxyConnection@875256468 wrapping com.mysql.cj.jdbc.ConnectionImpl@9753d50] to manual commit
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [update user set money = ? where username=?;]
o.s.jdbc.support.JdbcTransactionManager  : Participating in existing transaction
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [update user set money = ? where id=?;]
o.s.jdbc.support.JdbcTransactionManager  : Initiating transaction commit
o.s.jdbc.support.JdbcTransactionManager  : Committing JDBC transaction on Connection [HikariProxyConnection@875256468 wrapping com.mysql.cj.jdbc.ConnectionImpl@9753d50]
o.s.jdbc.support.JdbcTransactionManager  : Releasing JDBC Connection [HikariProxyConnection@875256468 wrapping com.mysql.cj.jdbc.ConnectionImpl@9753d50] after transaction

从日志中能够看到,前前后后一共就开启了一个事务,日志中有这么一句:

Participating in existing transaction

这个就阐明 handle1 办法没有本人开启事务,而是退出到 handle2 办法的事务中了。

5.2.2 REQUIRES_NEW

REQUIRES_NEW 示意创立一个新的事务,如果以后存在事务,则把 以后事务挂起。换言之,不论内部办法是否有事务,REQUIRES_NEW 都会开启本人的事务。

这块松哥要多说两句,有的小伙伴可能感觉 REQUIRES_NEW 和 REQUIRED 太像了,仿佛没啥区别。其实你要是单纯看最终回滚成果,可能的确看不到啥区别。然而,大家留神松哥下面的加粗,在 REQUIRES_NEW 中可能会同时存在两个事务,内部办法的事务被挂起,外部办法的事务单独运行,而在 REQUIRED 中则不会呈现这种状况,如果内外部办法流传性都是 REQUIRED,那么最终也只是一个事务。

还是下面那个例子,假如 handle1 和 handle2 办法都有事务,handle2 办法的事务流传性是 REQUIRED,而 handle1 办法的事务流传性是 REQUIRES_NEW,那么最终打印进去的事务日志如下:

o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [org.javaboy.spring_tran02.AccountService2.handle2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [HikariProxyConnection@422278016 wrapping com.mysql.cj.jdbc.ConnectionImpl@732405c2] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [HikariProxyConnection@422278016 wrapping com.mysql.cj.jdbc.ConnectionImpl@732405c2] to manual commit
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [update user set money = ? where username=?;]
o.s.jdbc.support.JdbcTransactionManager  : Suspending current transaction, creating new transaction with name [org.javaboy.spring_tran02.AccountService.handle1]
o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [HikariProxyConnection@247691344 wrapping com.mysql.cj.jdbc.ConnectionImpl@14ad4b95] for JDBC transaction
com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@14ad4b95
o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [HikariProxyConnection@247691344 wrapping com.mysql.cj.jdbc.ConnectionImpl@14ad4b95] to manual commit
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [update user set money = ? where id=?;]
o.s.jdbc.support.JdbcTransactionManager  : Initiating transaction commit
o.s.jdbc.support.JdbcTransactionManager  : Committing JDBC transaction on Connection [HikariProxyConnection@247691344 wrapping com.mysql.cj.jdbc.ConnectionImpl@14ad4b95]
o.s.jdbc.support.JdbcTransactionManager  : Releasing JDBC Connection [HikariProxyConnection@247691344 wrapping com.mysql.cj.jdbc.ConnectionImpl@14ad4b95] after transaction
o.s.jdbc.support.JdbcTransactionManager  : Resuming suspended transaction after completion of inner transaction
o.s.jdbc.support.JdbcTransactionManager  : Initiating transaction commit
o.s.jdbc.support.JdbcTransactionManager  : Committing JDBC transaction on Connection [HikariProxyConnection@422278016 wrapping com.mysql.cj.jdbc.ConnectionImpl@732405c2]
o.s.jdbc.support.JdbcTransactionManager  : Releasing JDBC Connection [HikariProxyConnection@422278016 wrapping com.mysql.cj.jdbc.ConnectionImpl@732405c2] after transaction

剖析这段日志咱们能够看到:

  1. 首先为 handle2 办法开启了一个事务。
  2. 执行完 handle2 办法的 SQL 之后,事务被刮起(Suspending)。
  3. 为 handle1 办法开启了一个新的事务。
  4. 执行 handle1 办法的 SQL。
  5. 提交 handle1 办法的事务。
  6. 复原被挂起的事务(Resuming)。
  7. 提交 handle2 办法的事务。

从这段日志中大家能够十分明确的看到 REQUIRES_NEW 和 REQUIRED 的区别。

松哥再来简略总结下(假如 handle1 办法的事务流传性是 REQUIRES_NEW):

  1. 如果 handle2 办法没有事务,handle1 办法本人开启一个事务本人玩。
  2. 如果 handle2 办法有事务,handle1 办法还是会开启一个事务。此时,如果 handle2 产生了异样进行回滚,并不会导致 handle1 办法回滚,因为 handle1 办法是独立的事务;如果 handle1 办法产生了异样导致回滚,并且 handle1 办法的异样没有被捕捉解决传到了 handle2 办法中,那么也会导致 handle2 办法回滚。

这个中央小伙伴们要略微留神一下,咱们测试的时候,因为是两个更新 SQL,如果更新的查问字段不是索引字段,那么 InnoDB 将应用表锁,这样就会产生死锁(handle2 办法执行时开启表锁,导致 handle1 办法陷入期待中,而必须 handle1 办法执行完,handle2 能力开释锁)。所以,在下面的测试中,咱们要将 username 字段设置为索引字段,这样默认就应用行锁了。

5.2.3 NESTED

NESTED 示意如果以后存在事务,则创立一个事务作为以后事务的嵌套事务来运行;如果以后没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。

假如 handle2 办法有事务,handle1 办法也有事务且流传性为 NESTED,那么最终执行的事务日志如下:

o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [org.javaboy.demo.AccountService2.handle2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [HikariProxyConnection@2025689131 wrapping com.mysql.cj.jdbc.ConnectionImpl@2ed3628e] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [HikariProxyConnection@2025689131 wrapping com.mysql.cj.jdbc.ConnectionImpl@2ed3628e] to manual commit
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [update user set money = ? where username=?;]
o.s.jdbc.support.JdbcTransactionManager  : Creating nested transaction with name [org.javaboy.demo.AccountService.handle1]
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [update user set money = ? where id=?;]
o.s.jdbc.support.JdbcTransactionManager  : Releasing transaction savepoint
o.s.jdbc.support.JdbcTransactionManager  : Initiating transaction commit
o.s.jdbc.support.JdbcTransactionManager  : Committing JDBC transaction on Connection [HikariProxyConnection@2025689131 wrapping com.mysql.cj.jdbc.ConnectionImpl@2ed3628e]
o.s.jdbc.support.JdbcTransactionManager  : Releasing JDBC Connection [HikariProxyConnection@2025689131 wrapping com.mysql.cj.jdbc.ConnectionImpl@2ed3628e] after transaction

要害一句在 Creating nested transaction

此时,NESTED 润饰的外部办法(handle1)属于内部事务的子事务,内部主事务回滚的话,子事务也会回滚,而外部子事务能够独自回滚而不影响内部主事务和其余子事务(须要解决掉外部子事务的异样)。

5.2.4 MANDATORY

MANDATORY 示意如果以后存在事务,则退出该事务;如果以后没有事务,则抛出异样。

这个好了解,我举两个例子:

假如 handle2 办法有事务,handle1 办法也有事务且流传性为 MANDATORY,那么最终执行的事务日志如下:

o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [org.javaboy.demo.AccountService2.handle2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [HikariProxyConnection@768820610 wrapping com.mysql.cj.jdbc.ConnectionImpl@14840df2] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [HikariProxyConnection@768820610 wrapping com.mysql.cj.jdbc.ConnectionImpl@14840df2] to manual commit
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [update user set money = ? where username=?;]
o.s.jdbc.support.JdbcTransactionManager  : Participating in existing transaction
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [update user set money = ? where id=?;]
o.s.jdbc.support.JdbcTransactionManager  : Initiating transaction commit
o.s.jdbc.support.JdbcTransactionManager  : Committing JDBC transaction on Connection [HikariProxyConnection@768820610 wrapping com.mysql.cj.jdbc.ConnectionImpl@14840df2]
o.s.jdbc.support.JdbcTransactionManager  : Releasing JDBC Connection [HikariProxyConnection@768820610 wrapping com.mysql.cj.jdbc.ConnectionImpl@14840df2] after transaction

从这段日志能够看出:

  1. 首先给 handle2 办法开启事务。
  2. 执行 handle2 办法的 SQL。
  3. handle1 办法退出到曾经存在的事务中。
  4. 执行 handle1 办法的 SQL。
  5. 提交事务。

假如 handle2 办法无事务,handle1 办法有事务且流传性为 MANDATORY,那么最终执行时会抛出如下异样:

No existing transaction found for transaction marked with propagation 'mandatory'

因为没有曾经存在的事务,所以出错了。

5.2.5 SUPPORTS

SUPPORTS 示意如果以后存在事务,则退出该事务;如果以后没有事务,则以非事务的形式持续运行。

这个也简略,举两个例子大家就明确了。

假如 handle2 办法有事务,handle1 办法也有事务且流传性为 SUPPORTS,那么最终事务执行日志如下:

o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [org.javaboy.demo.AccountService2.handle2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [HikariProxyConnection@1780573324 wrapping com.mysql.cj.jdbc.ConnectionImpl@44eafcbc] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [HikariProxyConnection@1780573324 wrapping com.mysql.cj.jdbc.ConnectionImpl@44eafcbc] to manual commit
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [update user set money = ? where username=?;]
o.s.jdbc.support.JdbcTransactionManager  : Participating in existing transaction
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [update user set money = ? where id=?;]
o.s.jdbc.support.JdbcTransactionManager  : Initiating transaction commit
o.s.jdbc.support.JdbcTransactionManager  : Committing JDBC transaction on Connection [HikariProxyConnection@1780573324 wrapping com.mysql.cj.jdbc.ConnectionImpl@44eafcbc]
o.s.jdbc.support.JdbcTransactionManager  : Releasing JDBC Connection [HikariProxyConnection@1780573324 wrapping com.mysql.cj.jdbc.ConnectionImpl@44eafcbc] after transaction

这段日志很简略,没啥好说的,认准 Participating in existing transaction 示意退出到曾经存在的事务中即可。

假如 handle2 办法无事务,handle1 办法有事务且流传性为 SUPPORTS,这个最终就不会开启事务了,也没有相干日志。

5.2.6 NOT_SUPPORTED

NOT_SUPPORTED 示意以非事务形式运行,如果以后存在事务,则把以后事务挂起。

假如 handle2 办法有事务,handle1 办法也有事务且流传性为 NOT_SUPPORTED,那么最终事务执行日志如下:

o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [org.javaboy.demo.AccountService2.handle2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [HikariProxyConnection@1365886554 wrapping com.mysql.cj.jdbc.ConnectionImpl@3198938b] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [HikariProxyConnection@1365886554 wrapping com.mysql.cj.jdbc.ConnectionImpl@3198938b] to manual commit
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [update user set money = ? where username=?;]
o.s.jdbc.support.JdbcTransactionManager  : Suspending current transaction
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL update
o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [update user set money = ? where id=?;]
o.s.jdbc.datasource.DataSourceUtils      : Fetching JDBC Connection from DataSource
o.s.jdbc.support.JdbcTransactionManager  : Resuming suspended transaction after completion of inner transaction
o.s.jdbc.support.JdbcTransactionManager  : Initiating transaction commit
o.s.jdbc.support.JdbcTransactionManager  : Committing JDBC transaction on Connection [HikariProxyConnection@1365886554 wrapping com.mysql.cj.jdbc.ConnectionImpl@3198938b]
o.s.jdbc.support.JdbcTransactionManager  : Releasing JDBC Connection [HikariProxyConnection@1365886554 wrapping com.mysql.cj.jdbc.ConnectionImpl@3198938b] after transaction

这段日志大家认准这两句就行了:Suspending current transaction 示意挂起以后事务;Resuming suspended transaction 示意复原挂起的事务。

5.2.7 NEVER

NEVER 示意以非事务形式运行,如果以后存在事务,则抛出异样。

假如 handle2 办法有事务,handle1 办法也有事务且流传性为 NEVER,那么最终会抛出如下异样:

Existing transaction found for transaction marked with propagation 'never'

5.3 回滚规定

默认状况下,事务只有遇到运行期异样(RuntimeException 的子类)以及 Error 时才会回滚,在遇到查看型(Checked Exception)异样时不会回滚。

像 1/0,空指针这些是 RuntimeException,而 IOException 则算是 Checked Exception,换言之,默认状况下,如果产生 IOException 并不会导致事务回滚。

如果咱们心愿产生 IOException 时也能触发事务回滚,那么能够依照如下形式配置:

Java 配置:

@Transactional(rollbackFor = IOException.class)
public void handle2() {jdbcTemplate.update("update user set money = ? where username=?;", 1, "zhangsan");
    accountService.handle1();}

XML 配置:

<tx:advice transaction-manager="transactionManager" id="txAdvice">
    <tx:attributes>
        <tx:method name="m3" rollback-for="java.io.IOException"/>
    </tx:attributes>
</tx:advice>

另外,咱们也能够指定在产生某些异样时不回滚,例如当零碎抛出 ArithmeticException 异样并不要触发事务回滚,配置形式如下:

Java 配置:

@Transactional(noRollbackFor = ArithmeticException.class)
public void handle2() {jdbcTemplate.update("update user set money = ? where username=?;", 1, "zhangsan");
    accountService.handle1();}

XML 配置:

<tx:advice transaction-manager="transactionManager" id="txAdvice">
    <tx:attributes>
        <tx:method name="m3" no-rollback-for="java.lang.ArithmeticException"/>
    </tx:attributes>
</tx:advice>

5.4 是否只读

只读事务个别设置在查询方法上,但不是所有的查询方法都须要只读事务,要看具体情况。

一般来说,如果这个业务办法只有一个查问 SQL,那么就没必要增加事务,强行增加最终成果事与愿违。

然而如果一个业务办法中有多个查问 SQL,状况就不一样了:多个查问 SQL,默认状况下,每个查问 SQL 都会开启一个独立的事务,这样,如果有并发操作批改了数据,那么多个查问 SQL 就会查到不一样的数据。此时,如果咱们开启事务,并设置为只读事务,那么多个查问 SQL 将被置于同一个事务中,多条雷同的 SQL 在该事务中执行将会获取到雷同的查问后果。

设置事务只读的形式如下:

Java 配置:

@Transactional(readOnly = true)

XML 配置:

<tx:advice transaction-manager="transactionManager" id="txAdvice">
    <tx:attributes>
        <tx:method name="m3" read-only="true"/>
    </tx:attributes>
</tx:advice>

5.5 超时工夫

超时工夫是说一个事务容许执行的最长工夫,如果超过该工夫限度但事务还没有实现,则主动回滚事务。

事务超时工夫配置形式如下(单位为秒):

Java 配置:

@Transactional(timeout = 10)

XML 配置:

<tx:advice transaction-manager="transactionManager" id="txAdvice">
    <tx:attributes>
        <tx:method name="m3" read-only="true" timeout="10"/>
    </tx:attributes>
</tx:advice>

TransactionDefinition 中以 int 的值来示意超时工夫,其单位是秒,默认值为 -1。

6. 注意事项

  1. 事务只能利用到 public 办法上才会无效。
  2. 事务须要从内部调用,Spring 自调事务用会生效。即雷同类里边,A 办法没有事务,B 办法有事务,A 办法调用 B 办法,则 B 办法的事务会生效,这点尤其要留神,因为代理模式只拦挡通过代理传入的内部办法调用,所以自调用事务是不失效的。
  3. 倡议事务注解 @Transactional 个别增加在实现类上,而不要定义在接口上,如果加在接口类或接口办法上时,只有配置基于接口的代理这个注解才会失效。

7. 小结

好啦,这就是松哥和大家分享的 Spring 事务的玩法,不晓得小伙伴们搞明确没有?配套视频曾经录好,正在上传,敬请期待~

正文完
 0