@Transactional 事务注解

@Transactional 事务注解

  • [1. @Transactional 是什么?](#1. @Transactional 是什么?)
  • [2. @Transactional什么时候使用?](#2. @Transactional什么时候使用?)
  • [3. @Transactional 的核心原理](#3. @Transactional 的核心原理)
  • [4. 常见属性](#4. 常见属性)
    • [4.1 propagation (事务传播行为)](#4.1 propagation (事务传播行为))
    • [4.2 isolation (隔离级别)](#4.2 isolation (隔离级别))
    • [4.3 timeout](#4.3 timeout)
    • [4.4 readOnly](#4.4 readOnly)
    • [4.5 rollbackFor / rollbackForClassName](#4.5 rollbackFor / rollbackForClassName)
    • [4.6 noRollbackFor / noRollbackForClassName](#4.6 noRollbackFor / noRollbackForClassName)
  • [5. 默认回滚机制](#5. 默认回滚机制)
  • [6. 示例代码](#6. 示例代码)
  • [7. 常见坑点](#7. 常见坑点)
    • [7.1 事务方法调用自身不会生效](#7.1 事务方法调用自身不会生效)
  • [8. 最佳实践](#8. 最佳实践)
  • [9. 举例事务不生效](#9. 举例事务不生效)
  • 10. @Transactional失效场景,对#7的补充
  • [11. 不要在@Transactional做外部调用](#11. 不要在@Transactional做外部调用)
  • 附录

1. @Transactional 是什么?

  • @TransactionalSpring 提供的事务注解,用于声明式事务管理。
  • 它可以用在 方法(public) 上,用来声明该方法/类中的数据库操作需要在事务中执行。
  • 当方法执行过程中出现异常时,可以根据配置进行 事务回滚,保证数据一致性。

2. @Transactional什么时候使用?

以下场景建议使用 @Transactional:

当你需要执行多个数据库操作,且这些操作必须具备原子性(要么全部成功,要么全部失败)。

  • 需要在出现错误或异常时自动回滚操作。
  • 希望避免手动管理事务(比如直接使用 JDBC 或 EntityManager 的事务 API)。

3. @Transactional 的核心原理

Spring 的事务管理主要基于 AOP(面向切面编程) + 数据库的事务支持。

  1. 代理机制
  • Spring 为标注了 @Transactional 的方法生成一个代理类。
  • 在方法执行前,代理类会开启事务;执行后,提交或回滚事务。
  1. 事务管理器
  • Spring 使用 PlatformTransactionManager 来统一管理事务(如 DataSourceTransactionManager 对 JDBC,JpaTransactionManager 对 JPA)。
  1. 数据库事务
  • 最终还是依赖数据库本身的事务机制(ACID)。

4. 常见属性

4.1 propagation (事务传播行为)

定义方法在事务环境中的运行方式。常见值:

传播行为 说明
REQUIRED(默认) 如果存在事务,加入当前事务;没有事务则新建一个。
REQUIRES_NEW 不管是否有事务,都会新建一个事务;原事务会挂起。
SUPPORTS 有事务就加入,没有就以非事务方式运行。
NOT_SUPPORTED 总是非事务运行,如果有事务则挂起。
MANDATORY 必须在事务中运行,否则抛异常。
NEVER 必须在非事务中运行,否则抛异常。
NESTED 嵌套事务(依赖于 JDBC 的 savepoint 实现),内层回滚不影响外层。

4.2 isolation (隔离级别)

定义事务之间数据访问的隔离性。对应数据库隔离级别:

隔离级别 说明
DEFAULT(默认) 使用数据库默认隔离级别。
READ_UNCOMMITTED 允许读取未提交数据(脏读)。
READ_COMMITTED 只能读取已提交数据(避免脏读)。
REPEATABLE_READ 多次读取结果一致(避免脏读、不可重复读)。
SERIALIZABLE 串行执行事务,避免所有并发问题,但效率最低。

4.3 timeout

  • 事务超时时间(秒),默认使用数据库默认超时。
  • 超时后事务会回滚。

4.4 readOnly

  • true:只读事务,通常用于查询。
  • 提示数据库优化器可优化,不会修改数据。

4.5 rollbackFor / rollbackForClassName

  • 指定遇到哪些 异常类 时回滚事务(默认只对运行时异常 RuntimeException 回滚)。

4.6 noRollbackFor / noRollbackForClassName

  • 指定遇到哪些异常时 不回滚。

5. 默认回滚机制

  • 默认情况下:

    • 运行时异常(RuntimeException)和 Error → 会回滚。
    • 受检异常(CheckedException) → 不会回滚。
  • 可以通过 rollbackFor 来修改默认规则。

6. 示例代码

java 复制代码
@Service
public class UserService {

    @Transactional(
        propagation = Propagation.REQUIRED,
        isolation = Isolation.READ_COMMITTED,
        rollbackFor = Exception.class,
        timeout = 5
    )
    public void registerUser(User user) {
        userMapper.insert(user);
        // 可能抛出异常
        if (user.getName() == null) {
            throw new IllegalArgumentException("用户名不能为空");
        }
        accountMapper.createAccount(user.getId());
    }
}

7. 常见坑点

  1. 事务方法调用自身不会生效

    • 因为事务依赖代理,内部调用不会经过代理,所以事务不起作用。
    • 解决办法:将方法拆到不同的 @Service 类,或者通过 AopContext.currentProxy() 调用。
  2. private 方法不生效

    • Spring AOP 只能代理 public 方法,private/protected 方法不会被增强。
  3. 异步方法事务不生效

    • 异步调用(@Async)会新建线程,不在同一个事务上下文。
  4. 只读事务不是强制的

    • readOnly = true 并不会阻止写操作,只是数据库可能做优化。
  5. 被用 final 、static 修饰的方法上加 @Transactional 也不会生效。

7.1 事务方法调用自身不会生效

java 复制代码
@Service
public class UserService {

    @Transactional
    public void addUser() {
        // 插入用户
        saveUser();

        // 调用另一个事务方法
        updateUser();
    }

    @Transactional
    public void updateUser() {
        // 更新用户
    }
}

问题:内部调用不会走代理

addUser() 里调用 updateUser() 时,调用方式是:

java 复制代码
this.updateUser();
  • 这里的 this 是当前对象(原始对象),不是代理对象。
  • 因此,Spring 的 AOP 拦截器(TransactionInterceptor)不会介入。
  • 结果:updateUser() 上的 @Transactional 不会生效,只是普通方法调用。
  1. 如果 addUser() 没有 @Transactional,但 updateUser() 有:
    • 从外部调用 addUser() 时,updateUser() 的事务不会生效。
  2. 如果 addUser() 有事务,updateUser() 也有事务:
    • 实际上,整个调用都在 addUser() 的事务中,updateUser() 的传播行为被忽略。

解决办法

java 复制代码
@Service
public class UserService {

    @Autowired
    private UserHelperService helper;

    @Transactional
    public void addUser() {
        saveUser();
        // 可以解决自身调用事务不生效的问题,本质是通过 代理对象 调用
        SpringUtils.getBean(UserService.class).updateUser()
    }
}

@Service
public class UserHelperService {
    @Transactional
    public void updateUser() {
        // 更新逻辑
    }
}

8. 最佳实践

  • 事务边界尽量放在 Service 层(而不是 Controller/DAO)。
  • 保持事务尽量短小,避免长时间占用锁。
  • 明确指定 rollbackFor,避免因受检异常导致事务未回滚。
  • 避免在事务中调用远程服务(如 HTTP、RPC),防止长事务。

9. 举例事务不生效


解决问题方法

10. @Transactional失效场景,对#7的补充

  1. rollbackFor 默认只回滚RunTimeException与Error,抛个IOException都会回滚失败。
  2. 异常如果仅仅只catch而没有抛出,回滚也会失败。
  3. 同类方法互相调用,没有走代理,事务根本没有生效。如#7.1
  4. 在非public方法上会失效。因为@Transactional事务是基于Spring AOP实现的而Spring AOP默认只拦截public方法的调用。
  5. 在final or static修饰的方法上也会失效。cglib代理无法对final方法子类化。static是静态方法它属于类不属于实例对象,无法被代理。
  6. 事务传播属性配置错误。如下,不管是否有事务,都会新建一个事务;原事务会挂起。,两个方法都不在一个事务上,当然无法保证数据的一致性。
  1. 多线程@Transactional是基于ThreadLocal存储上下问的,如果线程一变,事务就断了。也就失效了。

  2. 如果数据库使用MySQL的MyISAM引擎,那么根本就不支持事务。

11. 不要在@Transactional做外部调用

很多人为了方便会在@Transactional中去查询或者更新缓存,或者调用外部的RPC接口,再或者是发一个MQ消息,竟然还有人说,如果我这个地方调用外部服务失败了,我本地事务就回滚,也能报纸一致性,如果你是这么想的就打错特错了,如果你在事务中做远程调用,会因为网络交互,当中存在的网络延迟以及下游服务的响应慢等,而导致你的事务变成长事务,会占用数据库的连接池。还有外部调用有可能超时,带来事务的回滚,但是超时的外部调用可能后面自己慢慢处理成功了。这就导致了事务的不一致。

我们可以先去调用外部服务,通了之后再去做开启事务本地操作。如果调用失败了或者本地事务操作失败了,再进行事务的补偿,调用外部的接口再进行回滚。

附录

1.Spring @Transactional 详解:何时使用、为什么使用、如何使用 https://mp.weixin.qq.com/s/pbJllQXG9yN5liiJ6Ywbsw

2.工作 6 年,@Transactional 注解用的一塌糊涂! https://mp.weixin.qq.com/s/Tv0juKbCFqSyXedM2tO3hA

相关推荐
大白的编程日记.2 小时前
【MySQL学习笔记】数据库的CURD(一)
数据库·学习·mysql
冥净2 小时前
通过OracleOEM创建可监控用户
oracle
不剪发的Tony老师3 小时前
RedisFront:一款免费开源的跨平台Redis客户端工具
数据库·redis·redisfront
什么半岛铁盒4 小时前
C++项目:仿muduo库高并发服务器--------Any类的实现
linux·服务器·数据库·c++·mysql·github
努力学习的小廉4 小时前
初识MYSQL —— 数据库基础
android·数据库·mysql
禁默4 小时前
MySQL 表约束实战指南:从概念到落地,守护数据完整性
数据库·mysql
l1t4 小时前
测试duckdb的C插件模板的编译加工和加载
c语言·开发语言·数据库·插件·duckdb
T - mars4 小时前
数据迁移:MySQL => SQL Server
数据库·mysql
JIngJaneIL4 小时前
记账本|基于SSM的家庭记账本小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·家庭记账本小程序