乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

@Transactional 的回滚规则

定义与作用

rollbackFor 和 noRollbackFor 是 @Transactional 的两个异常控制属性,用于精确配置哪些异常触发事务回滚,哪些异常不触发回滚。Spring 的默认回滚规则是:只有 RuntimeException 及其子类、Error 及其子类才会触发回滚;受检异常(Checked Exception,即 Exception 的非 RuntimeException 子类)默认不触发回滚。

这一设计基于 Spring 的异常哲学:运行时异常通常表示编程错误或不可恢复的系统故障,应当回滚事务以保证数据一致性;而受检异常通常表示业务规则违例(如余额不足、库存不足),调用方可能期望捕获后做补偿处理,不一定需要回滚。

但在实际业务中,默认规则往往不够灵活。例如:

  • 自定义的业务异常(继承自 RuntimeException)需要触发回滚;
  • 某些特定的 RuntimeException(如第三方 API 超时)不应触发回滚;
  • 某些受检异常(如 SQLException)实际上表示数据层故障,应当回滚。

rollbackFor 和 noRollbackFor 正是为了解决这些精细化需求而设计的。

适用位置与常用属性

属性类型默认值说明
rollbackForClass<? extends Throwable>[]{RuntimeException.class, Error.class}触发回滚的异常类型数组
noRollbackForClass<? 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,则所有异常(包括受检异常)都会触发回滚。

完整示例:飞翔科技订单处理与异常策略

场景简述

飞翔科技公司的电商平台需要处理复杂的订单流程。小崔(后端开发)设计了以下异常策略:

  1. 库存不足(StockInsufficientException,受检异常):属于业务规则违例,允许调用方捕获后提示用户重新选择商品,不回滚(因为订单尚未写入数据库,只是前置校验失败);
  2. 支付网关超时(PaymentTimeoutException,运行时异常):属于系统故障,已扣减的库存和已生成的订单必须回滚;
  3. 参数校验失败(IllegalArgumentException):属于客户端错误,不回滚(无数据库操作发生);
  4. 数据库连接断开(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_idproduct_namestockprice
1001机械键盘5(未变化)
1002无线鼠标0(未变化)

customer_order 表:无记录。

关键验证:IllegalArgumentException 在 noRollbackFor 中声明,事务不回滚。但由于异常发生在任何数据库写操作之前,数据自然未变化。如果参数校验在写操作之后发生,不回滚规则将体现价值。

场景二(库存不足)控制台输出:

=== 场景二:库存不足 ===
[场景二] 捕获异常:StockInsufficientException - 库存不足,商品:1002,当前库存:0,需求:1

场景二数据库状态:与初始状态一致,无变化。

关键验证:StockInsufficientException 是受检异常,默认情况下 Spring 不会回滚。通过 noRollbackFor 显式声明后,调用方可以捕获该异常并友好提示用户"库存不足,请选择其他商品",而无需担心事务回滚问题。

场景三(支付超时触发回滚)控制台输出:

=== 场景三:支付超时触发回滚 ===
[订单处理] 库存已扣减,商品:1001,数量:3
[订单处理] 订单已创建,用户:2024001,金额:897.0
[场景三] 捕获异常:PaymentTimeoutException - 支付网关响应超时,订单金额:897.0

场景三数据库状态:

product_idproduct_namestockprice
1001机械键盘5(回滚后恢复)
1002无线鼠标0

customer_order 表:无记录(订单插入被回滚)。

关键验证:PaymentTimeoutException 在 rollbackFor 中声明,事务回滚生效。库存从 5 扣减到 2 后,因支付超时回滚恢复为 5;订单记录也被撤销。数据一致性得到保护。

场景四(正常流程)控制台输出:

=== 场景四:正常流程 ===
[订单处理] 库存已扣减,商品:1001,数量:1
[订单处理] 订单已创建,用户:2024002,金额:299.0
[支付网关] 支付成功,金额:299.0
[订单处理] 订单支付完成,用户:2024002

场景四数据库状态:

product_idproduct_namestockprice
1001机械键盘4(正常扣减)
1002无线鼠标0

customer_order 表:

order_iduser_idproduct_idquantitytotal_amountstatus
1202400210011299.00PAID

易错场景与面试考点

反例一:受检异常默认不回滚导致数据不一致

@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 有什么风险?

虽然这能确保所有异常都触发回滚,避免了受检异常漏回滚的问题,但也可能导致过度回滚。某些异常可能是业务上已处理的(如捕获后切换备用方案),或表示非致命错误(如通知发送失败)。过度回滚会撤销本可保留的业务数据,降低系统可用性。建议精确配置,仅将真正表示数据层或系统层故障的异常纳入回滚范围。

上一页
@Transactional 的隔离级别
下一页
@Transactional 的超时与只读属性