SpringBoot Spring事务完整版详解:@Transactional注解实操 + 七大事务传播机制用法

文章目录

做后端开发,Spring事务是CRUD核心刚需,也是面试必问高频考点 。不管是电商下单、转账扣款、订单创建、库存扣减,只要涉及多数据库增删改操作,必须保证要么全部成功,要么全部回滚,不能出现一半成功、一半失败的数据错乱情况。

很多同学只会简单加个@Transactional注解,但是注解失效不知道为啥、事务传播机制不会选、嵌套业务事务乱套、线上数据出问题不会排查

这篇博客以SpringBoot最新版本为例,纯实战、讲细节、带完整代码、讲透用法+原理+踩坑+传播机制选型,看完彻底搞定Spring事务,开发直接用,面试直接背。


一、先搞懂:什么是Spring事务?核心作用是什么?

1、数据库原生事务基础(Spring事务的根本)

Spring事务底层不是自己造的事务,Spring只是对数据库原生事务做了一层封装和管理。数据库原生事务必须满足ACID四大特性:

  • A原子性:多步操作不可分割,要么全成功,要么全回滚

  • C一致性:事务执行前后,数据整体状态合法一致

  • I隔离性:多个事务并发执行,互相隔离互不干扰

  • D持久性:事务提交后,数据永久落地,断电也不丢失

简单一句话:事务就是用来保证多数据库操作,要么同生、要么同死。

2、为什么要用Spring事务,不直接写数据库事务?

原生数据库事务需要手动写:开启事务、执行业务代码、异常捕获、手动回滚、手动提交,代码冗余极高,每个业务都要写重复模板。

Spring事务核心优势:AOP切面自动管理事务,我们只需要加一个注解,Spring自动帮我们:

  • 方法执行前:自动开启事务

  • 方法正常结束:自动提交事务

  • 方法抛出异常:自动回滚事务

零手动编码,极简开发,这就是@Transactional注解的核心价值。


二、SpringBoot开启事务基础配置(一步到位)

1、无需额外复杂配置,SpringBoot自动生效

SpringBoot项目只要引入数据库驱动+MyBatis/MyBatis-Plus/JPA依赖,事务管理器自动自动装配,无需手动创建Bean,直接开箱即用。

2、启动类必须加开启事务注解(必加)

在SpringBoot启动类上添加@EnableTransactionManagement,开启Spring事务管理开关,高版本SpringBoot部分可省略,但生产环境建议必加,防止事务失效

java 复制代码
@SpringBootApplication
@EnableTransactionManagement // 开启事务管理,核心必备
public class TransactionApplication {
    public static void main(String[] args) {
        SpringApplication.run(TransactionApplication.class, args);
    }
}

三、@Transactional注解完整实操用法(SpringBoot示例)

1、注解加在哪里?规范用法

  • 推荐加在Service层public方法上:业务逻辑都在Service,事务控制最标准

  • 加在类上:当前类所有public方法都默认开启事务

  • 不能加在非public方法上(private/protected):事务直接失效(核心踩坑点)

2、最简单基础使用示例(转账业务场景)

业务场景:A用户给B用户转账100元,两步数据库操作:A扣钱、B加钱。任何一步报错,必须全部回滚,不能扣钱不加钱。

① Service层业务代码(加事务注解)

java 复制代码
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    // 核心事务注解:当前方法所有数据库操作受事务管理
    @Transactional
    @Override
    public void transferMoney(Long fromUserId, Long toUserId, Integer money) {
        // 1. 转出用户扣钱
        accountMapper.reduceMoney(fromUserId, money);

        // 模拟业务代码异常,测试事务是否回滚
        int i = 1 / 0;

        // 2. 转入用户加钱
        accountMapper.addMoney(toUserId, money);
    }
}

② 效果说明

代码中模拟除零异常,程序执行报错,Spring自动触发事务回滚:A用户不会扣钱,B用户不会加钱,数据不会错乱,事务生效。

3、@Transactional核心常用参数详解(开发必配)

(1)rollbackFor:指定什么异常才回滚(最重要参数)

默认规则(大坑) :Spring事务默认只对RuntimeException运行时异常和Error错误回滚,如果是普通编译异常Exception,默认不回滚!

生产环境标准写法:所有异常都强制回滚

java 复制代码
// 只要抛出任何Exception,全部回滚,开发通用标配
@Transactional(rollbackFor = Exception.class)

(2)propagation:事务传播机制(下文重点单独精讲)

控制方法嵌套调用时,事务怎么传递、怎么共用、怎么新建独立事务,七大传播机制核心就在这个参数。

(3)isolation:事务隔离级别

控制多事务并发读写的数据隔离规则,解决脏读、不可重复读、幻读,开发一般默认即可,不用手动改

(4)timeout:事务超时时间

单位秒,事务执行超过时间自动回滚,防止事务卡死锁表。

(5)readOnly:是否只读事务

查询方法设置为true,提升查询性能,禁止修改数据。


四、必看:@Transactional事务5大失效踩坑点(90%人都踩过)

注解加了事务却不生效,数据照样错乱,基本都是下面5个原因,开发直接避坑:

  1. 方法不是public修饰:private/protected方法,AOP无法代理,事务直接失效

  2. 内部this调用本类事务方法:没有走Spring代理对象,事务不生效

  3. 抛出编译异常Exception,没配置rollbackFor:默认不回滚

  4. 数据库引擎不是InnoDB:MyISAM不支持事务,怎么加注解都没用

  5. 异常被try-catch捕获吃掉:没抛出异常,Spring感知不到,不会回滚


五、Spring七大事务传播机制详解(核心重点+场景+代码示例)

1、什么是事务传播机制?一句话通俗理解

传播机制就是:当一个带事务的方法A,调用另一个带事务的方法B,两个事务到底怎么合并、怎么传递、要不要共用、要不要新建、互不干扰。

简单说:方法嵌套调用时,事务的协作规则。

Spring一共7种传播属性,不用全死记,只记3个常用的,剩下看懂场景即可

2、提前定义两个测试业务方法(统一演示所有传播机制)

  • 主方法MainService:外层调用方法

  • 子方法SubService:内层被调用方法,单独加不同传播机制

3、七大传播机制逐个精讲(含义+适用场景+执行结果)

① REQUIRED(默认传播机制,开发最常用)

核心规则:有事务就加入当前事务,没有就新建事务。

通俗理解:有福同享,有难同当,大家是同一个事务,一荣俱荣一损俱损。

适用场景:绝大多数普通业务增删改,主业务和子业务必须同成功同回滚。

执行效果:外层有事务,内外共用一个事务,任何一处报错,全部回滚。

② SUPPORTS(支持事务,非必需)

核心规则:有事务就加入,没事务就非事务运行。

通俗理解:有就一起,没有也行,随遇而安。

适用场景:纯查询业务,不需要强制事务,有事务就共用,没有也不影响。

③ MANDATORY(强制要求事务)

核心规则:必须在已有事务内运行,没有事务直接报错。

通俗理解:没事务我就不干活,直接抛异常。

适用场景:核心关键子业务,必须依赖主事务,禁止单独执行。

④ REQUIRES_NEW(新建独立事务,核心常用)

核心规则不管外层有没有事务,每次都新建一个全新独立事务,新旧事务互不干扰

通俗理解:各玩各的,你的事务我不掺和,我的事务你管不着。外层回滚不影响内层,内层报错不影响外层。

适用场景:日志记录、操作流水、消息记录,哪怕主业务事务回滚,日志也要永久保存,不能跟着回滚。

⑤ NOT_SUPPORTED(强制非事务运行)

核心规则:永远非事务运行,有外层事务也先挂起,执行完再恢复。

适用场景:实时统计、临时查询、不需要事务的临时操作。

⑥ NEVER(强制非事务,有事务就报错)

核心规则:必须非事务运行,检测到有事务直接抛异常。

适用场景:禁止事务执行的特殊统计任务。

⑦ NESTED(嵌套事务)

核心规则:外层有事务,就嵌套为子事务(有保存点),外层回滚子必回滚,子回滚外层不影响;外层没事务就新建事务。

适用场景:部分子业务可独立回滚,主业务不受影响的嵌套场景。


六、两大高频传播机制代码实战对比(REQUIRED vs REQUIRES_NEW)

1、场景需求

主业务:下单创建订单;子业务:记录操作日志。要求:下单失败回滚,但日志必须保存,不能回滚。

2、REQUIRED默认模式(错误用法,不满足需求)

主方法和日志方法共用一个事务,下单报错回滚,日志也跟着回滚,日志丢失,业务不合格。

3、REQUIRES_NEW新模式(正确用法,生产标配)

① 主业务方法(默认REQUIRED)

java 复制代码
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private LogService logService;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void createOrder() {
        // 1. 创建订单
        orderMapper.insertOrder();

        // 2. 记录日志(独立事务)
        logService.saveOperateLog();

        // 模拟下单异常
        int i = 1 / 0;
    }
}

② 日志子方法(REQUIRES_NEW独立事务)

java 复制代码
@Service
public class LogServiceImpl implements LogService {

    // 新建独立事务,和主事务隔离
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    @Override
    public void saveOperateLog() {
        // 插入操作日志
        logMapper.insertLog();
    }
}

③ 最终执行效果

主业务下单报错自动回滚 ,订单创建失败;日志方法是独立事务,执行完直接提交,日志永久保存不回滚,完美满足生产业务需求。


七、关键答疑:有了@Transactional注解,是不是就不用管数据库事务、不用管锁了?(悲观锁/乐观锁必看)

很多新手最大误区:

以为加个@Transactional,事务就全包了,并发、超卖、数据冲突都自动解决了。

大错特错!完全两码事!

1、第一句话先定调:事务 和 锁,解决的是两个完全不同的问题

东西 解决什么问题 核心作用 能不能防并发超卖?
@Transactional 事务 解决多步操作要么全成功、要么全回滚 保证原子性,防止一半成功一半失败 不能!完全防不住并发修改
数据库锁/悲观锁/乐观锁 解决多个人同时改同一条数据,并发冲突问题 保证并发数据安全,防止超卖、数据覆盖 专门用来防并发

2、有了Spring事务,数据库事务还用不用管?

答案:不用你手动写代码管,但底层数据库事务依然必须存在。

① Spring事务 ≠ 替代数据库事务

再次强调:Spring事务只是"帮你自动开启、提交、回滚"数据库事务

底层干活的还是MySQL原生事务,Spring只是给你套了层AOP注解而已。

就像:

你不用自己点火做饭(不用手动写begin/commit/rollback)

但饭还是要用火做(数据库事务必须存在)

② 你不用写原生事务代码,但必须懂事务隔离级别

SpringBoot事务只管:原子性(成功失败)

不管:并发争抢、数据覆盖、超卖


3、重点:加了事务,为什么还会出现超卖、数据错乱?

举个最经典例子:商品库存只剩1件,两个人同时下单。

两个请求都加了@Transactional:

  1. 两个人同时查到库存=1

  2. 两个人都判断库存>0,都通过

  3. 两个人都减库存

  4. 最后库存变成-1,超卖了!

为什么加了事务还超卖?

因为事务只保证:每个人自己的操作要么全成功、要么全回滚。

事务不保证:两个人修改同一条数据互不冲突!

事务不处理并发排队问题!


八、悲观锁、乐观锁详细讲解(实战场景+SpringBoot适配)

1、什么时候必须加锁?

只要满足一句话,必须加锁,不加必出问题

多用户并发修改同一条数据 → 只用事务不够,必须加锁!

比如:

  • 库存扣减

  • 订单创建

  • 余额转账

  • 积分变动

  • 秒杀活动


2、悲观锁是什么?什么时候用?

核心思想:我先锁住,别人别改,我改完你再改。

每次操作数据,先加锁,独占资源,其他事务阻塞等待。

① 数据库原生悲观锁(for update)
java 复制代码
// 查询的时候直接上锁
select * from goods where id = 1 for update;

② 特点

  • 强一致性,绝对不会超卖

  • 并发性能差,排队等待

  • 容易死锁

③ 适用场景

并发不高、资金交易、对账、金额核心数据,必须保证绝对安全。


3、乐观锁是什么?什么时候用?

核心思想:我不上锁,我相信没人改,提交时校验版本号。

不加锁,通过version版本号控制更新,修改前判断版本号是否一致。

① 原理

  1. 查询数据时,带出version=1

  2. 更新时要求:where id=? and version=1

  3. 更新成功version+1

  4. 如果别人改过,version变了,更新行数=0,修改失败

② 特点

  • 性能高,不上锁,不阻塞

  • 并发高适合用

  • 失败需要重试

③ 适用场景

高并发秒杀、库存扣减、抢购、商品热点数据。


十、SpringBoot悲观锁&乐观锁完整实操代码(直接复制运行,实测防超卖)

前面讲清了理论,这一节直接上生产级可落地完整代码 ,基于库存扣减经典超卖场景,分别实现:无锁事务(必超卖)、悲观锁(防超卖)、乐观锁(防超卖),搭配数据库表、Mapper、Service全套代码,导入就能测试,直观看到效果差异。

1、先准备数据库库存表(必备)

创建商品库存数据表,悲观锁无需额外字段,乐观锁必须新增version版本号字段做校验。

sql 复制代码
-- 商品库存表:悲观锁、乐观锁通用基础表
CREATE TABLE goods_stock (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    goods_name VARCHAR(100) NOT NULL COMMENT '商品名称',
    stock_num INT NOT NULL DEFAULT 0 COMMENT '剩余库存数量',
    version INT NOT NULL DEFAULT 1 COMMENT '乐观锁版本号(悲观锁也保留,不影响)',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品库存表';

-- 初始化测试数据:库存仅10件,模拟限量抢购
INSERT INTO goods_stock(goods_name,stock_num,version) VALUES ('爆款秒杀手机',10,1);

2、实体类Entity代码(MyBatis/MyBatis-Plus通用)

java 复制代码
@Data
@TableName("goods_stock")
public class GoodsStock {
    @TableId(type = IdType.AUTO)
    private Long id;

    // 商品名称
    private String goodsName;

    // 剩余库存数量
    private Integer stockNum;

    // 乐观锁版本号
    private Integer version;
}

3、Mapper层接口代码

java 复制代码
@Mapper
public interface GoodsStockMapper {

    // ===================== 悲观锁专用SQL =====================
    // for update 行级悲观锁:查询当前商品库存并上锁,其他事务必须等待释放才能操作
    @Select("select * from goods_stock where id = #{goodsId} for update")
    GoodsStock getStockByGoodsIdForUpdate(Long goodsId);

    // 悲观锁库存扣减
    @Update("update goods_stock set stock_num = stock_num - 1 where id = #{goodsId}")
    int deductStockPessimistic(Long goodsId);

    // ===================== 乐观锁专用SQL =====================
    // 普通查询库存(不上锁)
    @Select("select * from goods_stock where id = #{goodsId}")
    GoodsStock getStockByGoodsId(Long goodsId);

    // 乐观锁库存扣减:必须匹配id+version,版本不对更新失败,实现并发控制
    @Update("update goods_stock set stock_num = stock_num - 1, version = version + 1 where id = #{goodsId} and version = #{version}")
    int deductStockOptimistic(@Param("goodsId") Long goodsId, @Param("version") Integer version);
}

4、第一种:只加事务不加锁(演示:必超卖,反面案例)

只加@Transactional事务注解,没有任何锁,并发下单必然超卖,新手千万别这么写。

java 复制代码
@Service
public class StockServiceImpl implements StockService {

    @Autowired
    private GoodsStockMapper stockMapper;

    // 只加事务,无任何锁,并发必超卖!
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void deductStockNoLock(Long goodsId) {
        // 1. 查询库存
        GoodsStock stock = stockMapper.getStockByGoodsId(goodsId);
        // 2. 判断库存是否充足
        if (stock.getStockNum() > 0) {
            // 3. 扣减库存
            stockMapper.deductStockPessimistic(goodsId);
            System.out.println("下单扣减成功,当前剩余库存:" + (stock.getStockNum() - 1));
        } else {
            throw new RuntimeException("库存不足,下单失败");
        }
    }
}

测试结果 :开启多线程模拟并发抢购,库存会扣成负数,事务原子性生效,但并发冲突没解决,超卖必现


5、第二种:SpringBoot + 悲观锁实操代码(资金核心业务专用,防超卖)

基于数据库for update行级悲观锁,事务内查询上锁,同一时间只有一个事务能操作数据,其他排队等待,绝对不会超卖,数据强一致

java 复制代码
@Service
public class StockServiceImpl implements StockService {

    @Autowired
    private GoodsStockMapper stockMapper;

    // 事务+悲观锁,防止超卖,资金对账核心业务首选
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void deductStockPessimisticLock(Long goodsId) {
        // 1. 查询库存 + for update悲观锁上锁
        // 只要事务没提交,其他线程查询该行数据直接阻塞等待
        GoodsStock stock = stockMapper.getStockByGoodsIdForUpdate(goodsId);

        // 2. 判断库存
        if (stock.getStockNum() > 0) {
            // 3. 扣减库存
            stockMapper.deductStockPessimistic(goodsId);
            System.out.println("悲观锁下单成功,当前剩余库存:" + (stock.getStockNum() - 1));
        } else {
            throw new RuntimeException("库存不足,下单失败");
        }
        // 事务提交瞬间,悲观锁自动释放,下一个线程开始执行
    }
}

核心关键点 :锁在事务范围内生效,事务不提交,锁不释放;行级锁只锁当前商品数据,不锁全表,性能比表锁好。

测试结果 :多线程并发请求,库存严格递减,不会出现负数,彻底杜绝超卖


6、第三种:SpringBoot + 乐观锁实操代码(高并发秒杀专用,高性能防超卖)

无锁设计,依靠version版本号校验,更新时版本不匹配直接更新失败,不阻塞线程、并发性能高,适合秒杀、抢购等高并发场景。额外加循环重试机制,避免正常抢购失败。

java 复制代码
@Service
public class StockServiceImpl implements StockService {

    @Autowired
    private GoodsStockMapper stockMapper;

    // 事务+乐观锁+重试机制,高并发秒杀专用
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void deductStockOptimisticLock(Long goodsId) {
        // 重试次数:并发更新失败自动重试3次,提升抢购成功率
        int retryCount = 3;
        while (retryCount > 0) {
            // 1. 无锁查询库存和当前版本号
            GoodsStock stock = stockMapper.getStockByGoodsId(goodsId);

            // 2. 判断库存
            if (stock.getStockNum() <= 0) {
                throw new RuntimeException("库存不足,下单失败");
            }

            // 3. 乐观锁更新:必须id和version同时匹配才更新成功
            int rows = stockMapper.deductStockOptimistic(goodsId, stock.getVersion());

            // 4. 更新行数>0:更新成功,扣减完成
            if (rows > 0) {
                System.out.println("乐观锁下单成功,当前版本号:" + (stock.getVersion() + 1));
                return;
            }

            // 更新行数=0:版本号已被修改,并发争抢失败,重试次数-1
            retryCount--;
            System.out.println("并发争抢冲突,自动重试剩余次数:" + retryCount);
        }

        // 重试完毕仍失败,抛出异常
        throw new RuntimeException("抢购人数过多,下单失败,请重试");
    }
}

核心原理 :多人同时抢购,只有一个人版本号匹配更新成功,其他人更新返回0,自动重试,不阻塞不排队,并发性能拉满,无超卖


7、三种写法最终对比总结(开发直接照选)

  1. 只加事务无锁 :代码最简单,并发必超卖,线上禁止使用

  2. 事务+悲观锁 :强数据一致,排队执行,资金、余额、对账业务必用

  3. 事务+乐观锁 :高并发高性能,无阻塞重试,秒杀、库存、抢购活动必用

九、全文终极总结

  1. @Transactional只管事务原子性(要么全成功全回滚),不管并发!

  2. 有注解事务,底层数据库事务依然生效,只是不用你手动写代码。

  3. 只用事务不加锁,并发必超卖,数据必错乱。

  4. 并发修改同一条数据:必须用悲观锁 or 乐观锁。

  5. 资金核心数据用悲观锁,高并发秒杀库存用乐观锁。

  6. 普通增删改事务 :直接用 @Transactional\(rollbackFor = Exception\.class\) 默认传播REQUIRED

  7. 日志、流水、记录不随主业务回滚:传播机制用REQUIRES_NEW

  8. 查询业务:用SUPPORTS + readOnly = true

  9. 事务失效优先检查:public方法、是否内部调用、是否捕获异常、是否配rollbackFor

相关推荐
ffqws_2 小时前
Spring Boot 配置读取全解析:从 application.yml 到 Java 对象的完整链路
java·数据库·spring boot
云烟成雨TD2 小时前
Spring AI 1.x 系列【29】Embedding Model(嵌入模型)
java·人工智能·spring
RuoyiOffice2 小时前
SpringBoot+Vue3 实现 OA 公文外来文与归档台账:外部收文、BPM办理、三类公文统一归档
spring boot·微服务·uni-app·vue·ruoyi·anti-design-vue·ruoyioffice
callJJ13 小时前
Spring Data Redis 两种编程模型详解:同步 vs 响应式
java·spring boot·redis·python·spring
海兰13 小时前
【第27篇】Micrometer + Zipkin
人工智能·spring boot·alibaba·spring ai
phltxy13 小时前
Spring Cloud 分布式服务部署实战:从 0 到 1 实现微服务上线
spring·spring cloud·微服务
海兰14 小时前
【第28篇】可观测性实战:LangFuse 方案详解
人工智能·spring boot·alibaba·spring ai
RuoyiOffice15 小时前
SpringBoot+Vue3 企业考勤如何处理法定假期?节假日方案、调休补班与工作日判断链路拆解
spring boot·后端·vue·anti-design-vue·ruoyioffice·假期·人力
xmjd msup15 小时前
spring security 超详细使用教程(接入springboot、前后端分离)
java·spring boot·spring