声明式事务控制

关于事务

理解事务

事务是一组操作的执行单元,相对于数据库操作来讲,事务管理的是一组SQL指令,比如增加,修改,删除等

事务的一致性,要求,这个事务内的操作必须全部执行成功,如果在此过程种出现了差错,比如有一条SQL语句没有执行成功,那么这一组操作都将全部回滚

事务由事务开始和事务结束之间执行的全部数据库操作组成。

事务四大特性

原子性A:事务是不可分割的最小操作单元,要么全成功,要么全失败

一致性C:事务完成时,必须所据都保持一致状态

隔离性I:数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下进行

持久性D:事务一旦提交或回滚,他对数据库的改变是永久的

声明式事务控制

理解声明式事务

Spring 对事务逻辑的代码有封装,在配置文件中或者注解管理即可实现相关操作

Spring 的声明式事务顾名思义就是采用声明的方式来处理事务。这里所说的声明,就是指在配置文件中声明,用在Spring 配置文件中声明式的处理事务来代替代码式的处理事务

声明式事务处理的作用

  • 事务管理不侵入开发的组件。具体来说,业务逻辑对象就不会意识到正在事务管理之中,事实上也应该如此,因为事务管理是属于系统层面的服务,而不是业务逻辑的一部分,如果想要改变事务管理策划的话,也只需要在定义文件中重新配置即可
  • 在不需要事务管理的时候,只要在设定文件上修改一下,即可移去事务管理服务,无需改变代码重新编译,这样维护起来极其方便

Spring提供了一个PlatformTransactionManager来表示事务管理器,所有的事务都由它负责管理。而事务由TransactionStatus表示。如果手写事务代码,使用try...catch如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
TransactionStatus tx = null;
try {
// 开启事务:
tx = txManager.getTransaction(new DefaultTransactionDefinition());
// 相关JDBC操作:
jdbcTemplate.update("...");
jdbcTemplate.update("...");
// 提交事务:
txManager.commit(tx);
} catch (RuntimeException e) {
// 回滚事务:
txManager.rollback(tx);
throw e;
}

Spring为啥要抽象出PlatformTransactionManagerTransactionStatus?原因是JavaEE除了提供JDBC事务外,它还支持分布式事务JTA(Java Transaction API)。分布式事务是指多个数据源(比如多个数据库,多个消息系统)要在分布式环境下实现事务的时候,应该怎么实现。分布式事务实现起来非常复杂,简单地说就是通过一个分布式事务管理器实现两阶段提交,但本身数据库事务就不快,基于数据库事务实现的分布式事务就慢得难以忍受,所以使用率不高。

Spring为了同时支持JDBC和JTA两种事务模型,就抽象出PlatformTransactionManager。因为我们的代码只需要JDBC事务,因此,在AppConfig中,需要再定义一个PlatformTransactionManager对应的Bean,它的实际类型是DataSourceTransactionManager

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@ComponentScan
@PropertySource("jdbc.properties")
public class AppConfig {
...
@Bean
PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}

Spring 声明式事务控制底层就是AOP

编程式:自己写代码实现功能

声明式:通过配置让框架实现框架实现

编程式事务控制

相关对象

PlatformTransactionManager

PlatformTransactionManager 接口是 spring 的事务管理器,它里面提供了我们常用的操作事务的方法。例如获取事务状态、提交事务和回滚事务等。

img

PlatformTransactionManager是接口类型,不同的 Dao 层技术则有不同的实现类

例如:

  • Dao 层技术是jdbc 或 mybatis 时:org.springframework.jdbc.datasource.DataSourceTransactionManager

  • Dao 层技术是hibernate时:org.springframework.orm.hibernate5.HibernateTransactionManager

TransactionDefinition

TransactionDefinition 是事务的定义信息对象,包含了事务的隔离级别、传播行为、超时时间和是否只读等信息。

在这里插入图片描述
TransactionStatus

TransactionStatus 接口提供的是事务具体的运行状态,方法介绍如下。

在 Spring 的编程式事务管理里,TransactionStatus 对象由 PlatformTransactionManagergetTransaction() 方法返回。它记录了事务的当前状态,例如事务是否为新开启的、是否已完成等,并且能让开发者对事务进行控制,比如提交或者回滚事务。

常用方法

  1. isNewTransaction()
  • 功能:判断当前事务是否为新开启的事务。若返回 true,表示该事务是在此次调用 getTransaction() 方法时新开启的;若返回 false,则说明当前事务是已存在事务的一部分(比如在事务传播行为是 REQUIRED 时,加入了已有的事务)。
  1. hasSavepoint()
  • 功能:判断当前事务是否有保存点。保存点是事务中的一个标记,可让事务部分回滚到该标记处,而非整个事务回滚。
  1. setRollbackOnly()
  • 功能:将当前事务标记为仅回滚。一旦调用此方法,事务在结束时会被强制回滚,即便后续代码尝试提交事务也不会生效。
  1. isRollbackOnly()
  • 功能:检查当前事务是否已被标记为仅回滚。
  1. isCompleted()
  • 功能:判断事务是否已经完成,即是否已经提交或者回滚。

声明式事务控制说明

使用编程的方式使用Spring事务仍然比较繁琐,更好的方式是通过声明式事务来实现。使用声明式事务非常简单,除了在AppConfig中追加一个上述定义的PlatformTransactionManager外,再加一个@EnableTransactionManagement就可以启用声明式事务:

1
2
3
4
5
6
7
@Configuration
@ComponentScan
@EnableTransactionManagement // 启用声明式
@PropertySource("jdbc.properties")
public class AppConfig {
...
}

然后,对需要事务支持的方法,加一个@Transactional注解:

1
2
3
4
5
6
7
8
@Component
public class UserService {
// 此public方法自动具有事务支持:
@Transactional
public User register(String email, String password, String name) {
...
}
}

或者更简单一点,直接在Bean的class处加上,表示所有public方法都具有事务支持:

1
2
3
4
5
@Component
@Transactional
public class UserService {
...
}

事务隔离级别

SQL 标准定义了四种隔离级别,MySQL 全都支持。这四种隔离级别分别是:

  1. 读未提交(READ UNCOMMITTED)
  2. 读提交 (READ COMMITTED)
  3. 可重复读 (REPEATABLE READ)
  4. 串行化 (SERIALIZABLE)

从上往下,隔离强度逐渐增强,性能逐渐变差。采用哪种隔离级别要根据系统需求权衡决定,其中,可重复读是 MySQL 的默认级别。

事务隔离其实就是为了解决上面提到的脏读、不可重复读、幻读这几个问题,下面展示了 4 种隔离级别对这三个问题的解决程度

img

事务传播行为

  • REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。一般的选择(默认值)

  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务)

  • MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常

  • REQUERS_NEW:新建事务,如果当前在事务中,把当前事务挂起。

  • NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起

  • NEVER:以非事务方式运行,如果当前存在事务,抛出异常

  • NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行 REQUIRED 类似的操作

  • 超时时间:默认值是-1,没有超时限制。如果有,以秒为单位进行设置

  • 是否只读:建议查询时设置为只读

示例

假设我们有一个简单的用户账户表,需要进行转账操作。

我们需要配置 Spring 的上下文和数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
</bean>

<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<!-- 配置 JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>

<!-- 配置业务服务 -->
<bean id="accountService" class="com.example.AccountService">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
<property name="transactionManager" ref="transactionManager"/>
</bean>
</beans>

然后,我们创建一个 AccountService 类来处理转账业务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.example;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

public class AccountService {

private JdbcTemplate jdbcTemplate;
private PlatformTransactionManager transactionManager;

public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}

public void transferMoney(int fromAccountId, int toAccountId, double amount) {
// 定义事务属性
TransactionDefinition def = new DefaultTransactionDefinition();
// 获取事务状态
TransactionStatus status = transactionManager.getTransaction(def);

try {
// 减少转出账户的余额
jdbcTemplate.update("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromAccountId);

// 模拟异常
// int result = 1 / 0;

// 增加转入账户的余额
jdbcTemplate.update("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toAccountId);

// 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 回滚事务
transactionManager.rollback(status);
e.printStackTrace();
}
}
}

最后,我们可以编写一个测试类来调用 AccountServicetransferMoney 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
public static void main(String[] args) {
// 加载 Spring 上下文
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

// 获取 AccountService 实例
AccountService accountService = (AccountService) context.getBean("accountService");

// 执行转账操作
accountService.transferMoney(1, 2, 100.0);
}
}

在这个示例中,我们使用 PlatformTransactionManagerTransactionDefinition 来手动管理事务。在 transferMoney 方法中,我们首先获取事务状态,然后执行数据库操作,如果操作过程中出现异常,我们会回滚事务,否则提交事务。

基于 XML 的声明式事务控制

Spring 的声明式事务顾名思义就是采用声明的方式来处理事务。这里所说的声明,就是指在配置文件中声明,用在 Spring 配置文件中声明式的处理事务来代替代码式的处理事务。

声明式事务控制明确事项:

  • 谁是切点?
  • 谁是通知?
  • 配置切面?

配置事务管理器与命名空间

applicationContext.xml 中添加事务相关命名空间及事务管理器配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<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"
xmlns:aop="http://www.springframework.org/schema/aop"
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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- 数据源配置(同上) -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
</bean>

<!-- 事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<!-- JdbcTemplate 配置(同上) -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>

<!-- 业务服务 -->
<bean id="accountService" class="com.example.AccountService">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>

<!-- 声明式事务配置 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 配置事务属性:方法名匹配规则 -->
<tx:method name="transferMoney" propagation="REQUIRED" isolation="DEFAULT"
timeout="-1" read-only="false" rollback-for="Exception"/>
</tx:attributes>
</tx:advice>

<!-- AOP 切面配置 -->
<aop:config>
<!-- 定义切点:匹配业务层方法 -->
<aop:pointcut id="servicePointcut"
expression="execution(* com.example.AccountService.*(..))"/>
<!-- 将事务通知与切点关联 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="servicePointcut"/>
</aop:config>
</beans>
配置项 作用
<tx:advice> 定义事务通知,指定事务管理器和事务属性
<tx:method> 配置具体方法的事务规则(如传播行为、隔离级别)
<aop:pointcut> 定义切点,匹配需要事务管理的方法
<aop:advisor> 将事务通知与切点关联,实现声明式事务控制

移除 AccountService 中的 PlatformTransactionManager 依赖和手动事务管理代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example;

import org.springframework.jdbc.core.JdbcTemplate;

public class AccountService {
private JdbcTemplate jdbcTemplate;

public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public void transferMoney(int fromAccountId, int toAccountId, double amount) {
// 减少转出账户的余额
jdbcTemplate.update("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromAccountId);

// 模拟异常(测试回滚)
// int result = 1 / 0;

// 增加转入账户的余额
jdbcTemplate.update("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toAccountId);
}
}

事务属性详解

<tx:method> 标签中配置事务规则:

属性 说明
name 匹配的方法名(支持通配符,如 * 表示所有方法)
propagation 事务传播行为(默认 REQUIRED
isolation 事务隔离级别(默认 DEFAULT,使用数据库默认隔离级别)
timeout 事务超时时间(单位秒,默认 -1 表示不超时)
read-only 是否只读事务(默认 false,查询操作建议设为 true
rollback-for 触发回滚的异常类型(如 Exception 表示所有异常均回滚)
no-rollback-for 不触发回滚的异常类型

编写测试类验证事务是否生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
AccountService accountService = (AccountService) context.getBean("accountService");

try {
// 正常转账(事务提交)
accountService.transferMoney(1, 2, 100.0);

// 模拟异常(事务回滚)
// accountService.transferMoney(1, 2, 100.0); // 内部有 int result = 1 / 0;
} catch (Exception e) {
System.err.println("事务回滚:" + e.getMessage());
}
}
}

基于注解的声明式事务控制

注解的方式,只需在方法上面加一个@Transaction注解,那么方法执行之前spring会自动开启一个事务,方法执行完毕之后,会自动提交或者回滚事务,而方法内部没有任何事务相关代码,用起来特别的方便。

将会通过如下完整示例进行说明

首先创建数据库表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 银行账户表
CREATE TABLE IF NOT EXISTS t_account (
id INT PRIMARY KEY AUTO_INCREMENT,
account_no VARCHAR(20) NOT NULL UNIQUE,
account_name VARCHAR(50) NOT NULL,
balance DECIMAL(10,2) NOT NULL DEFAULT 0.00,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 交易记录表
CREATE TABLE IF NOT EXISTS t_transaction (
id INT PRIMARY KEY AUTO_INCREMENT,
from_account_id INT NOT NULL,
to_account_id INT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
transaction_time DATETIME DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) NOT NULL,
remark VARCHAR(200),
FOREIGN KEY (from_account_id) REFERENCES t_account(id),
FOREIGN KEY (to_account_id) REFERENCES t_account(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

创建实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 实体类 账户
package edu.software.ergoutree.spring6jdbc.affairs.entity;

import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
public class Account {
private Integer id;
private String accountNo;
private String accountName;
private BigDecimal balance;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 实体类 表示一次交易
package edu.software.ergoutree.spring6jdbc.affairs.entity;

import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
public class Transaction {
private Integer id;
private Integer fromAccountId;
private Integer toAccountId;
private BigDecimal amount;
private LocalDateTime transactionTime;
private String status;
private String remark;
}

创建 DAO 层

1
2
3
4
5
6
7
8
9
10
11
package edu.software.ergoutree.spring6jdbc.affairs.dao;

import edu.software.ergoutree.spring6jdbc.affairs.entity.Account;
import java.math.BigDecimal;
// 账户数据访问对象的接口
public interface AccountDao {
Account findById(Integer id); // 根据账户 ID 查询账户信息
Account findByAccountNo(String accountNo); // 根据账户号码查询账户信息
void updateBalance(Integer id, BigDecimal balance); // 更新指定账户的余额
void save(Account account); // 保存新的账户信息到数据库
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 实现了 AccountDao 接口,使用 JdbcTemplate 与数据库进行交互。
package edu.software.ergoutree.spring6jdbc.affairs.dao;

import edu.software.ergoutree.spring6jdbc.affairs.entity.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.math.BigDecimal;

@Repository
public class AccountDaoImpl implements AccountDao {

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
public Account findById(Integer id) {
String sql = "SELECT * FROM t_account WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), id);
}

@Override
public Account findByAccountNo(String accountNo) {
String sql = "SELECT * FROM t_account WHERE account_no = ?";
// 将查询结果映射到 Account 实体类。
return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), accountNo);
}

@Override
public void updateBalance(Integer id, BigDecimal balance) {
String sql = "UPDATE t_account SET balance = ? WHERE id = ?";
jdbcTemplate.update(sql, balance, id);
}

@Override
public void save(Account account) {
String sql = "INSERT INTO t_account (account_no, account_name, balance) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, account.getAccountNo(), account.getAccountName(), account.getBalance());
}
}

创建 Service 层

1
2
3
4
5
6
7
8
9
10
11
package edu.software.ergoutree.spring6jdbc.affairs.service;

import edu.software.ergoutree.spring6jdbc.affairs.entity.Account;
import java.math.BigDecimal;

// 定义了账户服务的接口
public interface AccountService {
void transfer(String fromAccountNo, String toAccountNo, BigDecimal amount); // 转账
Account createAccount(Account account); // 新建账户
Account getAccount(String accountNo); // 根据账户号码查询账户信息
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package edu.software.ergoutree.spring6jdbc.affairs.service;

import edu.software.ergoutree.spring6jdbc.affairs.dao.AccountDao;
import edu.software.ergoutree.spring6jdbc.affairs.entity.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Service
public class AccountServiceImpl implements AccountService {

@Autowired
private AccountDao accountDao;

@Override
// @Transactional 用于声明事务的属性
@Transactional(
propagation = Propagation.REQUIRED,
rollbackFor = Exception.class, // 任何异常都会触发回滚。
timeout = 30
)
public void transfer(String fromAccountNo, String toAccountNo, BigDecimal amount) {
// 查询转出账户
Account fromAccount = accountDao.findByAccountNo(fromAccountNo);
if (fromAccount == null) {
throw new RuntimeException("转出账户不存在");
}

// 查询转入账户
Account toAccount = accountDao.findByAccountNo(toAccountNo);
if (toAccount == null) {
throw new RuntimeException("转入账户不存在");
}

// 检查余额是否足够
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("账户余额不足");
}

// 执行转账
BigDecimal fromBalance = fromAccount.getBalance().subtract(amount);
BigDecimal toBalance = toAccount.getBalance().add(amount);

// 更新账户余额
accountDao.updateBalance(fromAccount.getId(), fromBalance);
accountDao.updateBalance(toAccount.getId(), toBalance);
}

@Override
@Transactional(propagation = Propagation.REQUIRED)
public Account createAccount(Account account) {
accountDao.save(account);
return account;
}

@Override
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public Account getAccount(String accountNo) {
return accountDao.findByAccountNo(accountNo);
}
}

我们对需使用事务的目标上加@Transaction注解

  • @Transaction放在接口上,那么接口的实现类中所有public都被spring自动加上事务
  • @Transaction放在类上,那么当前类以及其下无限级子类中所有pubilc方法将被spring自动加上事务
  • @Transaction放在public方法上,那么该方法将被spring自动加上事务
  • 注意:@Transaction只对public方法有效

下面我们看一下@Transactional源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

/**
* 指定事务管理器的bean名称,如果容器中有多事务管理器PlatformTransactionManager,
* 那么你得告诉spring,当前配置需要使用哪个事务管理器
*/
@AliasFor("transactionManager")
String value() default "";

/**
* 同value,value和transactionManager选配一个就行,也可以为空,如果为空,默认会从容器中按照类型查找一个事务管理器bean
*/
@AliasFor("value")
String transactionManager() default "";

/**
* 事务的传播属性
*/
Propagation propagation() default Propagation.REQUIRED;

/**
* 事务的隔离级别,就是制定数据库的隔离级别,数据库隔离级别大家知道么?不知道的可以去补一下
*/
Isolation isolation() default Isolation.DEFAULT;

/**
* 事务执行的超时时间(秒),执行一个方法,比如有问题,那我不可能等你一天吧,可能最多我只能等你10秒
* 10秒后,还没有执行完毕,就弹出一个超时异常吧
*/
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

/**
* 是否是只读事务,比如某个方法中只有查询操作,我们可以指定事务是只读的
* 设置了这个参数,可能数据库会做一些性能优化,提升查询速度
*/
boolean readOnly() default false;

/**
* 定义零(0)个或更多异常类,这些异常类必须是Throwable的子类,当方法抛出这些异常及其子类异常的时候,spring会让事务回滚
* 如果不配做,那么默认会在 RuntimeException 或者 Error 情况下,事务才会回滚
*/
Class<? extends Throwable>[] rollbackFor() default {};

/**
* 和 rollbackFor 作用一样,只是这个地方使用的是类名
*/
String[] rollbackForClassName() default {};

/**
* 定义零(0)个或更多异常类,这些异常类必须是Throwable的子类,当方法抛出这些异常的时候,事务不会回滚
*/
Class<? extends Throwable>[] noRollbackFor() default {};

/**
* 和 noRollbackFor 作用一样,只是这个地方使用的是类名
*/
String[] noRollbackForClassName() default {};

}

参数介绍

参数 描述
value 指定事务管理器的bean名称,如果容器中有多事务管理器PlatformTransactionManager,那么你得告诉spring,当前配置需要使用哪个事务管理器
transactionManager 同value,value和transactionManager选配一个就行,也可以为空,如果为空,默认会从容器中按照类型查找一个事务管理器bean
propagation 事务的传播属性,下篇文章详细介绍
isolation 事务的隔离级别,就是制定数据库的隔离级别,数据库隔离级别大家知道么?不知道的可以去补一下
timeout 事务执行的超时时间(秒),执行一个方法,比如有问题,那我不可能等你一天吧,可能最多我只能等你10秒 10秒后,还没有执行完毕,就弹出一个超时异常吧
readOnly 是否是只读事务,比如某个方法中只有查询操作,我们可以指定事务是只读的 设置了这个参数,可能数据库会做一些性能优化,提升查询速度
rollbackFor 定义零(0)个或更多异常类,这些异常类必须是Throwable的子类,当方法抛出这些异常及其子类异常的时候,spring会让事务回滚 如果不配做,那么默认会在 RuntimeException 或者 Error 情况下,事务才会回滚
rollbackForClassName 同 rollbackFor,只是这个地方使用的是类名
noRollbackFor 定义零(0)个或更多异常类,这些异常类必须是Throwable的子类,当方法抛出这些异常的时候,事务不会回滚
noRollbackForClassName 同 noRollbackFor,只是这个地方使用的是类名

通过 @Transactional 注解,Spring 会自动管理事务的开始、提交和回滚。在@Transaction标注类或者目标方法上执行业务操作,此时这些方法会自动被spring进行事务管理

在 transfer 方法中,如果在转账过程中出现任何异常,事务会自动回滚,确保数据的一致性

下面讲解一些出现的注解:

  • @Transactional注解 :用于声明事务的属性。
    • propagation:事务传播行为
      • Propagation.REQUIRED 表示如果当前没有事务,则创建一个新事务;如果已经存在一个事务,则加入该事务。
      • Propagation.SUPPORTS 表示如果当前存在事务,则加入该事务;如果不存在事务,则以非事务方式执行。
    • rollbackFor:指定哪些异常会触发事务回滚,这里指定为 Exception.class 表示任何异常都会触发回滚。
    • timeout:指定事务的超时时间,单位为秒。
    • readOnly:指定事务是否为只读事务,用于提高查询性能。
    • isolation :指定事务的隔离级别

扩展事务功能

嵌套事务与传播行为 REQUIRES_NEW

场景: 日志记录操作需要独立事务,不受主事务回滚影响。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class LogService {
@Autowired
private JdbcTemplate jdbcTemplate;

// 独立事务:即使转账失败,日志仍需记录
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logTransaction(String fromAccountNo, String toAccountNo, BigDecimal amount) {
String sql = "INSERT INTO t_transaction (from_account_id, to_account_id, amount, status) VALUES (?, ?, ?, ?)";
jdbcTemplate.update(sql, fromAccountNo, toAccountNo, amount, "PROCESSING");
}
}

// 修改 AccountServiceImpl 的转账方法
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void transfer(String fromAccountNo, String toAccountNo, BigDecimal amount) {
// ...原有逻辑...

// 记录交易日志(独立事务)
logService.logTransaction(fromAccountNo, toAccountNo, amount);

// 模拟异常(主事务回滚,但日志记录成功)
// int a = 1 / 0;
}

控制隔离级别和只读事务的配置

1
2
3
4
5
6
7
8
9
10
11
// 控制隔离级别解决脏读问题
@Transactional(isolation = Isolation.READ_COMMITTED)
public BigDecimal getAccountBalance(String accountNo) {
return accountDao.findByAccountNo(accountNo).getBalance();
}

// 只读事务优化查询
@Transactional(readOnly = true)
public List<Transaction> getTransactionHistory(String accountNo) {
return accountDao.getTransactionHistory(accountNo);
}

注解优先级和类级的配置

1
2
3
4
5
6
7
8
@Service
@Transactional(isolation = Isolation.READ_COMMITTED, timeout = 10) // 类级默认配置
public class AccountServiceImpl implements AccountService {

@Override
@Transactional(isolation = Isolation.SERIALIZABLE, timeout = 30) // 方法级覆盖
public void highConcurrencyTransfer(...) { ... }
}

若容器中存在多个事务管理器,需通过 transactionManager 指定:

1
2
@Transactional(transactionManager = "orderTransactionManager")
public void createOrder(...) { ... }

新建一个测试类测试上述代码能否正常执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package edu.software.ergoutree.spring6jdbc.affairs;

import edu.software.ergoutree.spring6jdbc.affairs.entity.Account;
import edu.software.ergoutree.spring6jdbc.affairs.service.AccountService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
public class AccountServiceTest {

@Autowired
private AccountService accountService;

@Test
public void testTransfer() {
// 创建两个账户
Account account1 = new Account();
account1.setAccountNo("1001");
account1.setAccountName("张三");
account1.setBalance(new BigDecimal("1000.00"));
accountService.createAccount(account1);

Account account2 = new Account();
account2.setAccountNo("1002");
account2.setAccountName("李四");
account2.setBalance(new BigDecimal("500.00"));
accountService.createAccount(account2);

// 执行转账
accountService.transfer("1001", "1002", new BigDecimal("200.00"));

// 验证转账结果
Account updatedAccount1 = accountService.getAccount("1001");
Account updatedAccount2 = accountService.getAccount("1002");

assertEquals(new BigDecimal("800.00"), updatedAccount1.getBalance());
assertEquals(new BigDecimal("700.00"), updatedAccount2.getBalance());
}

@Test
public void testTransferWithInsufficientBalance() {
// 创建两个账户
Account account1 = new Account();
account1.setAccountNo("1003");
account1.setAccountName("王五");
account1.setBalance(new BigDecimal("100.00"));
accountService.createAccount(account1);

Account account2 = new Account();
account2.setAccountNo("1004");
account2.setAccountName("赵六");
account2.setBalance(new BigDecimal("500.00"));
accountService.createAccount(account2);

// 尝试转账超过余额的金额
assertThrows(RuntimeException.class, () -> {
accountService.transfer("1003", "1004", new BigDecimal("200.00"));
});

// 验证余额未改变
Account updatedAccount1 = accountService.getAccount("1003");
Account updatedAccount2 = accountService.getAccount("1004");

assertEquals(new BigDecimal("100.00"), updatedAccount1.getBalance());
assertEquals(new BigDecimal("500.00"), updatedAccount2.getBalance());
}
}

另外可以操作如下测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testNestedTransaction() {
// 创建账户
Account account1 = accountService.createAccount(...);
Account account2 = accountService.createAccount(...);

try {
// 转账并记录日志(主事务抛异常)
accountService.transfer("1001", "1002", new BigDecimal("200.00"));
} catch (Exception e) {
// 验证主事务回滚
Account updatedAccount1 = accountService.getAccount("1001");
assertEquals(1000.00, updatedAccount1.getBalance());

// 验证日志记录事务提交
List<Transaction> logs = logService.getTransactionHistory("1001");
assertFalse(logs.isEmpty());
}
}