拿下 Spring 事务
什么是事务
事务是数据库操作的最基本单元,是逻辑上的一组操作,要么都成功,要么都失败。是一个不可分割的工作单元。
事务的使用
事务具有 4 个特性:原子性、一致性、隔离性】持久性,简称为 ACID 特性。
- 原子性(Atomicity):一个事务是一个不可分割的工作单位,一个事务中包括的操作要么都成功要么都失败。
- 一致性(Consistency):事务必须保证数据库从一个一致性状态变到另一个一致性状态。比如转账的总金额,不能转着转着总金额少了或者多了。大部分一致性的需求需要程序员写业务代码保证。
- 隔离性(Isolation):一个事务的执行不能被其它事务干扰,即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事务之间不能互相打扰。
- 持久性(Durability):持久性也称为永久性,指一个事务一旦提交,它对数据库中数据的改变就是永久性的,后面的其它操作和故障都不应该对其有任何影响。
为什么要用事务
举例:银行转账。小明给小红转 100 元。小明需要减少余额 100,小红需要增加余额 100。这是两个操作,需要一起成功。如果在小明转账成功之后发生了异常,就会出现小明 减 100 余额,但是小红并没有加 100 余额。就会造成钱丢失的情况。这是绝对不允许的。伪代码如下:
public void accountMoney() { int money = 100; //小明 少 100 userDao.reduceMoney(money); // 其他业务 发生异常 int i = 1/0; //小红 多 100 userDao.addMoney(money); }
事务管理方式
Spring 支持 2 种事务管理方式。
- 编程式事务管理 编程式事务管理是通过编写代码实现的事务管理。可以根据需求规定事务从哪里开始,到哪里结束,拥有很高的灵活性。但是这种方式,会使业务代码与事务规则高度耦合,难以维护,因此我们很少使用这种方式对事务进行管理。所以,本文给大家介绍的是如何使用声明式事务管理。
- 声明式 声明式事务管理可以通过 2 种方式实现,分别是 XML和注解方式。Spring 在进行声明式事务管理时,底层使用了 AOP 。
事务管理器
Spring 并不会直接管理事务,而是通过事务管理器对事务进行管理的。
PlatformTransactionManager
Spring 提供了一个 PlatformTransactionManager 接口,这个接口被称为 Spring 的事务管理器,其源码如下:
public interface PlatformTransactionManager { // 根据传入的 TransactionDefinition 对象获取一个事务状态对象 TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException; // 提交事务 void commit(TransactionStatus var1) throws TransactionException; // 回滚事务 void rollback(TransactionStatus var1) throws TransactionException; }
该接口的源码很简单。这个接口针对不同的框架提供了不同的实现类,如下:
| 实现类 | 说明 |
|---|---|
| org.springframework.jdbc.datasource.DataSourceTransactionManager | 提供给 Spring JDBC 、MBatis 的事务管理器 |
| org.springframework.orm.hibernate5.HibernateTransactionManager | 提供给 Hibernate 的事务管理器 |
| org.springframework.orm.jpa.JpaTransactionManager | 提供给 JPA 的事务管理器 |
| org.springframework.jdo.JdoTransactionManager | 提供给 Jdo 的事务管理器 |
| org.springframework.transaction.jta.JtaTransactionManager | 提供给 JTA 的事务管理器 |
注意:这些实现类,需要导入对应的依赖才能看到。 该接口中还有两个对象,分别是 TransactionDefinition 和 TransactionStatus。
TransactionDefinition
- TransactionDefinition:事务定义,定义了事务的名称,传播属性,事务隔离级别,是否只读,超时时间。
public interface TransactionDefinition { int PROPAGATION_REQUIRED = 0; int PROPAGATION_SUPPORTS = 1; int PROPAGATION_MANDATORY = 2; int PROPAGATION_REQUIRES_NEW = 3; int PROPAGATION_NOT_SUPPORTED = 4; int PROPAGATION_NEVER = 5; int PROPAGATION_NESTED = 6; int ISOLATION_DEFAULT = -1; int ISOLATION_READ_UNCOMMITTED = 1; int ISOLATION_READ_COMMITTED = 2; int ISOLATION_REPEATABLE_READ = 4; int ISOLATION_SERIALIZABLE = 8; int TIMEOUT_DEFAULT = -1; default int getPropagationBehavior() { return 0; } default int getIsolationLevel() { return -1; } default int getTimeout() { return -1; } default boolean isReadOnly() { return false; } @Nullable default String getName() { return null; } static TransactionDefinition withDefaults() { return StaticTransactionDefinition.INSTANCE; } }
-
PROPAGATION_** 0 ~ 7 代表的是事务传播行为
-
ISOLATION_** -1 ~ 8 代表的是事务的隔离级别
-
TIMEOUT_DEFAULT 默认的超时时间,-1,代表使用数据库的超时时间
-
getPropagationBehavior:获取事务的传播行为,默认为 PROPAGATION_REQUIRED
-
getIsolationLevel:获取事务的隔离级别,默认为所使用数据库的隔离级别
-
getTimeout:获取事务的超时时间
-
isReadOnly:事务是否只读
-
getName:获取事务的名称
TransactionStatus
- TransactionStatus:事务状态,保存了事务执行过程中的状态。
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable { boolean hasSavepoint(); @Override void flush(); }
方法说明如下:
| 名称 | 说明 |
|---|---|
| hasSavepoint | 事务内部是否带有保存点 |
| flush | 刷新事务 |
- TransactionExecution
public interface TransactionExecution { boolean isNewTransaction(); void setRollbackOnly(); boolean isRollbackOnly(); boolean isCompleted(); }
方法说明如下:
| 名称 | 说明 |
|---|---|
| isNewTransaction | 当前事务是否是新的 |
| setRollbackOnly | 设置事务回滚 |
| isRollbackOnly | 事务是否已被标记为回滚 |
| isCompleted | 事务是否完成,即是否已经提交或回滚 |
- SavepointManager
public interface SavepointManager { Object createSavepoint() throws TransactionException; void rollbackToSavepoint(Object savepoint) throws TransactionException; void releaseSavepoint(Object savepoint) throws TransactionException; }
方法说明如下:
| 名称 | 说明 |
|---|---|
| createSavepoint | 创建保存点 |
| rollbackToSavepoint | 回滚到给定的保存点 |
| releaseSavepoint | 释放给定的保存点 |
有一个默认的抽象实现 AbstractTransactionStatus,对 TransactionExecution、savepoint、SavepointManager 有具体的实现逻辑,代码有点多,就不贴了,但是非常好理解。 对 TransactionExecution、savepoint、SavepointManager 有具体的实现逻辑,代码有点多,就不贴了,但是非常好理解。 DefaultTransactionStatus 又继承了 AbstractTransactionStatus,继续进行扩充。
public class DefaultTransactionStatus extends AbstractTransactionStatus { // 具体事务对象 @Nullable private final Object transaction; //是否是新开启的事务(=true时才会提交事务) private final boolean newTransaction; // 是否是新建的同步器(=true时才会执行回调事件) private final boolean newSynchronization; // 是否只读 private final boolean readOnly; // 是否已调试 private final boolean debug; // 挂起的资源信息(事务传播行为要求挂起当前事务时,挂起的事务暂存信息,执行完后用于恢复) @Nullable private final Object suspendedResources; //省略。。。。 }
事务传播行为
事务传播行为指的是,多事务方法之间进行调用时,这个过程中事务应该如何进行管理。例如,事务方法 A 在调用事务方法 B 时,B 方法是在调用者 A 方法的事务中运行呢,还是为自己开启一个新事务运行,这就是由事务方法 B 的事务传播行为决定的。
事务方法:能让数据库表数据发生改变的方法,例如新增、删除、修改数据的方法。
public enum Propagation { REQUIRED(0), SUPPORTS(1), MANDATORY(2), REQUIRES_NEW(3), NOT_SUPPORTED(4), NEVER(5), NESTED(6); }
| 行为 | 说明 |
|---|---|
| REQUIRED | 如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行 |
| SUPPORTS | 如果有事务在运行,当前的方法就在这个事务内运行;如果当前没有事务,则以非事务的方式运行。 |
| MANDATORY | 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 |
| REQUIRES_NEW | 当前的方法必须启动新事务,并在它自己的事务内运行,如果有事务正在运行,应该将它挂起 |
| NOT_SUPPORTED | 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 |
| NEVER | 以非事务方式运行,如果当前存在事务,则抛出异常。 |
| NESTED | 如果当前存在事务,则创建一个新事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 REQUIRED。 |
根据上面的描述,我们可以将行为分为三大类。
- 不要事务:NEVER、NOT_SUPPORTED。
- 如果有则用:SUPPORTS
- 必须使用事务:REQUIRED、REQUIRES_NEW、NESTED、MANDATORY
隔离级别
事务有一个特性为隔离性,多事务操作之间不会产生影响。但如果不考虑隔离性,则会产生三个读问题:脏读、不可重复读、虚(幻)读。
-
脏读:一个未提交事务读取到另一个未提交事务的数据
-
不可重复读:一个未提交事务读取到另一个提交事务修改的数据
-
虚(幻)读:一个未提交事务读取到另一提交事务添加的数据 那如何解决呢?可以通过设置事务隔离级别,解决读问题!Spring 中提供了以下隔离级别。
级别 说明 DEFAULT 使用所用的数据库的隔离级别 READ_UNCOMMITTED(读未提交) 可以读取到尚未提交的更改,可能导致脏读、幻读和不可重复读 READ_COMMITTED(读已提交) Oracle 的默认级别,可以读取到已提交的更改的数据,防止脏读,可能出现幻读和不可重复读 REPEATABLE_READ(可重复读) MySQL 的默认级别,同一条SQL多次执行,可以读取到已提交的新增的数据,防止脏读和不可重复读,可能出现幻读 SERIALIZABLE 可串行化,什么读问题都不会产生
加入依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.8.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.0.8.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.6</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.6</version> </dependency>
xml方式
我们先来看看不使用事务会发生什么情况。创建名为 aopxml的包。
提供数据库脚本
CREATE TABLE `tx_test` ( `id` int(11) NOT NULL, `name` varchar(64) DEFAULT NULL, `money` decimal(10,0) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `test`.`tx_test`(`id`, `name`, `money`) VALUES (1, '张三', 1000); INSERT INTO `test`.`tx_test`(`id`, `name`, `money`) VALUES (2, '李四', 1000);
开发代码
新建 dao 包
在类中提供两个方法,一个张三增加金额,一个李四减金额。
@Repository public class TXDao { @Autowired private JdbcTemplate jdbcTemplate; /** * 给张三增加金额 */ public void add(){ String sql = "update `tx_test` set money = money + 100 where id = 1;"; jdbcTemplate.update(sql); } /** * 给李四减金额 */ public void reduce(){ String sql = "update `tx_test` set money = money - 100 where id = 2;"; jdbcTemplate.update(sql); } }
新建 entity
public class TxTest { private Integer id; private String name; private BigDecimal money; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public BigDecimal getMoney() { return money; } public void setMoney(BigDecimal money) { this.money = money; } }
新建 service 包
@Service public class TXServiceImpl { @Autowired private TXDao tx; public void transfer(){ tx.add(); int i = 1/0; tx.reduce(); } }项目结构如下:

测试
public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("tx.xml"); TXServiceImpl bean = context.getBean(TXServiceImpl.class); bean.transfer(); }
控制台出现异常
Exception in thread "main" java.lang.ArithmeticException: / by zero at cn.cxyxj.txannon.service.TestServiceImpl.transfer(TestServiceImpl.java:20) at cn.cxyxj.txannon.AppMain.main(AppMain.java:20)

再来查看数据库数据,可以发现张三的金额增加了,但是李四的金额没有减。银行哭死!!! 所以我们需要引入 Spring 事务,解决上述出现的问题。
引入 tx 命名空间
<?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: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/aop https://www.springframework.org/schema/aop/spring-aop.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"> </beans>
注意: 上面说过 Spring 提供的声明式事务管理是依赖于 Spring AOP 实现的,因此还需要添加 aop 命名空间配置。当然我还额外引入了 spring-context 命名空间。
配置事务管理器以及 JdbcTemplate
<!--引入 jdbc.properties 中的配置--> <context:property-placeholder location="classpath:jdbc.properties"> </context:property-placeholder> <!--配置数据源 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <!--数据库连接地址--> <property name="url" value="${jdbc.url}"/> <!--数据库的用户名--> <property name="username" value="${jdbc.username}"/> <!--数据库的密码--> <property name="password" value="${jdbc.password}"/> <!--数据库驱动--> <property name="driverClassName" value="${jdbc.driver}"/> </bean> <!--定义 JdbcTemplate Bean--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!--将数据源的 Bean 注入到 JdbcTemplate 中--> <property name="dataSource" ref="dataSource"></property> </bean> <!--配置事务管理器,以 JDBC 为例--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean>
配置的事务管理器实现为 DataSourceTransactionManager,是 JDBC 和 MBatis 的PlatformTransactionManager 接口实现。
jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://192.168.50.172/tx_test?useSSL=false jdbc.username=Yj19980402 jdbc.password=root
配置事务通知
配置事务通知,指定所需要使用的事务管理器以及指定事务作用的方法和该事务属性。
<!--配置通知--> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <!--配置事务参数--> <tx:attributes> <!--指定哪个方法上面添加事务--> <tx:method name="transfer*" propagation="REQUIRED" isolation="DEFAULT" read-only="false" timeout="10"/> <!--可以配置多个方法 <tx:method name="account*"/>--> </tx:attributes> </tx:advice>
transaction-manage参数的默认值就是 transactionManager,如果事务管理器 id 与其一致,则可以不用指定。 <tx:method>元素包含多个属性参数,可以为某个或某些方法(name 属性指定的方法)定义事务属性,如下表所示:
| 事务属性 | 说明 |
|---|---|
| propagation | 指定事务的传播行为,默认为 REQUIRED |
| isolation | 指定事务的隔离级别,默认为所使用数据库的隔离级别 |
| read-only | 指定是否为只读事务,默认为 false |
| timeout | 表示超时时间,单位为“秒”。事务在指定的超时时间后,自动回滚。避免事务长时间不提交导致数据库资源占用。默认为 -1,代表不超时 |
| rollback-for | 指定出现哪些异常进行事务回滚 |
| no-rollback-for | 指定出现哪些异常不进行事务回滚 |
配置切入点和切面
<aop:config> <!--配置切入点--> <aop:pointcut id="pt" expression="execution(* com.cxyxj.aopxml.service.TXServiceImpl.*(..))"/> <!--配置切面--> <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/> </aop:config>
如上写法就对 transfer 方法进行了事务管理。就不会出现小明减少余额,而小红没有增加余额的情况,发生了异常就进行回滚。
注解方式
使用注解方式就不会有上面如此琐碎的配置了。再重新创建名为 txannon包,将 xml 方式使用到的 entity、dao、service 相关代码 copy 过来。
开启事务
使用 EnableTransactionManagement注解开启事务。
@ComponentScan(basePackages = "com.cxyxj.txannon") @EnableTransactionManagement //开启事务 public class AppMain { }
相当于 tx:annotation-driven 标签。
创建配置类
@Configuration @PropertySource("jdbc.properties") public class TxConfig { @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Value("${jdbc.driver}") private String driverClassName; //创建数据库连接池 @Bean public DriverManagerDataSource getDruidDataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(driverClassName); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; } //创建 JdbcTemplate 对象 @Bean public JdbcTemplate getJdbcTemplate(DataSource dataSource) { //到 ioc 容器中根据类型找到 dataSource JdbcTemplate jdbcTemplate = new JdbcTemplate(); //注入 dataSource jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } //创建事务管理器 @Bean public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; } }
可以不需要在配置切入点和切面了。
添加事务注解
在需要添加事务的方法上添加 @Transactional注解,表明该方法需要进行事务管理。
@Service public class TXServiceImpl { @Autowired private TXDao tx; @Transactional public void transfer(){ tx.add(); int i = 1/0; tx.reduce(); } }
@Transactional这个注解可以添加到类上面,也可以添加方法上面。如果把这个注解添加到类上面,这个类里面所有的方法都添加事务,如果把这个注解添加方法上面,则是为这个方法添加事务。
@Transactional
Transactional 这个注解里面可以配置很多事务相关参数。
public @interface Transactional { @AliasFor("transactionManager") String value() default ""; @AliasFor("value") String transactionManager() default ""; Propagation propagation() default Propagation.REQUIRED; Isolation isolation() default Isolation.DEFAULT; int timeout() default -1; boolean readOnly() default false; Class<? extends Throwable>[] rollbackFor() default {}; String[] rollbackForClassName() default {}; Class<? extends Throwable>[] noRollbackFor() default {}; String[] noRollbackForClassName() default {}; }
| 事务属性 | 说明 |
|---|---|
| value | 指定不同的事务管理器。 |
| transactionManager | 跟 value 一致。 |
| propagation | 指定事务的传播行为,默认为 REQUIRED |
| isolation | 指定事务的隔离级别,默认为所使用数据库的隔离级别 |
| read-only | 指定是否为只读事务,默认为 false |
| timeout | 表示超时时间,单位为“秒”。事务在指定的超时时间后,自动回滚。避免事务长时间不提交导致数据库资源占用。默认为 -1,代表不超时 |
| rollbackFor | 指定出现哪些异常进行事务回滚 |
| rollbackForClassName | 指定异常类名称,进行事务回滚 |
| noRollbackFor | 指定出现哪些异常不进行事务回滚 |
| noRollbackForClassName | 指定出现哪些异常类名称不进行事务回滚 |
基本用***了,现在就来看看事务的传播行为,这是Spring事务中难以理解的一块,因为它的场景很多。
事务传播行为详解
REQUIRED
如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行

- 如果 transfer 方法没有事务,则 reduce 方***创建一个事务。
- 两个方法的事务的传播行为都为 Propagation.REQUIRED。所以 transfer方***先开启一个事务,而 reduce会加入到 transfer方法的事务中,这两个方法用的是同一个事务,所以不论是在哪个方法中抛出异常,所有操作都会回滚。
REQUIRES_NEW
当前方法必须启动新事务,并在它自己的事务内运行。如果有事务正在运行,应该将它挂起。

reduce 方法行为修改为 Propagation.REQUIRES_NEW。transfer 方法创建新事务,然后调用 reduce 方法,reduce 方***将 transfer 方法的事务挂起,并创建属于 reduce 方法的事务。所以在该例子中会创建两个事务。由于有两个事务,那事务的回滚就出现了几种情况。
- 场景一

transfer 方法进行的操作会回滚,reduce 方法的操作不会回滚。
- 场景二

两个方法的操作都会回滚。这是由于 reduce 方法的异常会向 transfer 方法传递。
- 场景三

transfer 方法进行的操作不会回滚,reduce 方法的操作会回滚。
- 如果 transfer 方法没有事务,则 reduce 方***创建一个事务。
- 如果 transfer 方法有事务,则 reduce 方***将 transfer 方法的事务挂起,并创建属于 reduce 方法的事务。如果此时 transfer 方法发生了异常,则 transfer 方法操作会回滚,但不会导致 reduce 方法回滚。如果 reduce 方法发生了异常,则 reduce 方法操作会回滚,如果 transfer 方法没有捕获 reduce 方法的异常,那 transfer 方法也会回滚。
NESTED
如果当前存在事务(主事务),则创建一个新事务作为当前事务的嵌套事务(子事务)来运行;如果当前没有事务,则该取值等价于 REQUIRED。

- 如果 transfer 方法没有事务,则 reduce 方***创建一个事务。
- 如果 transfer 方法有事务,则 reduce 方***创建一个新事务,作为 transfer 方法事务的嵌套事务来运行。那会有什么场景呢?
- 场景一

transfer 方法发生异常并回滚,会导致 reduce 方法 同时回滚。
- 场景二

transfer 方法进行的操作不会回滚,reduce 方法的操作会回滚。注意:transfer 方法需要进行 catch,不然 transfer 方法也会回滚。
主事务方法异常回滚时,会同时回滚子事务。而子事务可以单独异常回滚,可以不影响主事务和其他子事务(前提是需要处理掉子事务的异常)
MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

由于 transfer 方法没有事务,在启动时就会抛出异常,如下:
No existing transaction found for transaction marked with propagation 'mandatory'
SUPPORTS
如果有事务在运行,当前的方法就在这个事务内运行;如果当前没有事务,则以非事务的方式运行。

由于 transfer 方法没有事务,所以 reduce 方法也不会创建事务,发生了异常也不会进行回滚。
NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。
transfer 方法有事务,但 reduce 方法传播行为是 NOT_SUPPORTED,所以会将 transfer 方法事务挂起,reduce 方法以非事务的方式运行。
所以图片例子会出现 transfer 方法进行的操作会回滚,reduce 方法的操作不会回滚。
NEVER
以非事务方式运行,如果当前存在事务,则抛出异常。

由于 transfer 方法有事务,在启动时就会抛出异常,如下:
Existing transaction found for transaction marked with propagation 'never'
回滚规则
上面一直在说遇到异常就回滚,那是遇到所有异常都会回滚吗?不是的,默认情况下,Spring 事务只有遇到 RuntimeException 以及 Error 时才会回滚,在遇到检查型异常时是不会回滚的,比如 IOException、TimeoutException。
那如果想在发生检查型异常时也进行回滚呢,可以使用 rollbackFor 属性进行如下配置:

那同理,如果遇到某个异常,不想进行回滚,使用 noRollbackFor 属性配置如下:
