@Transactional 的回滚规则
定义与作用
rollbackFor 和 noRollbackFor 是 @Transactional 的两个异常控制属性,用于精确配置哪些异常触发事务回滚,哪些异常不触发回滚。Spring 的默认回滚规则是:只有 RuntimeException 及其子类、Error 及其子类才会触发回滚;受检异常(Checked Exception,即 Exception 的非 RuntimeException 子类)默认不触发回滚。
这一设计基于 Spring 的异常哲学:运行时异常通常表示编程错误或不可恢复的系统故障,应当回滚事务以保证数据一致性;而受检异常通常表示业务规则违例(如余额不足、库存不足),调用方可能期望捕获后做补偿处理,不一定需要回滚。
但在实际业务中,默认规则往往不够灵活。例如:
- 自定义的业务异常(继承自
RuntimeException)需要触发回滚; - 某些特定的
RuntimeException(如第三方 API 超时)不应触发回滚; - 某些受检异常(如
SQLException)实际上表示数据层故障,应当回滚。
rollbackFor 和 noRollbackFor 正是为了解决这些精细化需求而设计的。
适用位置与常用属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
rollbackFor | Class<? extends Throwable>[] | {RuntimeException.class, Error.class} | 触发回滚的异常类型数组 |
noRollbackFor | Class<? extends Throwable>[] | {} | 不触发回滚的异常类型数组 |
配置示例:
// 自定义业务异常触发回滚,非法参数不触发回滚
@Transactional(
rollbackFor = {BusinessException.class, SQLException.class},
noRollbackFor = {IllegalArgumentException.class}
)
public void processOrder(OrderRequest request) { ... }
优先级规则:
noRollbackFor的优先级高于rollbackFor。如果某个异常同时匹配两者,以noRollbackFor为准,事务不回滚。
核心原理:异常分类与回滚决策流程
Spring 的 TransactionInterceptor 在目标方法返回后,检查抛出的异常类型,通过 RollbackRuleAttribute 匹配 rollbackFor 和 noRollbackFor 配置,最终决定是否回滚。
回滚决策流程
异常继承层次与匹配规则
匹配规则:Spring 使用 Class.isAssignableFrom() 进行匹配,即子类异常会继承父类的回滚规则。例如配置 rollbackFor = Exception.class,则所有异常(包括受检异常)都会触发回滚。
完整示例:飞翔科技订单处理与异常策略
场景简述
飞翔科技公司的电商平台需要处理复杂的订单流程。小崔(后端开发)设计了以下异常策略:
- 库存不足(
StockInsufficientException,受检异常):属于业务规则违例,允许调用方捕获后提示用户重新选择商品,不回滚(因为订单尚未写入数据库,只是前置校验失败); - 支付网关超时(
PaymentTimeoutException,运行时异常):属于系统故障,已扣减的库存和已生成的订单必须回滚; - 参数校验失败(
IllegalArgumentException):属于客户端错误,不回滚(无数据库操作发生); - 数据库连接断开(
SQLException):属于基础设施故障,必须回滚。
操作前:数据库表结构与初始数据
-- 商品库存表
CREATE TABLE product_stock (
product_id INT PRIMARY KEY,
product_name VARCHAR(100),
stock INT NOT NULL,
price DECIMAL(10,2)
);
-- 订单表
CREATE TABLE customer_order (
order_id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
total_amount DECIMAL(10,2),
status VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 初始数据
INSERT INTO product_stock (product_id, product_name, stock, price) VALUES
(1001, '机械键盘', 5, 299.00),
(1002, '无线鼠标', 0, 99.00);
自定义异常类
package com.feixiang.exception;
// 库存不足:受检异常,业务规则违例
public class StockInsufficientException extends Exception {
public StockInsufficientException(String message) {
super(message);
}
}
package com.feixiang.exception;
// 支付超时:运行时异常,系统故障
public class PaymentTimeoutException extends RuntimeException {
public PaymentTimeoutException(String message) {
super(message);
}
}
Service 层:精细化回滚规则配置
package com.feixiang.service;
import com.feixiang.exception.PaymentTimeoutException;
import com.feixiang.exception.StockInsufficientException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderProcessingService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 订单处理:精细化回滚规则
* - rollbackFor: PaymentTimeoutException(系统故障), SQLException(数据库故障)
* - noRollbackFor: IllegalArgumentException(参数错误), StockInsufficientException(业务规则)
*/
@Transactional(
rollbackFor = {PaymentTimeoutException.class, java.sql.SQLException.class},
noRollbackFor = {IllegalArgumentException.class, StockInsufficientException.class}
)
public void processOrder(int userId, int productId, int quantity)
throws StockInsufficientException {
// 1. 参数校验(可能抛出 IllegalArgumentException,不回滚)
if (quantity <= 0) {
throw new IllegalArgumentException("购买数量必须大于 0,当前:" + quantity);
}
// 2. 查询库存
Integer stock = jdbcTemplate.queryForObject(
"SELECT stock FROM product_stock WHERE product_id = ?",
Integer.class, productId
);
if (stock == null) {
throw new IllegalArgumentException("商品不存在,ID:" + productId);
}
// 3. 库存校验(可能抛出 StockInsufficientException,不回滚)
if (stock < quantity) {
throw new StockInsufficientException(
"库存不足,商品:" + productId + ",当前库存:" + stock + ",需求:" + quantity
);
}
// 4. 扣减库存(数据库写操作开始)
jdbcTemplate.update(
"UPDATE product_stock SET stock = stock - ? WHERE product_id = ?",
quantity, productId
);
System.out.println("[订单处理] 库存已扣减,商品:" + productId + ",数量:" + quantity);
// 5. 查询价格并计算总价
Double price = jdbcTemplate.queryForObject(
"SELECT price FROM product_stock WHERE product_id = ?",
Double.class, productId
);
double totalAmount = price * quantity;
// 6. 插入订单记录
jdbcTemplate.update(
"INSERT INTO customer_order (user_id, product_id, quantity, total_amount, status) " +
"VALUES (?, ?, ?, ?, ?)",
userId, productId, quantity, totalAmount, "PENDING_PAYMENT"
);
System.out.println("[订单处理] 订单已创建,用户:" + userId + ",金额:" + totalAmount);
// 7. 模拟支付(可能抛出 PaymentTimeoutException,触发回滚)
simulatePaymentGateway(productId, totalAmount);
// 8. 更新订单状态为已支付
jdbcTemplate.update(
"UPDATE customer_order SET status = ? WHERE user_id = ? AND product_id = ? AND status = ?",
"PAID", userId, productId, "PENDING_PAYMENT"
);
System.out.println("[订单处理] 订单支付完成,用户:" + userId);
}
private void simulatePaymentGateway(int productId, double amount) {
// 模拟支付网关超时(商品 1001 且金额 > 500 时触发)
if (productId == 1001 && amount > 500) {
throw new PaymentTimeoutException("支付网关响应超时,订单金额:" + amount);
}
System.out.println("[支付网关] 支付成功,金额:" + amount);
}
}
测试执行与结果
package com.feixiang;
import com.feixiang.exception.StockInsufficientException;
import com.feixiang.service.OrderProcessingService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class RollbackRuleTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext("com.feixiang");
OrderProcessingService service = ctx.getBean(OrderProcessingService.class);
// 场景一:参数错误(IllegalArgumentException,不回滚)
System.out.println("=== 场景一:参数错误 ===");
try {
service.processOrder(2024001, 1001, -1);
} catch (Exception e) {
System.out.println("[场景一] 捕获异常:" + e.getClass().getSimpleName() + " - " + e.getMessage());
}
// 场景二:库存不足(StockInsufficientException,不回滚)
System.out.println("\n=== 场景二:库存不足 ===");
try {
service.processOrder(2024001, 1002, 1); // 鼠标库存为 0
} catch (Exception e) {
System.out.println("[场景二] 捕获异常:" + e.getClass().getSimpleName() + " - " + e.getMessage());
}
// 场景三:正常下单后支付超时(PaymentTimeoutException,触发回滚)
System.out.println("\n=== 场景三:支付超时触发回滚 ===");
try {
service.processOrder(2024001, 1001, 3); // 键盘 299*3=897 > 500,触发超时
} catch (Exception e) {
System.out.println("[场景三] 捕获异常:" + e.getClass().getSimpleName() + " - " + e.getMessage());
}
// 场景四:正常下单并支付成功
System.out.println("\n=== 场景四:正常流程 ===");
try {
service.processOrder(2024002, 1001, 1); // 键盘 299*1=299 <= 500,正常支付
} catch (Exception e) {
System.out.println("[场景四] 捕获异常:" + e.getClass().getSimpleName() + " - " + e.getMessage());
}
ctx.close();
}
}
操作后:控制台输出与数据变化
场景一(参数错误)控制台输出:
=== 场景一:参数错误 ===
[场景一] 捕获异常:IllegalArgumentException - 购买数量必须大于 0,当前:-1
场景一数据库状态:
| product_id | product_name | stock | price |
|---|---|---|---|
| 1001 | 机械键盘 | 5(未变化) | |
| 1002 | 无线鼠标 | 0(未变化) |
customer_order 表:无记录。
关键验证:IllegalArgumentException 在 noRollbackFor 中声明,事务不回滚。但由于异常发生在任何数据库写操作之前,数据自然未变化。如果参数校验在写操作之后发生,不回滚规则将体现价值。
场景二(库存不足)控制台输出:
=== 场景二:库存不足 ===
[场景二] 捕获异常:StockInsufficientException - 库存不足,商品:1002,当前库存:0,需求:1
场景二数据库状态:与初始状态一致,无变化。
关键验证:StockInsufficientException 是受检异常,默认情况下 Spring 不会回滚。通过 noRollbackFor 显式声明后,调用方可以捕获该异常并友好提示用户"库存不足,请选择其他商品",而无需担心事务回滚问题。
场景三(支付超时触发回滚)控制台输出:
=== 场景三:支付超时触发回滚 ===
[订单处理] 库存已扣减,商品:1001,数量:3
[订单处理] 订单已创建,用户:2024001,金额:897.0
[场景三] 捕获异常:PaymentTimeoutException - 支付网关响应超时,订单金额:897.0
场景三数据库状态:
| product_id | product_name | stock | price |
|---|---|---|---|
| 1001 | 机械键盘 | 5(回滚后恢复) | |
| 1002 | 无线鼠标 | 0 |
customer_order 表:无记录(订单插入被回滚)。
关键验证:PaymentTimeoutException 在 rollbackFor 中声明,事务回滚生效。库存从 5 扣减到 2 后,因支付超时回滚恢复为 5;订单记录也被撤销。数据一致性得到保护。
场景四(正常流程)控制台输出:
=== 场景四:正常流程 ===
[订单处理] 库存已扣减,商品:1001,数量:1
[订单处理] 订单已创建,用户:2024002,金额:299.0
[支付网关] 支付成功,金额:299.0
[订单处理] 订单支付完成,用户:2024002
场景四数据库状态:
| product_id | product_name | stock | price |
|---|---|---|---|
| 1001 | 机械键盘 | 4(正常扣减) | |
| 1002 | 无线鼠标 | 0 |
customer_order 表:
| order_id | user_id | product_id | quantity | total_amount | status |
|---|---|---|---|---|---|
| 1 | 2024002 | 1001 | 1 | 299.00 | PAID |
易错场景与面试考点
反例一:受检异常默认不回滚导致数据不一致
@Service
public class RiskyService {
@Autowired
private JdbcTemplate jdbcTemplate;
// 错误:未配置 rollbackFor,受检异常不回滚
@Transactional
public void createOrder() throws BusinessException {
jdbcTemplate.update("INSERT INTO orders ..."); // 订单已插入
jdbcTemplate.update("UPDATE inventory ..."); // 库存已扣减
// 抛出受检异常
throw new BusinessException("业务规则校验失败");
}
}
问题分析:BusinessException 继承自 Exception(受检异常),默认不在 Spring 的回滚范围内。方法抛出异常后,Spring 执行 commit 而非 rollback,导致订单和库存的修改被永久保存,但业务层认为操作失败。这是生产环境中最常见的数据不一致陷阱之一。
正确写法:
@Transactional(rollbackFor = BusinessException.class)
public void createOrder() throws BusinessException {
// ...
}
或更简洁地,让 BusinessException 继承 RuntimeException:
public class BusinessException extends RuntimeException {
// ...
}
反例二:rollbackFor = Exception.class 过度回滚
@Service
public class OverRollbackService {
// 错误:所有异常都触发回滚,包括预期的业务异常
@Transactional(rollbackFor = Exception.class)
public void process() {
jdbcTemplate.update("INSERT INTO log ..."); // 记录操作日志
// ...
}
}
问题分析:配置 rollbackFor = Exception.class 会将所有异常(包括受检异常)纳入回滚范围。虽然这避免了"受检异常不回滚"的问题,但也导致所有异常都触发回滚,包括那些业务上已做补偿处理的异常。例如,如果方法内部捕获了 FileNotFoundException 并切换到备用文件,事务仍会被回滚,导致日志记录被撤销。
正确写法:精确指定需要回滚的异常类型,而非一刀切:
@Transactional(rollbackFor = {SQLException.class, PaymentException.class, SystemException.class})
public void process() {
// ...
}
反例三:noRollbackFor 与 rollbackFor 的优先级误解
@Service
public class PriorityService {
@Transactional(
rollbackFor = {RuntimeException.class},
noRollbackFor = {IllegalStateException.class}
)
public void process() {
// ...
throw new IllegalStateException("状态错误"); // IllegalStateException 是 RuntimeException 的子类
}
}
问题分析:IllegalStateException 同时匹配 rollbackFor = RuntimeException.class 和 noRollbackFor = IllegalStateException.class。Spring 的匹配规则是先检查 noRollbackFor,再检查 rollbackFor。因此 noRollbackFor 优先级更高,该异常不会触发回滚。如果开发者误以为 rollbackFor 的宽泛声明会覆盖 noRollbackFor 的精确声明,就会产生与预期相反的行为。
面试高频考点
Q1:Spring 默认对哪些异常回滚?对哪些不回滚?
默认只对
RuntimeException及其子类、Error及其子类回滚。所有受检异常(Exception的非RuntimeException子类)默认不回滚。这是 Spring 的设计选择:运行时异常通常表示不可恢复的错误,应当回滚;受检异常通常表示可预期的业务条件,允许调用方捕获并处理。
Q2:为什么我的自定义异常没有触发回滚?
如果自定义异常继承自
Exception(受检异常),默认不会触发回滚。解决方案有两种:一是让自定义异常继承RuntimeException;二是在@Transactional(rollbackFor = MyException.class)中显式声明。
Q3:rollbackFor 和 noRollbackFor 同时匹配时的优先级?
noRollbackFor的优先级高于rollbackFor。如果异常同时匹配两者,以noRollbackFor为准,事务不回滚。Spring 的匹配逻辑是:先遍历noRollbackFor列表,若匹配则直接提交;再遍历rollbackFor列表,若匹配则回滚;最后应用默认规则(RuntimeException/Error 回滚,其他提交)。
Q4:配置 rollbackFor = Exception.class 有什么风险?
虽然这能确保所有异常都触发回滚,避免了受检异常漏回滚的问题,但也可能导致过度回滚。某些异常可能是业务上已处理的(如捕获后切换备用方案),或表示非致命错误(如通知发送失败)。过度回滚会撤销本可保留的业务数据,降低系统可用性。建议精确配置,仅将真正表示数据层或系统层故障的异常纳入回滚范围。