spring中@Transactional注解对事务的支持

一、数据库事务基本概念

1.1 事务的定义

事务(Transaction)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。事务是数据库并发控制和恢复的基本单位,具有不可分割的特性------事务中的所有操作要么全部执行成功,要么全部不执行。

在关系型数据库中,事务通常由一条或多条SQL语句组成。以银行转账为例:从账户A扣款100元并向账户B增加100元,这两个操作必须作为一个整体------要么都成功,要么都失败回滚,绝不能出现"钱扣了但没到账"的情况。

1.2 ACID特性

事务的可靠性由ACID四个核心特性来保证:

**原子性(Atomicity):**事务是一个不可分割的最小工作单元。事务中的所有操作要么全部完成,要么全部不执行。如果在执行过程中发生错误,系统会将已经执行的操作回滚到事务开始前的状态。

**一致性(Consistency):**事务执行前后,数据库必须从一个一致性状态转换到另一个一致性状态。一致性是由业务规则和数据库约束(如主键、外键、唯一约束、CHECK约束等)共同定义的。

**隔离性(Isolation):**多个事务并发执行时,每个事务都感觉不到其他事务的存在,仿佛自己是数据库中唯一运行的事务。隔离性通过锁机制和多版本并发控制(MVCC)来实现。

**持久性(Durability):**一旦事务被提交,其结果就是永久性的,即使系统发生崩溃也不会丢失。InnoDB通过Redo Log(重做日志)和Double Write Buffer(双写缓冲区)来保证持久性

二、事务隔离级别

2.1 并发事务引发的问题

当多个事务同时操作同一数据时,如果不加以控制,会出现以下三类典型问题:

**脏读(Dirty Read):**一个事务读取到另一个未提交事务的修改数据。如果那个事务最终回滚,读取到的就是"脏数据"------这些数据从未真正存在于数据库中。

**不可重复读(Non-Repeatable Read):**一个事务内两次读取同一行数据,但两次读取的结果不同。这是因为在两次读取之间,另一个事务修改并提交了该行数据。

**幻读(Phantom Read):**一个事务按照相同条件两次查询,第二次查询结果的行数发生了变化(增多或减少)。这是因为另一个事务在此期间插入或删除了满足条件的行。幻读与不可重复读的核心区别:幻读关注的是行数的变化(INSERT/DELETE),不可重复读关注的是同一行内容的变化(UPDATE)。

2.2 四种隔离级别

SQL标准定义了四种事务隔离级别,从低到高依次为:

**READ UNCOMMITTED(读未提交):**最低级别。事务可以读取其他事务未提交的修改。存在脏读、不可重复读、幻读问题。几乎不用于生产环境,仅在某些需要非阻塞读的日志/监控场景中有零星应用。

**READ COMMITTED(读已提交):**事务只能读取其他事务已提交的数据,解决了脏读问题。但不可重复读和幻读仍然存在。这是Oracle和PostgreSQL的默认隔离级别。

**REPEATABLE READ(可重复读):**事务在执行期间看到的数据始终保持一致------同一事务内多次读取同一行数据结果相同。解决了脏读和不可重复读问题,但理论上仍存在幻读。这是MySQL InnoDB的默认隔离级别。

**SERIALIZABLE(可串行化):**最高级别。事务完全串行执行,通过强制排序彻底杜绝所有并发问题。代价是并发性能极低,通常只在金融等对数据一致性要求极高的场景中使用。

三、@Transactional 用法

1.准备条件

在非springboot项目中,想要使用@Transactional 注解,要在配置类中开启事务注解的支持

java 复制代码
@Configuration
@ComponentScan("com.xxx")
@PropertySource("classpath:jdbc.properties")
@EnableAspectJAutoProxy //开启aspectj注解的支持 aop动态代理
@EnableTransactionManagement //开启事务注解的支持
public class JavaConfig {
......
2. 基本使用

@Transactional注解可用于类 或public方法上,用于声明该类/方法需要被事务管理,Spring会自动完成"开启事务→执行方法→提交事务/回滚事务"的流程。

java 复制代码
    @Transactional(rollbackFor = Exception.class)
    public void changAge(){
        dao.updateAgeById(110,1);
    }
3.关键属性
  • rollbackFor:指定需要回滚的异常类型,最常用 rollbackFor = Exception.class。默认只回滚RuntimeException和Error, checked异常(如IOException)不会回滚,必须显式指定。
  • propagation:事务传播机制,默认REQUIRED(有事务则加入,无则新建),日常开发无需修改,特殊场景(如嵌套事务)可调整。
  • readOnly:设为true表示只读事务,仅用于查询场景,可优化数据库性能(避免开启写事务)。
  • timeout:超时时间,默认:永远不超时 -1,设置timeout = 时间 秒数 超时时间,就会回滚事务和释放异常
3.1事务的传播机制
  • ‌REQUIRED ‌(默认):当前有事务就加入,没有就新建一个。
    • 最常用,适合大多数业务场景。
    • 内外层方法共享同一个事务,任意一方失败都会回滚。
  • ‌**SUPPORTS** ‌:当前有事务就加入,没有就以非事务方式执行。
    • 方法本身不强制要求事务。
    • 适合查询类操作,有事务时能参与,没有也不影响。
  • ‌**MANDATORY** ‌:必须在现有事务中运行,没有事务就抛异常。
    • 强制要求调用方已开启事务。
    • 适合内部辅助方法,确保不会被单独调用。
  • ‌**REQUIRES_NEW** ‌:总是新建事务,如果有现有事务就挂起。
    • 内外层事务独立,互不影响。
    • 适合日志记录、审计等需要独立提交的操作。
  • ‌**NOT_SUPPORTED** ‌:以非事务方式执行,如果有现有事务就挂起。
    • 方法本身不需要事务。
    • 适合耗时较长且不需要回滚的操作。
  • ‌**NEVER** ‌:必须以非事务方式执行,有事务就抛异常。
    • 与 MANDATORY 相反。
    • 适合明确不需要事务的场景。
  • ‌**NESTED** ‌:当前有事务就创建嵌套事务,没有就新建事务。
    • 嵌套事务可以独立回滚,不影响外层。
    • 适合需要部分回滚的复杂业务场景。
3.2 显示的给事务添加传播机制
java 复制代码
  @Transactional(propagation = Propagation.REQUIRED)
    public void changAge(){
        dao.updateAgeById(110,1);
    }
4.什么时候该用?什么时候不用?

核心原则:有多条写操作(增/删/改),需要原子性(要么全成功、要么全回滚),就必须加;单条写操作、纯查询,坚决不加。

| 场景 | 是否加@Transactional | 说明 |
| 多条增删改(如订单创建+扣库存) | 是 | 保证原子性,避免部分成功、部分失败 |
| 单条增/删/改 | 否 | 数据库单条SQL本身就是原子性,加注解多余且耗性能 |
| 纯查询(查列表、查详情) | 否 | 查询无需提交/回滚,加注解会开启无用事务 |
| 复杂多表联查(大数据量) | 可选 | 可加@Transactional(readOnly = true)优化性能 |

无DB操作(仅参数校验、调用第三方接口) 否 无事务需要管理,加注解无意义
5.@Transactional 失效场景(高频坑,必避)

事务失效的核心本质只有一个:没有通过Spring生成的代理对象调用带事务的方法,或代理机制被破坏。以下是100%会遇到的失效场景,按出现频率排序。

1. 同类内部方法调用(自调用,最常见)

这是最容易踩的坑!同一个类中,无事务方法调用有事务方法,事务必失效。

一个方法加了事务注解,那么spring会对这个类生成一个对应的代理对象,放入spring容器。

如果同类自己调用自己,使用的this对象。不是用的代理对象。this对象调用其他方式不会对其他方法增强的。只有代理对象才会对要调用的方法增强。所以事务会失效。

只有代理对象里面才有方法被事务加持。

java 复制代码
    @Service
    public class UserService {
        // 无事务方法 
        public void methodA() { 
            // 自调用:this.methodB(),不走代理,事务失效
            methodB(); 
        } 
        // 有事务方法 
        @Transactional(rollbackFor = Exception.class) 
        public void methodB() {
            // 数据库操作
        }
    }

原因:methodA直接调用methodB,是原生对象(this)调用,绕过了Spring代理,AOP无法拦截事务逻辑。

注意:哪怕两个方法都加@Transactional,自调用依然失效!

2. 方法修饰符错误(private/final/static)
java 复制代码
    // 失效:private方法,动态代理无法重写
    @Transactional 
    private void save() {}
    // 失效:final方法,动态代理无法重写 
    @Transactional 
    public final void save() {} 
    // 失效:static方法,不属于对象实例,代理无法拦截 
    @Transactional 
    public static void save() {}

规则:@Transactional只能用在public、非final、非static方法上。

3. 异常被try-catch"吃掉"
java 复制代码
    @Transactional(rollbackFor = Exception.class) 
    public void save() { 
        try { 
            // 数据库操作 
            insertData(); 
        } catch (Exception e) { 
            // 异常被捕获,未抛出,Spring无法感知异常,不会回滚 
            e.printStackTrace(); 
        } 
    }

原因:Spring事务默认只有"未捕获的异常"才会触发回滚,捕获异常后不抛出,事务会正常提交。

4. 类未被Spring管理(无@Component/@Service等)
java 复制代码
// 失效:未加@Service,不是Spring Bean,AOP无法代理 
public class UserService { 
        @Transactional 
        public void save() {} 
    }

原因:只有Spring管理的Bean,才会被AOP代理,事务注解才会生效。

四、用法总结:

场景1:同一个类中,A→B→C 多层调用,均有写库操作(需整体原子性)

最优方案:只给最外层入口方法加事务,内部方法改为private,直接调用(无需注入自己、无需AopContext,无循环依赖)。

java 复制代码
    @Service public class BizService {
        // 唯一事务入口:只给最外层public方法加注解 
        @Transactional(rollbackFor = Exception.class) public void methodA() {
            // A自身写库操作 
            insertA();
            // 内部直接调用private方法,无代理问题,事务生效 
            methodB();
        }
        // 内部方法:private,不加事务注解 
        private void methodB() {
            // B自身写库操作 
            insertB();
            methodC();
        }
            // 内部方法:private,不加事务注解 
        private void methodC() { 
            // C自身写库操作 
            insertC(); 
            // 任意环节抛异常,整体回滚 
            if (true) { throw new RuntimeException("异常回滚");} 
        } 
        // 私有数据库操作方法 
        private void insertA() { /* mapper操作 */ }
        private void insertB() { /* mapper操作 */ } 
        private void insertC() { /* mapper操作 */ } }
        
    }
场景2:跨类调用(A→B→C,分属不同Service)

最优方案:只给最外层入口方法加事务,内部跨类调用天然走代理,无需额外处理。

相关推荐
运维行者_1 小时前
供应链安全成最大风险源,企业如何构建可控的运维体系?
运维·开发语言·网络·数据库·安全·云计算·流量监控
高铭杰1 小时前
Postgresql源码(160)新的共享内存注册机制,为动态调整shared_buffers打基础
数据库·postgresql
数据库小学妹1 小时前
AI Agent连数据库怎么控权限?只读账号的4个缺口
数据库·人工智能·安全·数据库权限·审计·ai数据库·只读账号
瑞克-桑切斯1 小时前
6、MySQL 总结
数据库
凤凰院凶涛QAQ1 小时前
《MySQL内核拆解:从SQL到InnoDB》(三) : 数据库设计 & 聚合查询 & 分组查询
数据库·sql·mysql
Decay__2 小时前
01_MySQL 体系架构全解 —— 连接层、SQL 层、存储引擎层与 InnoDB 深入
运维·数据库·sql·mysql·架构
Linux_cy3 小时前
nginx 502 排查手册:九种原因分类、定位命令与检查顺序
linux·运维·服务器·网络·数据库·nginx·ssh
在多学一点3 小时前
MySQL_DDL的基本命令
数据库·mysql·adb
是程序喵呀3 小时前
Redis实现持久化
数据库·redis·缓存