Spring事务传播实践与Spring“事务失效”
事务传播机制
研究方法
想要自己动手的小伙伴,可以参考最后一节“代码脚本清单”。
我们想要了解 Spring 事务传播的影响,那么我们先要对事务有所了解。
本质上讲,同一个连接,提交前的所有sql构成一个事务。所以说找连接就对了。
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
比如在这个项目里,我用的是 DriverManagerDataSource,那我就可以在这个类里面找 getConnection 的方法,这就让我找到了
protected Connection getConnectionFromDriverManager(String url, Properties props) throws SQLException {
return DriverManager.getConnection(url, props);
}
在 debug 模式下“顺藤摸瓜”,就找到赋值的地方
// AbstractDriverBasedDataSource , DriverManagerDataSource 的超类
protected Connection getConnectionFromDriver(@Nullable String username, @Nullable String password) throws SQLException {
// ...(省略)
Connection con = this.getConnectionFromDriver(mergedProps);
// ...(省略)
}
我们还发现了 createUser 被 cglib 动态代理的秘密!盒盒盒~
因为 UserService 使用的是 JdbcTemplate.update 方法,所以我们顺着这个代码往下找 getConnection 的地方,再次找到
// JdbcTemplate.class
public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action) throws DataAccessException {
// ...(省略)
Connection con = DataSourceUtils.getConnection(this.obtainDataSource());
// ...(省略)
}
通过在这行打断点,我们就可以知道 createUser 和 addAccount 是不是使用的同一个连接,是不是同一个事务了!
无事务
Spring 常用的是声明式事务,即使用 @Transactional 注解,那么相反的,不使用声明就是无事务的情况。
在不声明事务的情况下, createUser 方法和 addAccount 方法中的 jdbcTemplate.update 会分别打开不同的连接。并且,代码执行成功后,自动提交到数据库。
单个方法与事务
在研究事务的传播之前,我们先看看单个方法设置事务传播类型的情况
@Transactional(propagation = Propagation.XXX)
public void createUser(String name) {
// 新增用户基本信息
jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
// 出现分母为零的异常
int i = 1 / 0;
}
情况分为三类:
- 创建事务的:创建事务的都会因为出现的 java.lang.ArithmeticException: / by zero 异常而回滚
REQUIRED,REQUIRES_NEW,NESTED - 不创建事务的:不创建事务,也就意味着是无事务状态,所以发生异常也不会回滚。最终数据都写入了数据库。
SUPPORTS,NOT_SUPPORTED,NEVER - 抛异常的:试图通过 PlatformTransactionManager.getTransaction 获取当前事务,但是如果当前事务不存在,抛出org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'异常
MANDATORY
createUser 异常
既然是研究事务的传播,那首先要保证有事务,能产生事务主要是 REQUIRED,REQUIRES_NEW,NESTED。
- REQUIRED
如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
这个也是默认的传播机制。 - REQUIRES_NEW
新建事务,如果当前存在事务,把当前事务挂起。 - PROPAGATION_NESTED
如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
注意:实际创建嵌套事务将仅在特定事务管理器上起作用。 开箱即用,这仅适用于JDBC DataSourceTransactionManager。 一些JTA提供程序可能也支持嵌套事务。
我们把这三个分别放在 createUser 上进行实验。
@Transactional(propagation = Propagation.XXX)
public void createUser(String name) {
// 新增用户基本信息
jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
//调用accountService添加帐户
accountService.addAccount(name, 10000);
// 出现分母为零的异常
int i = 1 / 0;
}
实验编号 | createUser(异常) | addAccount | 是否同一个事务 | User是否插入 | Account是否插入 | 备注 |
---|---|---|---|---|---|---|
1 | required | 无事务 | 是 | 失败 | 失败 | 虽然 addAccount 没有明确声明事务,但是默认共享了createUser的连接和事务 |
2 | required | required | 是 | 失败 | 失败 | required:如果已经存在一个事务,就加入到这个事务中。addAccount 符合该行为 |
3 | required | supports | 是 | 失败 | 失败 | supports:在 createUser 有事务时,加入到这个事务中。 |
4 | required | mandatory | 是 | 失败 | 失败 | createUser 已经创建一个事务,addAccount 加入到当前事务,未报错 |
==== | ==== | ==== | ==== | ==== | ==== | 1-4组实验,都加入到了当前事务。也就是说都支持当前事务 |
5 | required | requires_new | 否 | 失败 | 成功 | addAccount 新建事务并在完成后提交。所以 createUser 中的异常不会影响 addAccount 的事务提交。 |
6 | required | not_supported | 否 | 失败 | 成功 | addAccount 以非事务方式执行。 |
==== | ==== | ==== | ==== | ==== | ==== | 5-6两组实验,表明 requires_new 和 not_supported 均不支持当前的事务 |
7 | required | nested | 是 | 失败 | 失败 |
- createUser 换成 requires_new 和 nested 实验结果都一样。
addAccount 异常
public class UserService {
// ...
@Transactional(propagation = Propagation.REQUIRED)
public void createUser(String name) {
jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
accountService.addAccount(name, 10000);
}
}
@Service
public class AccountService {
// ...
@Transactional(propagation = Propagation.XXX)
public void addAccount(String name, int initMoney) {
String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
jdbcTemplate.update("insert INTO account (account_name,user,money) VALUES (?,?,?)", accountid, name, initMoney);
// 出现分母为零的异常
int i = 1 / 0;
}
}
实验编号 | createUser | addAccount(异常) | 是否同一个事务 | User是否插入 | Account是否插入 | 备注 |
---|---|---|---|---|---|---|
1 | required | 无事务 | 是 | 失败 | 失败 | |
2 | required | required | 是 | 失败 | 失败 | |
3 | required | supports | 是 | 失败 | 失败 | |
4 | required | mandatory | 是 | 失败 | 失败 | |
==== | ==== | ==== | ==== | ==== | ==== | |
5 | required | requires_new | 否 | 失败 | 失败 | 异常向上抛出,导致 createUser 事务也执行失败。 |
6 | required | not_supported | 否 | 失败 | 成功 | addAccount 以非事务方式执行。出现异常前已经将改动自动提交到数据库。 |
==== | ==== | ==== | ==== | ==== | ==== | 5-6两组实验,表明 requires_new 和 not_supported 均不支持当前的事务 |
7 | required | nested | 是 | 失败 | 失败 |
Spring“事务失效”
@Service
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.REQUIRED)
public void createUser(String name) {
// 新增用户基本信息
jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
//调用accountService添加帐户
this.addAccount(name, 10000);
// 出现分母为零的异常
int i = 1 / 0;
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addAccount(String name, int initMoney) {
String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
jdbcTemplate.update("insert INTO account (account_name,user,money) VALUES (?,?,?)", accountid, name, initMoney);
// 出现分母为零的异常
// int i = 1 / 0;
}
}
按照我们的“惯性思维”,这里应该 createUser 和 addAccount 是不同的事务,因此,account 插入成功,user 失败。但是结果却“出人意料”, account 和 user 都没有插入成功!
难道 Spring “事务失效”了?
首先我们调试发现 UserService 调用 createUser 时,是通过动态代理实现的,获取事务,开启事务这些操作也都是由动态代理完成的。
而直接通过 this.addAccount 是直接调用对象内的方法,而不会触发动态代理的,这一点和使用 accountService.addAccount 是不一样的:
如果真想在 UserService 里面调用 addAccount,而且事务要起作用的话,可以这么干
@Service
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private UserService userService;
@Transactional(propagation = Propagation.REQUIRED)
public void createUser(String name) {
// 新增用户基本信息
jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
//调用userService(自身)添加帐户
userService.addAccount(name, 10000);
// 出现分母为零的异常
int i = 1 / 0;
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addAccount(String name, int initMoney) {
String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
jdbcTemplate.update("insert INTO account (account_name,user,money) VALUES (?,?,?)", accountid, name, initMoney);
// 出现分母为零的异常
// int i = 1 / 0;
}
}
NEVER
public class UserService {
// ...
@Transactional
public void createUser(String name) {
jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
accountService.addAccount(name, 10000);
}
}
@Service
public class AccountService {
// ...
@Transactional(propagation = Propagation.NEVER)
public void addAccount(String name, int initMoney) {
String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
jdbcTemplate.update("insert INTO account (account_name,user,money) VALUES (?,?,?)", accountid, name, initMoney);
}
}
抛出异常 org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'。
以非事务方式执行,如果当前存在事务,则抛出异常。
MANDATORY
@Transactional(propagation = Propagation.MANDATORY)
public void createUser(String name) {
jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
accountService.addAccount(name, 10000);
}
抛出异常 org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory' 。
如果当前没有事务,就抛出异常。
总结
- Spring 事务的传播机制,支持继续使用当前事务的主要有Propagation.REQUIRED,Propagation.SUPPORTS,Propagation.MANDATORY,不支持继续使用当前事务的包括Propagation.REQUIRES_NEW,Propagation.NOT_SUPPORTED,Propagation.NEVER。还有一个与 REQUIRED 行为类似的 NESTED 嵌套传播。
- Spring 事务传播时,判断两个方法是否属于同一个事务,关键还得看他们是否使用相同的数据库连接。
- Spring 事务是基于 AOP 的,所以直接使用 this 方法会导致“事务失效”。
扩展阅读
spring中事务的注解配置优先级别 阅读
Transactional注解比XML配置优先级要高
聊聊如何在spring事务中正确进行远程调用 阅读
1.TransactionSynchronizationManager.registerSynchronization
2.通过TransactionalEventListener注解+ApplicationEventPublisher
代码脚本清单
建表sql语句:
CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
PRIMARY KEY (`id`)
) COMMENT '用户表';
CREATE TABLE `account` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`account_name` VARCHAR(100) DEFAULT NULL,
`user` VARCHAR(100) DEFAULT NULL,
`money` DOUBLE DEFAULT NULL,
PRIMARY KEY (`id`)
) COMMENT='账号表';
项目目录:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.coderead</groupId>
<artifactId>spring-transaction</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
spring.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
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 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">
<context:annotation-config/>
<context:component-scan base-package="org.coderead.spring.**"> </context:component-scan>
<bean class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--
similarly, don't forget the PlatformTransactionManager
-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- don't forget the DataSource -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<constructor-arg name="url" value="jdbc:mysql://localhost:3306/tx_experience?serverTimezone=UTC"/>
<constructor-arg name="username" value="root"/>
<constructor-arg name="password" value="123456"/>
</bean>
<tx:annotation-driven transaction-manager="txManager"/>
</beans>
org.coderead.spring.tx.SpringTransactionTest.java
public class SpringTransactionTest {
@Test
public void test() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserService bean = context.getBean(UserService.class);
bean.createUser("kendoziyu");
}
}
org.coderead.spring.tx.UserService.java
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private AccountService accountService;
@Transactional
public void createUser(String name) {
// 新增用户基本信息
jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
//调用accountService添加帐户
accountService.addAccount(name, 10000);
// 出现分母为零的异常
// int i = 1 / 0;
}
}
org.coderead.spring.tx.AccountService.java
@Service
public class AccountService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public void addAccount(String name, int initMoney) {
String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
jdbcTemplate.update("insert INTO account (account_name,user,money) VALUES (?,?,?)", accountid, name, initMoney);
// 出现分母为零的异常
// int i = 1 / 0;
}
}