在 Spring 应用中,无论使用 JdbcTemplate
还是 JPA/Hibernate 或 Spring Data JPA,都需要处理数据库事务。
数据库事务是一个事务单元,它要么全部完成,要么都不完成,并使数据库处于一致状态。在实现数据库事务时,需要考虑到 ACID(原子性、一致性、隔离性、持久性)属性。
让我们了解一下如何在 Spring Boot 应用中处理数据库事务。
使用 JDBC 进行事务处理 {#使用-jdbc-进行事务处理}
首先,让我们快速了解一下我们通常是如何在普通 JDBC 中处理数据库事务的。
class UserService {
void register(User user) {
String sql = "...";
Connection conn = dataSource.getConnection(); // <1>
try(conn) { // <6>
conn.setAutoCommit(false); // <2>
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, user.getEmail());
pstmt.setString(2, user.getPassword());
pstmt.executeUpdate(); // <3>
conn.commit(); // <4>
} catch(SQLException e) {
conn.rollback(); // <5>
}
}
}
在上述代码片段中:
<1>
我们从数据源连接池中获取了一个数据库连接。<2>
默认情况下,每个数据库操作都会自动在自己的事务中运行。为了自己控制事务,我们将AutoCommit
设置为false
。<3>
然后,我们执行了必要的数据库操作。<4>
如果一切OK,则提交事务。<5>
如果抛出了异常,则回滚事务。<6>
由于我们使用的是 try-with-resources 语法,连接将自动关闭。
在该事务处理的示例中,存在大量的模板代码。
Spring 通过 AOP 简化了数据库事务处理机制。我们可以使用 @Transactional
注解以声明式或使用 TransactionTemplate
以编程方式实现数据库事务处理。
Spring 声明式事务处理 - @Transactional
{#spring-声明式事务处理---transactional}
通常,在三层分层架构中,web 层有 controller,业务逻辑层有 service,数据访问层有 repository。
一般来说,"事务单元" 以 service 方法为模型,并被视为 "事务边界"。
我们可以在 Service 层方法上添加 Spring 的 @Transactional
注解来定义事务范围。该方法中的所有数据库操作都将在一个事务中运行。如果方法执行成功,则事务将被提交。如果在方法执行过程中出现任何 RuntimeException
异常,则事务将被回滚。
@Service
class UserService {
private final JdbcTemplate jdbcTemplate;
@Transactional
void register(User user) {
String sql = "...";
jdbcTemplate.update(sql);
}
}
你可以在方法级别添加 @Transactional
注解,使该特定方法具有事务性;也可以在类级别添加 @Transactional
注解,使该类中的所有 public
方法都具有事务性。
当你在类或其任何方法上添加 @Transactional
注解时,Spring 会为该类创建一个代理,使用 Spring AOP 来执行事务处理逻辑。
添加 @Transactional
注解时,默认情况下:
- 传播(propagation)设置为
REQUIRED
,即参与当前事务(如果存在)或启动新事务。 - 如果方法抛出任何
RuntimeException
,则回滚事务。 - 如果方法抛出任何受检异常(非 runtime 异常),则不会回滚事务。
你可以通过如下方式指定 @Transactional
属性来自定义这种行为:
@Service
class UserService {
private final JdbcTemplate jdbcTemplate;
@Transactional(
propagation = Propagation.REQUIRES_NEW,
rollbackFor = DuplicateUserException.class,
noRollbackFor = IllegalArgumentException.class)
void register(User user) throws DuplicateUserException {
String sql = "...";
jdbcTemplate.update(sql);
}
}
class DuplicateUserException extends Exception {}
在上面的代码片段中,我们将传播级别指定为 REQUIRES_NEW
,而不是默认的 REQUIRED
。REQUIRES_NEW
传播级别会暂停当前事务(如果存在),启动一个新事务,执行数据库操作,提交事务,然后恢复先前暂停的事务。
此外,我们还指定了如果出现 DuplicateUserException
,即使它是一个受检异常,也要回滚事务。以及,如果出现 IllegalArgumentException
(即使它是一个 RuntimeException
),也不回滚事务。
Spring 编程式事务处理 - TransactionTemplate
{#spring-编程式事务处理---transactiontemplate}
Spring 还提供了一种编程式事务处理,即使用 TransactionTemplate
。具体如下:
@Service
class UserService {
private final TransactionTemplate transactionTemplate;
void register(User user) {
transactionTemplate.execute(status -> {
String sql = "...";
jdbcTemplate.update(sql);
// 如果异常,则回滚
//status.setRollbackOnly();
return result;
});
}
}
如果想更精准地控制事务,可以使用 TransactionTemplate
。
使用 @Transactional
注解时的常见错误 {#使用-transactional-注解时的常见错误}
我们需要了解代理是如何工作的,以免事务配置出错。
让我们以 controller 调用事务方法为例,该方法又会调用其他事务方法。
UserController --> UserService --> AccountService
假设我们的用户注册流程如下:
@Controller
class UserController {
private final UserService userService;
@PostMapping("/register")
String register() {
User user = ...;
userService.register(user);
return "...";
}
}
//--------------------------------------------------------------
@Service
class UserService {
private final UserRepository userRepository;
private final AccountService accountService;
@Transactional
public void register(User user) {
userRepository.save(user);
Account account = ...;
accountService.create(account);
UserPreferences preferences = ...;
userRepository.savePreferences(preferences);
}
}
//--------------------------------------------------------------
@Service
class AccountService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void create(Account account) {
...
}
}
在上面的示例中,我们从 UserController
调用 UserService.register(...)
方法,该方法使用了 @Transactional
注解。由于没有新事务正在进行,因此将创建一个新事务。用户详细信息将在当前事务中保存到数据库中。之后,我们将调用 AccountService.create(account)
,它注解了 @Transactional(propagation = Propagation.REQUIRES_NEW)
。现在,正在进行的当前事务将被暂时中止,一个新事务将被启动。账户创建将在新事务中执行,如果没有异常发生,该事务将立即提交。然后恢复前一个事务并保存用户首选项(UserPreferences)。
如果在保存首选项时出现 RuntimeException
异常,那么只有"用户记录插入"会回滚,但"账户创建"不会,因为"账户创建"发生在一个已成功提交的单独事务中。
这将如期运行。
但是,假设"账户创建"方法也在 UserService
中,如下所示:
@Controller
class UserController {
private final UserService userService;
@PostMapping("/register")
String register() {
User user = ...;
userService.register(user);
return "...";
}
}
//--------------------------------------------------------------
@Service
class UserService {
private final UserRepository userRepository;
@Transactional
public void register(User user) {
userRepository.save(user);
Account account = ...;
this.create(account);
UserPreferences preferences = ...;
userRepository.savePreferences(preferences);
}
// 账户创建,和 register() 方法在同一个类中
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void create(Account account) {
...
}
}
我们配置了 REQUIRES_NEW
传播级别,我们可能会认为 "账户创建" 逻辑将在新事务中执行,但这是错误的。
在 UserService
上添加 @Transactional
注解时,会创建一个代理,并将该代理注入到 UserController
中。从 UserController
调用 userService.register(...)
方法时,就是在调用代理的 register()
方法。代理将根据 @Transactional
语义应用事务处理逻辑,然后将实际逻辑的执行委托给实际的 UserService
实例。
现在,在 UserService.register()
方法中调用 create(account)
方法时,它将是对实际 UserService
对象(非代理对象)的本地方法调用,而实际 UserService
对象感知不到事务处理。
在上述情况下,create(account)
逻辑也将在调用 UserService.register()
方法时创建的同一个父事务中执行。
因此,如果要调用一个具有不同事务语义的方法,最好的办法是将该方法移到一个单独的类中。也有其他方法,比如使用自注入,但不推荐使用这种方法。
参考:https://www.sivalabs.in/spring-boot-database-transaction-management-tutorial/