长文捋明白 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 中对事务的支持提供了三大基础设施,我们先来了解下。
PlatformTransactionManager
TransactionDefinition
TransactionStatus
这三个核心类是 Spring 处理事务的核心类。
2.2.1 PlatformTransactionManager
PlatformTransactionManager 是事务处理的核心,它有诸多的实现类,如下:
PlatformTransactionManager 的定义如下:
boolean isNewTransaction(); boolean hasSavepoint(); void setRollbackOnly(); boolean isRollbackOnly(); void flush(); boolean isCompleted(); }public interface TransactionStatus extends SavepointManager, Flushable {
isNewTransaction() 方法获取当前事务是否是一个新事务。
hasSavepoint() 方法判断是否存在 savePoint()。
setRollbackOnly() 方法设置事务必须回滚。
isRollbackOnly() 方法获取事务只能回滚。
flush() 方法将底层会话中的修改刷新到数据库,一般用于 Hibernate/JPA 的会话,对如 JDBC 类型的事务无任何影响。
isCompleted() 方法用来获取是一个事务是否结束。
这就是 Spring 中支持事务的三大基础设施。
3. 编程式事务
我们先来看看编程式事务怎么玩。
通过 PlatformTransactionManager 或者 TransactionTemplate 可以实现编程式事务。如果是在 Spring Boot 项目中,这两个对象 Spring Boot 会自动提供,我们直接使用即可。但是如果是在传统的 SSM 项目中,则需要我们通过配置来提供这两个对象,松哥给一个简单的配置参考,如下(简单起见,数据库操作我们使用 JdbcTemplate):
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); } } }@Service
这段代码很简单,没啥好解释的,在 try...catch...
中进行业务操作,没问题就 commit,有问题就 rollback。如果我们需要配置事务的隔离性、传播性等,可以在 DefaultTransactionDefinition 对象中进行配置。
上面的代码是通过 PlatformTransactionManager 实现的编程式事务,我们也可以通过 TransactionTemplate 来实现编程式事务,如下:
<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> <property name="dataSource" ref="dataSource"/> </bean><bean>
配置事务通知
<aop:pointcut expression="execution(* org.javaboy.demo.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="pc1"/> </aop:config><aop:config>
第二步和第三步中定义出来的方法交集,就是我们要添加事务的方法。
配置完成后,如下一些方法就自动具备事务了:
@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()); } }@Configuration
这里要配置的东西其实和 XML 中配置的都差不多,最最关键的就两个:
事务管理器 PlatformTransactionManager。
@EnableTransactionManagement 注解开启事务支持。
配置完成后,接下来,哪个方法需要事务就在哪个方法上添加 @Transactional
注解即可,向下面这样:
<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><?xml version="1.0" encoding="UTF-8"?>
那么 Java 代码中的配置如下:
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
TransactionDefinition 中定义了各种隔离级别。
PlatformTransactionManager
<tx:attributes> <!--以 add 开始的方法,添加事务--> <tx:method name="add*"/> <tx:method name="insert*" isolation="SERIALIZABLE"/> </tx:attributes> </tx:advice><tx:advice transaction-manager="transactionManager">
Java:
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; } }public enum Propagation {
具体含义如下:
一共是七种传播性,具体配置也简单:
TransactionTemplate中的配置
//创建事务的默认配置 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); } }public void update2() {
声明式事务的配置(XML)
public void update4() { jdbcTemplate.update("update account set money = ? where username=?;", 998, "lisi"); int i = 1 / 0; }@Transactional(noRollbackFor = ArithmeticException.class,propagation = Propagation.REQUIRED)
用就是这么来用,至于七种传播的具体含义,松哥来和大家一个一个说。
5.2.1 REQUIRED
REQUIRED 表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
例如我有如下一段代码:
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 transactiono.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 transactiono.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [org.javaboy.spring_tran02.AccountService2.handle2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
分析这段日志我们可以看到:
首先为 handle2 方法开启了一个事务。
执行完 handle2 方法的 SQL 之后,事务被刮起(Suspending)。
为 handle1 方法开启了一个新的事务。
执行 handle1 方法的 SQL。
提交 handle1 方法的事务。
恢复被挂起的事务(Resuming)。
提交 handle2 方法的事务。
从这段日志中大家可以非常明确的看到 REQUIRES_NEW 和 REQUIRED 的区别。
松哥再来简单总结下(假设 handle1 方法的事务传播性是 REQUIRES_NEW):
如果 handle2 方法没有事务,handle1 方法自己开启一个事务自己玩。
如果 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 : 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 transactiono.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [org.javaboy.demo.AccountService2.handle2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
从这段日志可以看出:
首先给 handle2 方法开启事务。
执行 handle2 方法的 SQL。
handle1 方法加入到已经存在的事务中。
执行 handle1 方法的 SQL。
提交事务。
假设 handle2 方法无事务,handle1 方法有事务且传播性为 MANDATORY,那么最终执行时会抛出如下异常:
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 transactiono.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [org.javaboy.demo.AccountService2.handle2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
这段日志很简单,没啥好说的,认准 Participating in existing transaction
表示加入到已经存在的事务中即可。
假设 handle2 方法无事务,handle1 方法有事务且传播性为 SUPPORTS,这个最终就不会开启事务了,也没有相关日志。
5.2.6 NOT_SUPPORTED
NOT_SUPPORTED 表示以非事务方式运行,如果当前存在事务,则把当前事务挂起。
假设 handle2 方法有事务,handle1 方法也有事务且传播性为 NOT_SUPPORTED,那么最终事务执行日志如下:
Existing transaction found for transaction marked with propagation "never"
5.3 回滚规则
默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)以及 Error 时才会回滚,在遇到检查型(Checked Exception)异常时不会回滚。
像 1/0,空指针这些是 RuntimeException,而 IOException 则算是 Checked Exception,换言之,默认情况下,如果发生 IOException 并不会导致事务回滚。
如果我们希望发生 IOException 时也能触发事务回滚,那么可以按照如下方式配置:
Java 配置:
<tx:attributes> <tx:method name="m3" rollback-for="java.io.IOException"/> </tx:attributes> </tx:advice><tx:advice transaction-manager="transactionManager">
另外,我们也可以指定在发生某些异常时不回滚,例如当系统抛出 ArithmeticException 异常并不要触发事务回滚,配置方式如下:
Java 配置:
<tx:attributes> <tx:method name="m3" no-rollback-for="java.lang.ArithmeticException"/> </tx:attributes> </tx:advice><tx:advice transaction-manager="transactionManager">
5.4 是否只读
只读事务一般设置在查询方法上,但不是所有的查询方法都需要只读事务,要看具体情况。
一般来说,如果这个业务方法只有一个查询 SQL,那么就没必要添加事务,强行添加最终效果适得其反。
但是如果一个业务方法中有多个查询 SQL,情况就不一样了:多个查询 SQL,默认情况下,每个查询 SQL 都会开启一个独立的事务,这样,如果有并发操作修改了数据,那么多个查询 SQL 就会查到不一样的数据。此时,如果我们开启事务,并设置为只读事务,那么多个查询 SQL 将被置于同一个事务中,多条相同的 SQL 在该事务中执行将会获取到相同的查询结果。
设置事务只读的方式如下:
Java 配置:
<tx:attributes> <tx:method name="m3" read-only="true"/> </tx:attributes> </tx:advice><tx:advice transaction-manager="transactionManager">
5.5 超时时间
超时时间是说一个事务允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。
事务超时时间配置方式如下(单位为秒):
Java 配置:
<tx:attributes> <tx:method name="m3" read-only="true" timeout="10"/> </tx:attributes> </tx:advice><tx:advice transaction-manager="transactionManager">
在 TransactionDefinition
中以 int 的值来表示超时时间,其单位是秒,默认值为-1。
6. 注意事项
事务只能应用到 public 方法上才会有效。
事务需要从外部调用, Spring 自调事务用会失效 。即相同类里边,A 方法没有事务,B 方法有事务,A 方法调用 B 方法,则 B 方法的事务会失效,这点尤其要注意,因为代理模式只拦截通过代理传入的外部方法调用,所以自调用事务是不生效的。
建议事务注解 @Transactional 一般添加在实现类上,而不要定义在接口上,如果加在接口类或接口方法上时,只有配置基于接口的代理这个注解才会生效。
来源:https://www.tuicool.com/articles/V7NVNfv