SpringTx
我们知道诸如MySQL、Oracle、SQLServer等这类大型关系型数据库系统具有事务和事务隔离级别的概念,我们的Java程序一般都是通过JDBC操作数据库,比如开启事务、提交事务、设置隔离级别等,在JDBC接口中也有这些概念对应的API。如果直接使用JDBC操作数据库,那么我们就需要调用相关的API手动管理事务,不过在Spring中,框架提供了事务管理器TransactionManager功能。我们在配置文件中配置好事务管理器,当框架从数据源中获取JDBC的Connection对象并进行一些操作时,Spring会自动通过AOP拦截管理事务操作,合理使用Spring事务管理器能大大简化我们代码的编写。
事务相关基本概念
在具体介绍Spring中事务管理器相关用法之前,我们先回忆一下有关数据库事务的基础概念。
事务(Transaction):事务是数据库操作的基本执行单元,它将一组数据库操作捆绑在一起,要么全部成功执行,要么全部不执行。举例来说,一个经典的场景是银行转账,从账户A扣款和向账户B入账这两个操作必须作为一个整体,任何一步失败都需要撤销已完成的操作,否则就会出现数据不一致的问题。
ACID是数据库事务正确执行的四个基本要素:
- 原子性(Atomicity):事务中的所有操作要么全部成功提交,要么全部失败回滚,不存在中间状态。
- 一致性(Consistency):事务执行前后,数据库必须从一个一致的状态转变到另一个一致的状态,业务规则和数据约束始终得到保障。
- 隔离性(Isolation):多个并发事务之间互相隔离,一个事务的中间状态不应被其他事务看到,隔离的程度由隔离级别决定。
- 持久性(Durability):事务一旦提交,其对数据库的改变是永久性的,即使系统崩溃也不会丢失。
事务隔离级别:为了平衡并发性能与数据一致性,数据库定义了四种标准隔离级别,从低到高分别是:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE。隔离级别越高,并发性能越低,但数据一致性越好。JDBC作为一个通用的抽象接口,其中也定义了相关隔离级别的常量值,这部分源码可以在java.sql.Connection类中找到。
| 事务隔离级别 | 说明 |
|---|---|
| TRANSACTION_NONE | 代表JDBC驱动不支持事务,一般不会用到该项。 |
| TRANSACTION_READ_UNCOMMITTED | 允许脏读,不可重复读和幻读。 |
| TRANSACTION_READ_COMMITTED | 不可脏读,但允许不可重复读和幻读。 |
| TRANSACTION_REPEATABLE_READ | 不可脏读和不可重复读,但允许幻读。 |
| TRANSACTION_SERIALIZABLE | 不可脏读,不可重复读,不可幻读。 |
注:MySQL数据库使用InnoDB存储引擎时支持事务,默认的事务隔离级别为REPEATABLE_READ。不过InnoDB使用MVCC(多版本并发控制)和Gap Lock(间隙锁)实现事务隔离,幻读也被阻止了,因此实际严格程度超过标准定义。有关这部分内容可以参考MySQL相关章节,这里不过多展开介绍。
事务传播属性:JDBC实现了事务API,但“事务传播”这个概念严格来说是由Spring框架提供的,而不是JDBC原生的功能。Spring框架中的事务管理器支持事务传播配置,它用于配置在多个声明了事务的方法互相调用时,事务如何在方法间传播。
| 事务传播属性 | 说明 |
|---|---|
| PROPAGATION_REQUIRED | 支持当前事务,如果当前没有事务就新建,最常用,默认。 |
| PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
| PROPAGATION_MANDATORY | 支持当前事务,如果当前没有事务,就抛出异常。 |
| PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
| PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
| PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
| PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。 |
不过对于PROPAGATION_NESTED,实际上MySQL的InnoDB不支持真正的嵌套事务,InnoDB只支持单层事务,PROPAGATION_NESTED在MySQL中通过SAVEPOINT(保存点)来模拟,下面是一个例子。
START TRANSACTION;
INSERT INTO orders VALUES (1, 'item1');
SAVEPOINT sp1;
INSERT INTO orders VALUES (2, 'item2');
-- 回滚到保存点
ROLLBACK TO SAVEPOINT sp1;
COMMIT;
关于Spring-tx相关依赖
SpringBoot工程中,spring-tx通常会被spring-boot-starter-data-jpa、spring-boot-starter-data-jdbc、mybatis-spring-boot-starter等和数据库操作相关的Starter依赖自动引入,因此我们无需手动额外引入Maven依赖。
但如果你是手动搭建的传统Spring工程,使用事务管理器需要显式引入SpringTx的依赖,此时可以添加如下配置。这里假设我们使用的是Spring6版本,SpringTx的版本号一般与Spring框架保持一致。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>6.2.17</version>
</dependency>
配置事务管理器
spring-tx中,事务是被事务管理器TransactionManager维护的。不过在SpringBoot工程中,无论是2.x版本还是最新的3.x版本,我们在非多数据源情况下通常不需要通过JavaConfig编程式注册事务管理器,SpringBoot会进行自动装配,事务管理器默认就是配置好的。但这里我们出于演示目的还是手动编写一些配置代码,来学习事务管理器是如何被创建的。
对于使用Spring的JdbcTemplate、MyBatis等场景,我们需要配置的事务管理器Bean是DataSourceTransactionManager,至于SpringDataJPA有点特殊,它需要JpaTransactionManager,不过其实所有事务管理器都会实现PlatformTransactionManager接口,总而言之我们需要配置一个框架需要的事务管理器。
package com.gacfox.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
@Configuration
public class TransactionManagerConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
transactionManager.setDefaultTimeout(-1);
return transactionManager;
}
}
实例化事务管理器需要数据源DataSource对象,这里假设我们在其他地方已经注册这个Bean了,此外我们还配置了事务的超时时间为-1,表示永久等待不设置超时时间,这也是事务管理器的默认行为。
实际上,正如前面所说,SpringBoot中通常我们并不需要手动配置事务管理器,它是自动装配的。但如果你使用JavaConfig手动配置了多数据源情况就不同了,一种常见的错误是事务管理器只被自动配置到了主数据源上,非主数据源使用事务时虽然代码没报错但事务实际也没生效。
编程式事务管理
编程式事务管理需要在业务代码中显式地编写事务控制逻辑,它虽然代码侵入性较强,但灵活性最高、事务的管理最直观。编程式事务管理虽然写法比声明式事务复杂,但在很多项目中编程式事务管理是推荐的开发方式。
PlatformTransactionManager API
前面我们向Spring注册过事务管理器,PlatformTransactionManager其实就是Spring事务管理的核心接口,基于事务管理器的API我们可以获得最底层的事务控制能力。下面例子代码中,我们使用PlatformTransactionManager手动控制事务的开启、提交和回滚。
package com.gacfox.demo.service;
import com.gacfox.demo.model.Account;
import com.gacfox.demo.repository.AccountRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import java.math.BigDecimal;
@Service
@RequiredArgsConstructor
public class AccountService {
private final AccountRepository accountRepository;
private final PlatformTransactionManager transactionManager;
public void transfer(Long fromId, Long toId, BigDecimal amount) {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(def);
try {
Account from = accountRepository.findById(fromId)
.orElseThrow(() -> new RuntimeException("转出账户不存在: " + fromId));
Account to = accountRepository.findById(toId)
.orElseThrow(() -> new RuntimeException("转入账户不存在: " + toId));
if (from.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
accountRepository.save(from);
accountRepository.save(to);
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
}
直接使用PlatformTransactionManager需要手动创建DefaultTransactionDefinition,它是一个包含事务隔离级别和传播属性的配置类。调用transactionManager.getTransaction()时会真正开启事务,事务操作执行完成后调用transactionManager.commit()提交事务,如果中途出错,调用transactionManager.rollback()回滚事务。
使用TransactionTemplate
上面代码我们可以看到,直接使用PlatformTransactionManager和直接用JDBC控制事务也差不多,代码仍非常繁琐,实际上Spring提供了TransactionTemplate类能够进一步简化代码编写。将上面代码改为TransactionTemplate写法如下。
package com.gacfox.demo.service;
import com.gacfox.demo.model.Account;
import com.gacfox.demo.repository.AccountRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;
@Service
@RequiredArgsConstructor
public class AccountService {
private final AccountRepository accountRepository;
private final TransactionTemplate transactionTemplate;
public void transfer(Long fromId, Long toId, BigDecimal amount) {
transactionTemplate.executeWithoutResult(status -> {
Account from = accountRepository.findById(fromId)
.orElseThrow(() -> new RuntimeException("转出账户不存在: " + fromId));
Account to = accountRepository.findById(toId)
.orElseThrow(() -> new RuntimeException("转入账户不存在: " + toId));
if (from.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
accountRepository.save(from);
accountRepository.save(to);
});
}
}
TransactionTemplate的executeWithoutResult()方法可以接收一个Lambda表达式作为参数,如果回调中抛出未受检异常,事务会自动回滚;如果需要手动触发回滚,可以调用status.setRollbackOnly()。
此外,如果被TransactionTemplate包裹的Lambda表达式没有返回值,调用executeWithoutResult()方法即可,如果有返回值则可以调用execute(),Lambda表达式的返回值会被作为execute()的返回值供我们取用。
使用声明式事务
声明式事务也是实际开发中常用的方式,声明式事务是基于Spring的AOP实现,我们在需要启用事务的类或方法上标注@Transactional注解就行了。
@Service("testService")
@Transactional(rollbackFor = Exception.class)
public class TestServiceImpl implements TestService {
// ... 具体实现相关代码
}
代码中,我们使用了@Transactional标注了一个类,这标明其方法执行时,默认就会以声明的事务隔离级别和传播属性执行。rollbackFor = Exception.class指定抛出任何异常时回滚事务。
除了rollbackFor,这个注解还有一些其它参数可用:
| 属性 | 功能说明 |
|---|---|
| value | 指定事务管理器Bean名称,与transactionManager互为别名 |
| transactionManager | 同value,用来指定事务管理器Bean名称,多数据源场景常用 |
| propagation | 设置事务传播行为,控制嵌套调用时事务的传递规则,默认REQUIRED |
| isolation | 设置事务隔离级别,控制并发事务可见性,默认DEFAULT(使用数据库默认隔离级别) |
| readOnly | 是否开启只读事务,true仅查询、数据库优化,禁止增删改;默认false |
| timeout | 事务超时时间,单位秒,超时自动回滚;默认-1不限制 |
| rollbackFor | 指定触发回滚的异常类型,可指定多个;默认只回滚运行时异常 |
| rollbackForClassName | 按异常全限定类名字符串指定回滚异常,作用同rollbackFor |
| noRollbackFor | 指定不触发回滚的异常类型 |
| noRollbackForClassName | 按异常全限定类名字符串指定不回滚异常 |
@Transactional注解可以标注在类上(对所有public方法生效)或方法上,方法上的配置优先级高于类上的配置。