Spring @Transactional 注解详解:从入门到避坑

一、为什么需要事务?

想象一个下单场景:

  1. 创建订单
  2. 扣减库存
  3. 扣除用户余额

如果第2步成功了,第3步失败,库存已经扣了但钱没扣,数据就乱了。事务的作用就是保证这一组操作要么全成功,要么全失败。

二、快速使用

java 复制代码
@Service
public class OrderService {

    @Transactional
    public void submitOrder(OrdersSubmitDTO dto) {
        orderMapper.insert(order);           // 插入订单
        orderDetailMapper.insertBatch(list); // 插入明细
        shoppingCartMapper.clean(userId);    // 清空购物车
    }
}

加了这个注解,方法内所有数据库操作会被包装在一个事务中。任意一步抛异常,Spring 会自动回滚。

三、核心属性详解

属性 作用 常用值
propagation 事务传播行为 REQUIRED(默认)、REQUIRES_NEW
isolation 隔离级别 READ_COMMITTEDREPEATABLE_READ
rollbackFor 指定回滚的异常类型 Exception.class
noRollbackFor 指定不回滚的异常 IllegalArgumentException.class
timeout 事务超时时间(秒) -1(默认无限制)
readOnly 是否为只读事务 true / false

3.1 propagation 传播行为

java 复制代码
@Transactional(propagation = Propagation.REQUIRED)  // 默认
public void methodA() {
    // 如果已有事务,加入;没有则新建
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
    // 挂起当前事务,新建一个独立事务
}

常见场景 :日志记录必须保存,不管主业务是否回滚。此时日志方法用 REQUIRES_NEW,主业务回滚不影响日志。

3.2 isolation 隔离级别

复制代码
@Transactional(isolation = Isolation.READ_COMMITTED)
隔离级别 脏读 不可重复读 幻读
READ_UNCOMMITTED
READ_COMMITTED
REPEATABLE_READ
SERIALIZABLE

MySQL 默认 REPEATABLE_READ,Spring 默认跟随数据库。

3.3 rollbackFor 回滚配置

重点 :默认只回滚 RuntimeExceptionError

java 复制代码
// 错误示范:IOException 不会触发回滚!
@Transactional
public void pay() throws IOException {
    // ...
}

// 正确配置:回滚所有异常
@Transactional(rollbackFor = Exception.class)
public void pay() throws IOException {
    // ...
}

四、底层原理(简述)

Spring 通过 AOP 动态代理 实现事务:

  1. 运行时生成目标类的代理对象
  2. 方法执行前开启事务(begin
  3. 方法正常结束则提交(commit
  4. 方法抛异常则回滚(rollback

这也是为什么同类内部调用会导致事务失效------绕过了代理对象。

五、失效场景全解析(避坑指南)

场景 1:同类内部调用(最高频)

java 复制代码
@Service
public class OrderService {
    
    public void createOrder() {
        saveOrder(); // 直接调用,事务失效!
    }
    
    @Transactional
    public void saveOrder() {
        // ...
    }
}

原因this.saveOrder() 没有走代理对象。

解决

java 复制代码
@Autowired
private OrderService orderService;

public void createOrder() {
    orderService.saveOrder(); // 通过注入的代理调用
}

场景 2:异常被吞掉

java 复制代码
@Transactional
public void submit() {
    try {
        orderMapper.insert(order);
        payService.pay(); // 抛异常
    } catch (Exception e) {
        log.error("支付失败", e);
        // 异常没抛出去,Spring 认为成功,提交事务!
    }
}

解决

java 复制代码
} catch (Exception e) {
    throw new RuntimeException(e);
    // 或者手动回滚:
    // TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}

场景 3:异常类型不匹配

如 3.3 所述,受检异常(IOExceptionSQLException)默认不回滚。

场景 4:方法非 public

@Transactional 只能作用于 public 方法。

场景 5:数据库引擎不支持

MySQL 的 MyISAM 引擎不支持事务,表必须是 InnoDB

六、最佳实践

  1. 尽量放在 Service 层,不要在 Controller 或 Mapper 上加
  2. 事务方法尽量精简,避免长事务(大事务会锁表、拖垮性能)
  3. 查询操作加 readOnly = true
  4. 明确指定 rollbackFor = Exception.class,避免受检异常漏网
  5. 不要在事务里调外部 HTTP 接口,会拉长事务时间

七、总结

要点 记忆口诀
代理调用才生效 同类 this. 调用要警惕
异常必须抛出去 别在 catch 里默默吞掉
回滚范围要明确 rollbackFor = Exception.class
长事务是性能杀手 查询加 readOnly,事务尽量短
相关推荐
努力努力再努力wz1 小时前
【MySQL 进阶系列】C/C++ 如何通过客户端库访问 MySQL?从连接原理到 API 调用流程详解(附完整demo代码)
服务器·c语言·数据结构·数据库·c++·b树·mysql
xuhaoyu_cpp_java1 小时前
单调栈(算法)
java·数据结构·经验分享·笔记·学习·算法
黑夜里的小夜莺2 小时前
黑马点评登录成功后点击【我的】会跳转到登录页面 BUG 修复
java·bug
倔强的石头_2 小时前
Cherry Studio零代码打造私域工具——“爆款文案拆解仿写机”
后端
_杨瀚博2 小时前
信创-为什么ORACLE使用JDBC查询SYSDATE时,RS.getDate能获取到时间部分?
后端
IT_陈寒2 小时前
JavaScript的this又背刺我,这次真长记性了
前端·人工智能·后端
七夜zippoe2 小时前
DolphinDB分布式表:创建与管理
数据库·分布式·维度·dolphindb·数据写入
wuyikeer2 小时前
Java进阶——IO 流
java·开发语言·python
何中应2 小时前
Redis集群搭建
数据库·redis·缓存