长文捋明白 Spring 事务!隔离性?传播性?一网打尽!

Posted _江南一点雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了长文捋明白 Spring 事务!隔离性?传播性?一网打尽!相关的知识,希望对你有一定的参考价值。


事务的重要性不言而喻,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),以上是关于长文捋明白 Spring 事务!隔离性?传播性?一网打尽!的主要内容,如果未能解决你的问题,请参考以下文章

实战Spring事务传播性与隔离性

Spring事务传播性与隔离级别

spring事务传播性与隔离级别

事务事务特性事务隔离级别spring事务传播特性

spring事务的传播机制和隔离性

什么是事务事务特性事务隔离级别spring事务传播特性