@Transactional 作用与使用场景(Spring 事务注解)
一、核心作用
@Transactional 是 Spring 提供的声明式事务 注解,底层基于 AOP 实现,不用手动写commit/rollback,自动管理数据库事务:
- 事务原子性:注解内所有数据库操作要么全部成功提交,要么全部回滚;
- 自动回滚 :默认遇到运行时异常 (RuntimeException)、错误 (Error) 自动回滚;普通 Checked 受检异常默认不回滚;
- 事务传播控制 :通过
propagation控制嵌套事务如何运行; - 隔离级别控制 :通过
isolation解决脏读、不可重复读、幻读; - 超时、只读、锁控制:配置超时时间、只读事务、悲观锁。
底层原理
Spring AOP 动态代理:调用带注解方法前开启事务,方法正常结束提交;抛出指定异常则回滚。
坑:同类内部方法自调用不生效(不会走代理),需要自己注入自己或拆到不同类。
二、常用属性详解
java
@Transactional(
propagation = Propagation.REQUIRED, // 传播机制(默认)
isolation = Isolation.DEFAULT, // 隔离级别,使用数据库默认
rollbackFor = RuntimeException.class, // 指定什么异常回滚
noRollbackFor = {}, // 指定异常不回滚
readOnly = false, // 只读事务,优化查询
timeout = 30 // 事务超时秒数
)
-
propagation 传播机制(高频)
REQUIRED(默认):有事务就加入,无则新建(90% 业务使用)REQUIRES_NEW:新建独立事务,外层回滚不影响内层SUPPORTS:有事务就用,没有就无事务执行NOT_SUPPORTED:挂起当前事务,无事务执行MANDATORY:必须存在外部事务,否则抛异常NEVER:禁止有事务,有则抛异常NESTED:嵌套事务,可单独回滚子事务(依赖数据库 savepoint)
-
rollbackFor 默认只回滚
RuntimeException/Error,如果需要受检异常 (IOException 等) 回滚,必须手动指定:java@Transactional(rollbackFor = Exception.class) -
readOnly=true 标记只读事务,数据库优化查询,禁止增删改,适合纯查询接口。
三、适用使用场景
1. 多表 / 多操作必须同时成功的业务(最常用)
- 下单:扣库存、生成订单、扣余额、生成物流记录,任一失败全部回滚
- 转账:A 扣钱、B 加钱,不能单边成功
- 数据同步:主表新增 + 多张关联子表批量插入 / 更新
java
@Transactional
public void createOrder(OrderDTO dto){
orderMapper.insert();
stockMapper.deduct();
accountMapper.subMoney();
}
2. 批量操作、批量导入 / 批量更新
批量插入几百上千条数据,中途报错需要全部撤销,防止半批数据入库。
3. 嵌套事务场景(REQUIRES_NEW / NESTED)
- 主流程失败,但日志、记录操作必须留存:日志方法单独开新事务
java
// 主方法,失败回滚
@Transactional
public void pay(){
payMapper.update();
try{
saveLog();
}catch (Exception e){}
}
// 独立事务,不受外层回滚影响
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(){
logMapper.insert();
}
4. 需要自定义异常回滚规则
业务自定义 Exception,需要触发回滚,配置rollbackFor = Exception.class。
5. 长流程业务,设置事务超时
复杂统计、大量数据计算,防止长事务占用锁:timeout = 60。
6. 纯查询接口(只读事务优化)
java
@Transactional(readOnly = true)
public Page<DataVO> queryData(){}
四、不适合使用 @Transactional 的场景
- 只读查询简单接口:不加注解性能更好,不需要事务;
- 耗时极长的大事务(文件 IO、网络请求、循环大量计算):长事务锁表,引发阻塞死锁;拆分事务;
- 同类内部调用 :
A类.methodA()调用本类@Transactional methodB(),事务失效; - 静态方法、private 方法:AOP 无法代理,注解无效;
- 只做单条简单 insert/update,无联动数据,没必要加。
五、常见踩坑点
- 方法非 public → 事务失效;
- 同类内部调用带 @Transactional 方法 → 失效;
- 抛出 try-catch 捕获异常,没有重新抛出 → 不会自动回滚;
- 只抛出受检 Exception,未配置 rollbackFor → 不回滚;
- 多数据源未指定
transactionManager,事务管理器不匹配。
六、最简最佳实践
- 写在Service 层 public 方法上,不写在 Controller;
- 增删改业务统一加:
@Transactional(rollbackFor = Exception.class); - 查询使用
@Transactional(readOnly = true); - 独立日志、记录用
REQUIRES_NEW隔离事务。
@GlobalTransactional 作用、原理、使用场景
一、基础说明
@GlobalTransactional 是 Seata 分布式事务 注解,用来解决跨多个微服务、多数据库的数据一致性问题。 对比:
@Transactional:本地事务,仅限单库、单服务;@GlobalTransactional:全局分布式事务,跨服务 / 多数据源统一回滚。
核心作用
- 开启Seata AT 模式全局事务 ,当前方法作为全局事务入口(TC 发起方);
- 所有被调用的微服务本地事务(
@Transactional)都纳入同一全局事务; - 任一节点报错,所有服务的数据库操作全部回滚;
- 正常执行完毕,所有分支事务统一提交。
执行流程(AT 模式)
- 入口方法加
@GlobalTransactional,Seata TC 创建全局 XID; - 每个微服务
@Transactional本地执行业务,记录 undo_log 快照; - 全部执行成功 → TC 通知所有分支提交,删除 undo_log;
- 任意分支抛异常 → TC 通知所有分支根据 undo_log 回滚数据。
二、常用属性
java
@GlobalTransactional(
timeout = 60000, // 全局事务超时时间(毫秒),超时自动回滚
name = "createOrderTx", // 全局事务名称,用于日志排查
rollbackFor = Exception.class // 指定哪些异常触发全局回滚
)
- 默认只在抛出异常向外抛出时回滚;
- 如果内部 try-catch 吞掉异常,Seata 感知不到,不会回滚。
三、使用场景(什么时候必须用)
1. 跨微服务调用(最常用)
下单场景:订单服务 ↔ 库存服务 ↔ 账户服务,三个库三张表。 任一服务失败,全部撤销:
java
@Service
public class OrderServiceImpl {
@Autowired StockFeign stockFeign;
@Autowired AccountFeign accountFeign;
// 全局事务入口
@GlobalTransactional(rollbackFor = Exception.class)
public void createOrder() {
// 1. 本地生成订单(本地@Transactional)
orderMapper.insert();
// 2. 远程扣库存(库存服务本地事务)
stockFeign.deduct();
// 3. 远程扣余额(账户服务本地事务)
accountFeign.subBalance();
}
}
任意 feign 调用抛异常,订单、库存、账户全部回滚。
2. 单服务多数据源(多库本地分布式事务)
一个服务连接 MySQL-DB1、MySQL-DB2,同时操作两个库: 单库@Transactional只能管一个数据源,多库必须用@GlobalTransactional。
3. 复杂长链路业务(多级服务调用)
A 服务调 B,B 调 C,C 调 D,整条链路数据需要原子一致性。 只需要最外层入口方法 加 @GlobalTransactional,内层微服务只需要普通@Transactional即可。
4. 导入 / 批量同步跨库数据
批量同步数据到多个业务库,中途任一库写入失败,全部回滚避免脏数据。
四、使用规范(重要)
-
仅加在全局事务入口 只在最外层发起方法加,内层微服务只使用普通
@Transactional,不要重复加@GlobalTransactional。 -
内层必须配合本地事务 每个微服务数据库操作方法必须标注
@Transactional,否则分支无 undo_log,无法回滚。 -
异常不能被吞 不能 try-catch 后不抛出,Seata 捕获不到异常不会触发全局回滚:
java// 错误写法,吞异常不回滚 @GlobalTransactional public void test() { try { stockFeign.deduct(); } catch (Exception e) { log.error("异常"); // 没有重新抛出,全局事务不回滚 } } // 正确写法 @GlobalTransactional(rollbackFor = Exception.class) public void test() { try { stockFeign.deduct(); } catch (Exception e) { log.error("异常", e); throw new RuntimeException(e); // 重新抛出触发回滚 } } -
不能和本地事务混淆
@GlobalTransactional依赖 Seata 组件,必须配置:
- seata 注册中心、配置中心
- 数据源代理
DataSourceProxy - 业务库创建 undo_log 表
五、不适合使用场景
- 仅单库单服务:只用
@Transactional即可,无需分布式事务,性能损耗大; - 纯查询接口:无数据修改,不需要事务;
- 高并发短平快简单业务:Seata AT 模式有锁、undo_log 开销,尽量拆分避免长全局事务;
- 异步、MQ 消息消费单独事务场景,可选用 TCC/SAGA 模式替代 AT。
六、@Transactional vs @GlobalTransactional 对比
| 注解 | 事务范围 | 依赖组件 | 适用场景 |
|---|---|---|---|
| @Transactional | 单数据库、单服务 | Spring 自带,无需中间件 | 本地单体业务 |
| @GlobalTransactional | 多服务 / 多数据源分布式 | Seata | 微服务跨库跨服务数据一致性 |
七、常见坑
- 内层微服务不加
@Transactional,分支事务无法回滚; - 捕获异常不重新抛出,全局事务提交不回滚;
- 数据源未包装为 Seata 代理数据源,undo_log 不生成;
- 全局事务超时,自动触发全局回滚;
- 同类内部调用
@GlobalTransactional方法,AOP 失效,分布式事务不生效。
一、同类内部方法自调用不生效(不会走代理),需要自己注入自己或拆到不同类
Spring 事务(@Transactional)、Seata 分布式事务(@GlobalTransactional)都是AOP 动态代理 实现。 只有外部对象调用 带注解的方法时,才会经过代理增强,开启事务; 同类内部 this.xxx () 自调用,直接走原生对象,不经过代理,事务完全失效。
下面分两种场景举例:普通本地事务 + 分布式事务。
场景 1:@Transactional 内部调用失效(错误示例)
java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 外部调用入口,无事务注解
public void createOrder() {
// 内部自调用 this.addOrder(),不走代理!事务失效
this.addOrder();
}
// 加了事务注解
@Transactional(rollbackFor = Exception.class)
public void addOrder() {
orderMapper.insert(new Order());
// 抛出异常,期望回滚,但因为自调用,事务没开启,数据不会回滚
int i = 1 / 0;
}
}
调用逻辑:
java
// controller 调用
orderService.createOrder();
现象:插入订单成功,报除零异常,数据没有回滚,事务注解完全无效。
场景 2:@GlobalTransactional 内部调用同样失效
java
@Service
public class OrderService {
@Autowired
StockFeign stockFeign;
public void submitOrder() {
// this自调用,分布式事务失效
this.globalCreateOrder();
}
@GlobalTransactional(rollbackFor = Exception.class)
public void globalCreateOrder() {
orderMapper.insert(...);
stockFeign.deductStock();
throw new RuntimeException("扣库存失败");
}
}
调用 submitOrder() 抛异常,不会触发全局回滚,订单数据残留。
二、两种标准解决方案
方案 1:自己注入自己(本类代理对象调用,推荐)
java
@Service
public class OrderService {
// 注入自身代理对象
@Autowired
private OrderService orderService;
@Autowired
private OrderMapper orderMapper;
public void createOrder() {
// 使用代理对象调用,会走AOP增强,事务生效
orderService.addOrder();
}
@Transactional(rollbackFor = Exception.class)
public void addOrder() {
orderMapper.insert(new Order());
int i = 1 / 0;
}
}
执行后抛异常,数据库数据自动回滚,事务正常生效。
方案 2:拆分方法到新类(解耦,大型项目推荐)
把带事务的方法抽成独立 Service,通过注入调用,天然走代理:
java
@Service
public class OrderBizService {
@Autowired
private OrderMapper orderMapper;
@Transactional(rollbackFor = Exception.class)
public void addOrder() {
orderMapper.insert(new Order());
int i = 1 / 0;
}
}
java
@Service
public class OrderService {
// 注入独立业务类
@Autowired
private OrderBizService orderBizService;
public void createOrder() {
// 外部类调用,代理生效
orderBizService.addOrder();
}
}
三、补充:使用 ApplicationContext 获取自身代理(不推荐)
java
@Service
public class OrderService implements ApplicationContextAware {
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.context = applicationContext;
}
public void createOrder() {
// 从容器拿代理对象
OrderService proxy = context.getBean(OrderService.class);
proxy.addOrder();
}
@Transactional(rollbackFor = Exception.class)
public void addOrder() {
orderMapper.insert(new Order());
int i = 1 / 0;
}
}
缺点:代码侵入性强,日常开发优先用「自注入」或「拆分类」。
四、总结记忆
this.方法()内部调用 → 不走代理 → 事务失效;- 本类注入自己
@Autowired OrderService self→self.方法()生效; - 抽离到其他 Service 注入调用,代码结构更清晰,无坑;
@Transactional、@GlobalTransactional都遵循这条规则。