做业务开发,@Transactional几乎每天都在用。 看上去一个简单注解,就可以自动帮我们完成事务提交、异常回滚,很多同学以为只要把注解贴上去就万事大吉。
但线上不止一次遇到诡异故障:代码抛异常了,数据库数据却已经写入,没有回滚。排查半天,注解明明写在代码上,看起来完全没问题。
根源大多不是数据库故障,而是 Spring AOP 代理机制、异常捕获、方法权限、事务传播行为这些细节没有理解到位。 今天结合线上真实踩坑案例,把 7 个高频失效场景、错误 Demo、修复方案、生产编码规范全部整理出来。
前置说明:Spring 声明式事务底层依赖AOP 动态代理 。只有通过 Spring 代理对象调用方法,事务才会生效;一旦绕过代理,
@Transactional仅仅只是普通注释。
场景 1:@Transactional 写在非 public 方法上
JDK/CGLIB 代理不会拦截 private /protected/default 方法,注解完全无效。
❌错误 Demo
java
@Service
public class OrderService {
// private,事务注解无效
@Transactional(rollbackFor = Exception.class)
private void saveOrder() {
orderMapper.insert(new Order());
throw new RuntimeException("业务异常");
}
}
✅修复:把方法改为public。
场景 2:本类内部 this 调用事务方法(最高频)
问题:同一个 Service 内部,使用
this调用被@Transactional修饰的方法,不走代理,事务直接失效。
❌错误 Demo
typescript
@Service
public class OrderService {
public void createOrder() {
// this调用,绕过Spring代理,事务注解失效
this.saveOrder();
}
@Transactional
public void saveOrder() {
// 插入订单
orderMapper.insert(new Order());
// 抛出运行时异常,期望回滚,实际不会回滚
throw new RuntimeException("模拟业务异常");
}
}
✅修复方案一:自我注入(推荐,改动最小)
typescript
@Service
public class OrderService {
// 自注入,拿到代理对象
@Autowired
private OrderService self;
public void createOrder() {
// 使用代理对象调用
self.saveOrder();
}
@Transactional
public void saveOrder() {
orderMapper.insert(new Order());
throw new RuntimeException("模拟业务异常");
}
}
✅修复方案二:拆分方法到另外一个 Service; ✅修复方案三:开启expose‑proxy = true,使用AopContext.currentProxy()获取代理。
场景 3:没有配置 rollbackFor,受检异常不触发回滚
Spring 事务默认仅对
RuntimeException、Error回滚;普通受检 Exception(IOException 等)不会自动回滚。
❌错误 Demo
java
@Service
public class OrderService {
// 没有配置 rollbackFor,抛出受检异常不会回滚
@Transactional
public void saveOrder() throws IOException {
orderMapper.insert(new Order());
throw new IOException("IO受检异常");
}
}
✅修复,指定 rollbackFor
java
@Transactional(rollbackFor = Exception.class)
public void saveOrder() throws IOException {
orderMapper.insert(new Order());
throw new IOException("IO受检异常");
}
业务项目建议统一习惯写
rollbackFor = Exception.class。
场景 4:多线程,子线程无法继承主线程事务
Spring 事务信息保存在
ThreadLocal,子线程是全新线程,拿不到父线程事务上下文,子线程数据库操作是独立事务,不会跟随主线程回滚。
❌错误 Demo
java
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void saveOrder() {
orderMapper.insert(new Order());
// 子线程执行DB操作,不属于当前事务
new Thread(() -> {
orderMapper.insert(new Order());
}).start();
throw new RuntimeException("主线程异常");
}
}
现象:主线程异常回滚主线程 insert;子线程插入的数据不会回滚。
✅说明:不要指望子线程参与父事务;子线程需要数据库操作要自己开启独立事务。
场景 5:try‑catch 捕获吃掉异常,没有向外抛出
Spring 事务拦截器只有感知到方法抛出异常,才会触发回滚;如果异常被 catch 吞掉,直接提交事务。
❌错误 Demo
java
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void saveOrder() {
try {
orderMapper.insert(new Order());
int i = 1 / 0; // 抛出算术异常
} catch (Exception e) {
// 异常被吃掉,没有抛出,事务不会回滚
log.error("保存订单异常", e);
}
}
}
✅方案 A:catch 之后重新抛出异常
php
@Transactional(rollbackFor = Exception.class)
public void saveOrder() {
try {
orderMapper.insert(new Order());
int i = 1 / 0;
} catch (Exception e) {
log.error("保存订单异常", e);
throw new RuntimeException(e); // 重新抛出
}
}
✅方案 B:catch 住不想抛出异常,手动标记事务回滚
java
import org.springframework.transaction.support.TransactionAspectSupport;
@Transactional(rollbackFor = Exception.class)
public void saveOrder() {
try {
orderMapper.insert(new Order());
int i = 1 / 0;
} catch (Exception e) {
log.error("保存订单异常", e);
// 手动设置回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
场景 6:数据表引擎 MyISAM,不支持事务
MyISAM 不支持事务,无论注解怎么写,不会回滚。只发生在测试环境、老项目。
sql
-- 查看表引擎
show create table `order`;
-- 修改为InnoDB
alter table `order` engine = InnoDB;
这个属于数据库层面问题,Java 代码看不出任何问题,抛异常数据依旧落库。
场景 7:事务传播行为配置错误,嵌套事务回滚冲突
典型:REQUIRES_NEW、NESTED、REQUIRED混用,出现Transaction rolled‑back because it has been marked as rollback‑only。
❌错误 Demo:内层异常,外层 catch,出现 rollback‑only
typescript
@Service
public class OrderService {
@Autowired
private ItemService itemService;
@Transactional(rollbackFor = Exception.class)
public void createOrder() {
orderMapper.insert(new Order());
try {
// REQUIRED,加入当前外层事务
itemService.saveItem();
} catch (Exception e) {
log.error("保存商品失败", e);
// 内层抛出异常标记整个事务回滚,外层catch也无法提交
}
}
}
@Service
class ItemService{
@Transactional
public void saveItem(){
itemMapper.insert(new Item());
throw new RuntimeException("商品异常");
}
}
现象:内层异常把整个全局事务标记 rollbackOnly;外层即使 catch,最终整个事务依旧回滚。
✅修复方案:如果希望子方法失败不影响主事务,子方法使用 propagation = Propagation.REQUIRES_NEW,开启全新独立事务。
java
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveItem(){
itemMapper.insert(new Item());
throw new RuntimeException("商品异常");
}
额外:长事务问题(事务生效,但带来线上灾难)
即使事务没有失效,如果事务方法内部包含 RPC、HTTP 调用、耗时循环,会造成长事务:数据库连接占用、行锁长期持有,引发数据库阻塞、接口大面积超时。
❌反面示例
arduino
@Transactional(rollbackFor = Exception.class)
public void createOrder(){
orderMapper.insert(order);
// 远程RPC调用,网络慢会拉长事务时间,严禁写在事务内部
remoteUserFeign.getUserInfo(order.getUserId());
// http请求
restTemplate.getForObject("xxx",String.class);
}
✅正确:把远程调用挪到事务方法外面。
✅生产环境 @Transactional 编码 CheckList
@Transactional只加在public方法;- 禁止本类
this调用事务方法,使用代理对象; - 业务统一配置
@Transactional(rollbackFor = Exception.class); - 事务方法内部不要放 RPC/HTTP/ 外部接口等耗时调用;
- 事务内部避免大循环,防止长事务;
- 确认业务表使用 InnoDB 引擎;
- 子线程不会继承父线程事务,不能共用事务;
- catch 异常场景,要么重新抛出,要么手动
setRollbackOnly(); - 嵌套事务理清传播行为,区分 REQUIRED / REQUIRES_NEW。
AI 局限性
AI 可以快速生成带@Transactional的 demo,但是经常产出:本类 this 调用、缺少 rollbackFor 的隐患代码。Demo 跑通,上线发生数据不回滚故障。AI 很难识别 AOP 代理带来的隐性陷阱。
@Transactional不是数据一致性的护身符。 注解生效前提是经过 Spring 代理对象调用。很多线上脏数据 bug,不是数据库故障,而是编码破坏 AOP 代理规则。写业务代码,不能只看功能跑通,要充分考虑异常场景。
你在线上遇到过事务不回滚的坑吗?踩过哪一类?欢迎评论区交流。
我是十年 Java 后端程序媛,分享 JDK 迁移、业务踩坑实录,完整文章合集同步更新公众号:Java 后端程序媛手记。