[TOC]

Spring 事务失效的六种情况

最近有小伙伴告知松哥说面试中被问到这个问题了,不知道该怎样回答,这能忍?捋一篇文章和小伙伴们共享下吧。

已然捋成文章,就连同 Spring 业务一同梳理下吧。

1. 什么是业务

数据库业务是指作为单个逻辑工作单元履行的一系列操作,这些操作要么一同成功,要么一同失利,是一个不行分割的工作单元。

在咱们日常工作中,涉及到业务的场景非常多,一个 service 中往往需求调用不同的 dao 层办法,这些办法要么一同成功要么一同失利,咱们需求在 service 层保证这一点。

说到业务最典型的案例便是转账了:

张三要给李四转账 500 块钱,这儿涉及到两个操作,从张三的账户上减去 500 块钱,给李四的账户增加 500 块钱,这两个操作要么一同成功要么一同失利,怎样保证他们一同成功或许一同失利呢?答案便是业务。

业务有四大特性(ACID):

Spring 事务失效的六种情况

  • 原子性(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 是业务处理的中心,它有许多的完结类,如下:

Spring 事务失效的六种情况

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 用来描述业务的详细规矩,也称作业务的特点。业务有哪些特点呢?看下图:

Spring 事务失效的六种情况

能够看到,主要是五种特点:

  1. 阻隔性
  2. 传达性
  3. 回滚规矩
  4. 超时时刻
  5. 是否只读

这五种特点接下来松哥会和咱们详细介绍。

TransactionDefinition 类中的办法如下:

Spring 事务失效的六种情况

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

  1. getIsolationLevel(),获取业务的阻隔等级
  2. getName(),获取业务的称号
  3. getPropagationBehavior(),获取业务的传达性
  4. getTimeout(),获取业务的超时时刻
  5. isReadOnly(),获取业务是否是只读业务

TransactionDefinition 也有许多的完结类,如下:

Spring 事务失效的六种情况

假如开发者运用了编程式业务的话,直接运用 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;
}

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. 业务失效

那么什么状况下业务会失效呢?

6.1 办法自调用

这个主要是针对声明式业务的,通过前面的介绍,小伙伴们其实也能够看出来,声明式业务底层其实便是 AOP,所以在声明式业务中,咱们咱们拿到的服务类并不是服务类自身,而是一个署理方针,在这个署理方针中的署理办法中,主动增加了业务的逻辑,所以假如咱们直接办法自调用,没有通过这个署理方针,业务就会失效。

我写一段伪代码小伙伴们一同来看下:

public class UserService{
   @Transactional
   public void sayHello(){}
}

此刻,假如咱们在 UserController 中注入 UserService,那么拿到的并不是 UserService 方针自身,而是通过动态署理为 UserService 生成的一个动态署理类,这个动态署理就类似下面这样(伪代码):

public class UserServiceProxy extends UserService{
    public void sayHello(){
        try{
            //敞开业务
            //调用父类 sayHello
            //提交业务
        }catch(Exception e){
            //回滚业务
        }
    }
}

所以你终究调用的并不是 UserService 自身的办法,而是动态署理方针中的办法。

因此,假如存在这样的代码:

public class UserService{
   @Transactional
   public void sayHello(){}
   public void useSayHello(){sayHello();}
}

在 useSayHello 中调用 sayHello 办法,sayHello 办法上虽然有业务注解,可是这儿的业务不生效(由于调用的不是的动态署理方针中的 sayHello 办法,而是当时方针 this 的 sayHello 办法)。

6.2 反常被捕获

搞理解了 6.1,再来看 6.2 末节就很简单懂了。

假如咱们在 sayHello 办法中将反常捕获了,那么动态署理类中的办法,就感知不知道方针办法产生反常了,天然也就不会主动处理业务回滚了。仍是曾经面的 UserServiceProxy 为例:

public class UserServiceProxy extends UserService{
    public void sayHello(){
        try{
            //敞开业务
            //调用父类 sayHello
            //提交业务
        }catch(Exception e){
            //回滚业务
        }
    }
}

假如调用 调用父类 sayHello 的时分,sayHello 办法主动将反常捕获了,那么很明显,这儿就不会进行反常回滚了。

6.3 办法非 public

这个算是 Spring 官方的一个强制要求了,声明式业务办法只能是 public,关于非 public 的办法假如想用声明式业务,那得上 AspectJ。

6.4 非运转时反常

这个前面 5.3 末节介绍过了,默许状况下,只会捕获 RuntimeException,假如想扩大捕获范围,能够自行装备。

6.5 不是 Spring Bean

基于 6.1 末节的理解,来看这个应该也很好懂。声明式业务主要是通过动态署理来处理业务的,假如你拿到手的 UserService 方针便是原原本本的 UserService(假如自己 new 了一个 UserService 便是这种状况),那么业务代码在哪里?没有业务处理的代码,业务天然不会生效。

声明式业务的中心,便是动态署理生成的那个方针,没有用到那个方针,业务就没戏。

6.6 数据库不支撑业务

这个没啥好说,数据库不支撑,Spring 咋配都没用。

7. 小结

好啦,这便是松哥和咱们共享的 Spring 业务的玩法,不知道小伙伴们搞理解没有?