DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

Spring Boot 抛出受检异常后事务没有回滚

受检异常默认不要求 Spring 回滚。检查 rollbackFor、全局回滚规则、异常捕获、代理入口与数据库事务能力,避免只改 throws。

事务方法已经写入数据库,随后抛出了 IOException 或自定义的业务异常,调用方也收到了失败,数据库里的修改却提交了。若异常继承自 Exception、没有继承 RuntimeException,先检查回滚规则。

Spring 默认对 RuntimeException 和 Error 回滚,对受检异常不自动回滚。这里适用于 Spring Boot 3.x、Spring Framework 6.x,且项目没有修改默认回滚规则。Framework 6.2 起可以通过 @EnableTransactionManagement(rollbackOn = ALL_EXCEPTIONS) 改变全局默认值,因此不能仅凭异常类型就断言某个项目一定会提交。

throws 声明和 REQUIRED 都没有设置回滚条件

Java 方法上的 throws 只是异常声明。@Transactional(propagation = Propagation.REQUIRED) 也只决定是否创建或参与事务;它不会额外要求所有异常都回滚。

下面用一个自定义受检异常说明。异常类放在单独文件中:

public class BookingRejectedException extends Exception {
    public BookingRejectedException(String message) {
        super(message);
    }
}

假设项目的 Service 已注入 JdbcTemplate jdbc,bookings 是隔离验证中使用的业务表。这段方法先写入,再按示例条件拒绝:

@Transactional
public void createBooking(long id, boolean reject)
        throws BookingRejectedException {
    jdbc.update("insert into bookings(id) values (?)", id);
    if (reject) {
        throw new BookingRejectedException("Booking rejected");
    }
}

在默认规则下,受检异常离开方法并不要求 Spring 回滚。这解释了为什么“接口返回失败”和“数据库已提交”可以同时发生。是否最终提交还会受到外层事务、其他回滚规则和数据库事务能力影响。

给需要撤销写入的业务异常指定类型

如果 BookingRejectedException 表示整个预约操作失败,应把规则加在实际事务入口上:

import org.springframework.transaction.annotation.Transactional;

@Transactional(rollbackFor = BookingRejectedException.class)
public void createBooking(long id, boolean reject)
        throws BookingRejectedException {
    jdbc.update("insert into bookings(id) values (?)", id);
    if (reject) {
        throw new BookingRejectedException("Booking rejected");
    }
}

rollbackFor 按异常类型及其继承关系匹配。这里明确要求该业务异常回滚,原有对运行时异常的默认回滚行为也仍然存在。若项目要求所有 Exception 都撤销事务,可以审查后使用 rollbackFor = Exception.class,但要确认业务中是否存在“通知调用方某种情况,同时允许提交”的受检异常。

检查类级注解、方法级注解和 noRollbackFor 时,应看实际生效的事务规则。遇到相反规则,匹配更具体的异常类型会影响结果;不要在不同位置机械地叠加注解。优先用异常类,避免过于宽泛的类名字符串匹配误伤其他异常。

Framework 6.2 的全局 ALL_EXCEPTIONS 设置适合统一项目策略,但它会影响其他事务方法,不能为了修一个接口直接更改全站默认语义。旧版本也没有这个属性。

配了 rollbackFor 仍然提交,检查异常是否到达事务拦截器

下面的捕获会把异常变成正常返回。事务拦截器看不到原来的受检异常,rollbackFor 就没有机会按它判断:

try {
    reserveInventory();
} catch (BookingRejectedException ex) {
    return;
}

这个片段假定内部调用尚未在其他事务边界标记回滚。如果调用的是另一个经过代理的事务方法,内层可能已经把共享事务设为 rollback-only;外层捕获异常不能取消这个标记,提交时仍可能报 UnexpectedRollbackException。

如果失败应撤销整个操作,应让对应异常离开事务方法,或者按项目设计明确标记回滚。控制器在事务 Service 调用结束后捕获异常并组织 HTTP 响应,是另一层处理;关键是异常先经过事务边界。

还要检查同类调用绕过事务代理。一个普通方法通过 this 调用带注解的方法时,给内层方法增加 rollbackFor 不能补回缺失的代理拦截。多数据源项目则要确认写入使用的是该注解指定事务管理器管理的资源。

数据库本身也必须支持事务。对于 MySQL,还要确认涉及的业务表使用支持事务的存储引擎;注解不会替非事务表提供回滚能力。已经独立提交的 REQUIRES_NEW 工作和数据库之外的副作用,也不属于当前事务能够撤销的范围。

修改后,用独立的业务标识分别核对正常提交、受检异常、运行时异常和内部捕获路径。失败路径结束后,在新的事务或连接中读取数据库结果,避免只观察测试方法中的对象状态;带自动回滚的测试事务也可能掩盖应用真实的提交边界。保留原事务配置,回退规则不会撤销此前已经提交的数据。