文章目录
- 一、先搞懂:什么是Spring事务?核心作用是什么?
- 1、数据库原生事务基础(Spring事务的根本)
- 2、为什么要用Spring事务,不直接写数据库事务?
- 二、SpringBoot开启事务基础配置(一步到位)
- 1、无需额外复杂配置,SpringBoot自动生效
- 2、启动类必须加开启事务注解(必加)
- 三、@Transactional注解完整实操用法(SpringBoot示例)
- 1、注解加在哪里?规范用法
- 2、最简单基础使用示例(转账业务场景)
- ① Service层业务代码(加事务注解)
- ② 效果说明
- 3、@Transactional核心常用参数详解(开发必配)
- (1)rollbackFor:指定什么异常才回滚(最重要参数)
- (2)propagation:事务传播机制(下文重点单独精讲)
- (3)isolation:事务隔离级别
- (4)timeout:事务超时时间
- (5)readOnly:是否只读事务
- 四、必看:@Transactional事务5大失效踩坑点(90%人都踩过)
- 五、Spring七大事务传播机制详解(核心重点\+场景\+代码示例)
- 1、什么是事务传播机制?一句话通俗理解
- 2、提前定义两个测试业务方法(统一演示所有传播机制)
- 3、七大传播机制逐个精讲(含义\+适用场景\+执行结果)
- ① REQUIRED(默认传播机制,开发最常用)
- ② SUPPORTS(支持事务,非必需)
- ③ MANDATORY(强制要求事务)
- ④ REQUIRES\_NEW(新建独立事务,核心常用)
- ⑤ NOT_SUPPORTED(强制非事务运行)
- ⑥ NEVER(强制非事务,有事务就报错)
- ⑦ NESTED(嵌套事务)
- 六、两大高频传播机制代码实战对比(REQUIRED vs REQUIRES_NEW)
- 1、场景需求
- 2、REQUIRED默认模式(错误用法,不满足需求)
- 3、REQUIRES_NEW新模式(正确用法,生产标配)
- ① 主业务方法(默认REQUIRED)
- ② 日志子方法(REQUIRES_NEW独立事务)
- ③ 最终执行效果
- 七、关键答疑:有了@Transactional注解,是不是就不用管数据库事务、不用管锁了?(悲观锁/乐观锁必看)
- 1、第一句话先定调:事务 和 锁,解决的是两个完全不同的问题
- 2、有了Spring事务,数据库事务还用不用管?
- ① Spring事务 ≠ 替代数据库事务
- ② 你不用写原生事务代码,但必须懂事务隔离级别
- 3、重点:加了事务,为什么还会出现超卖、数据错乱?
- 八、悲观锁、乐观锁详细讲解(实战场景+SpringBoot适配)
- 1、什么时候必须加锁?
- 2、悲观锁是什么?什么时候用?
- ① 数据库原生悲观锁(for update)
- ② 特点
- ③ 适用场景
- 3、乐观锁是什么?什么时候用?
- ① 原理
- ② 特点
- ③ 适用场景
- 十、SpringBoot悲观锁&乐观锁完整实操代码(直接复制运行,实测防超卖)
- 1、先准备数据库库存表(必备)
- 2、实体类Entity代码(MyBatis/MyBatis-Plus通用)
- 3、Mapper层接口代码
- 4、第一种:只加事务不加锁(演示:必超卖,反面案例)
- 5、第二种:SpringBoot + 悲观锁实操代码(资金核心业务专用,防超卖)
- 6、第三种:SpringBoot + 乐观锁实操代码(高并发秒杀专用,高性能防超卖)
- 7、三种写法最终对比总结(开发直接照选)
- 九、全文终极总结
做后端开发,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部分可省略,但生产环境建议必加,防止事务失效。
@SpringBootApplication@EnableTransactionManagement// 开启事务管理,核心必备publicclassTransactionApplication{publicstaticvoidmain(String[]args){SpringApplication.run(TransactionApplication.class,args);}}三、@Transactional注解完整实操用法(SpringBoot示例)
1、注解加在哪里?规范用法
推荐加在Service层public方法上:业务逻辑都在Service,事务控制最标准
加在类上:当前类所有public方法都默认开启事务
不能加在非public方法上(private/protected):事务直接失效(核心踩坑点)
2、最简单基础使用示例(转账业务场景)
业务场景:A用户给B用户转账100元,两步数据库操作:A扣钱、B加钱。任何一步报错,必须全部回滚,不能扣钱不加钱。
① Service层业务代码(加事务注解)
@ServicepublicclassAccountServiceImplimplementsAccountService{@AutowiredprivateAccountMapperaccountMapper;// 核心事务注解:当前方法所有数据库操作受事务管理@Transactional@OverridepublicvoidtransferMoney(LongfromUserId,LongtoUserId,Integermoney){// 1. 转出用户扣钱accountMapper.reduceMoney(fromUserId,money);// 模拟业务代码异常,测试事务是否回滚inti=1/0;// 2. 转入用户加钱accountMapper.addMoney(toUserId,money);}}② 效果说明
代码中模拟除零异常,程序执行报错,Spring自动触发事务回滚:A用户不会扣钱,B用户不会加钱,数据不会错乱,事务生效。
3、@Transactional核心常用参数详解(开发必配)
(1)rollbackFor:指定什么异常才回滚(最重要参数)
默认规则(大坑):Spring事务默认只对RuntimeException运行时异常和Error错误回滚,如果是普通编译异常Exception,默认不回滚!
生产环境标准写法:所有异常都强制回滚
// 只要抛出任何Exception,全部回滚,开发通用标配@Transactional(rollbackFor=Exception.class)(2)propagation:事务传播机制(下文重点单独精讲)
控制方法嵌套调用时,事务怎么传递、怎么共用、怎么新建独立事务,七大传播机制核心就在这个参数。
(3)isolation:事务隔离级别
控制多事务并发读写的数据隔离规则,解决脏读、不可重复读、幻读,开发一般默认即可,不用手动改。
(4)timeout:事务超时时间
单位秒,事务执行超过时间自动回滚,防止事务卡死锁表。
(5)readOnly:是否只读事务
查询方法设置为true,提升查询性能,禁止修改数据。
四、必看:@Transactional事务5大失效踩坑点(90%人都踩过)
注解加了事务却不生效,数据照样错乱,基本都是下面5个原因,开发直接避坑:
方法不是public修饰:private/protected方法,AOP无法代理,事务直接失效
内部this调用本类事务方法:没有走Spring代理对象,事务不生效
抛出编译异常Exception,没配置rollbackFor:默认不回滚
数据库引擎不是InnoDB:MyISAM不支持事务,怎么加注解都没用
异常被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)
@ServicepublicclassOrderServiceImplimplementsOrderService{@AutowiredprivateLogServicelogService;@Transactional(rollbackFor=Exception.class)@OverridepublicvoidcreateOrder(){// 1. 创建订单orderMapper.insertOrder();// 2. 记录日志(独立事务)logService.saveOperateLog();// 模拟下单异常inti=1/0;}}② 日志子方法(REQUIRES_NEW独立事务)
@ServicepublicclassLogServiceImplimplementsLogService{// 新建独立事务,和主事务隔离@Transactional(propagation=Propagation.REQUIRES_NEW,rollbackFor=Exception.class)@OverridepublicvoidsaveOperateLog(){// 插入操作日志logMapper.insertLog();}}③ 最终执行效果
主业务下单报错自动回滚,订单创建失败;日志方法是独立事务,执行完直接提交,日志永久保存不回滚,完美满足生产业务需求。
七、关键答疑:有了@Transactional注解,是不是就不用管数据库事务、不用管锁了?(悲观锁/乐观锁必看)
很多新手最大误区:
以为加个@Transactional,事务就全包了,并发、超卖、数据冲突都自动解决了。
大错特错!完全两码事!
1、第一句话先定调:事务 和 锁,解决的是两个完全不同的问题
| 东西 | 解决什么问题 | 核心作用 | 能不能防并发超卖? |
|---|---|---|---|
| @Transactional 事务 | 解决多步操作要么全成功、要么全回滚 | 保证原子性,防止一半成功一半失败 | 不能!完全防不住并发修改 |
| 数据库锁/悲观锁/乐观锁 | 解决多个人同时改同一条数据,并发冲突问题 | 保证并发数据安全,防止超卖、数据覆盖 | 专门用来防并发 |
2、有了Spring事务,数据库事务还用不用管?
答案:不用你手动写代码管,但底层数据库事务依然必须存在。
① Spring事务 ≠ 替代数据库事务
再次强调:Spring事务只是“帮你自动开启、提交、回滚”数据库事务。
底层干活的还是MySQL原生事务,Spring只是给你套了层AOP注解而已。
就像:
你不用自己点火做饭(不用手动写begin/commit/rollback)
但饭还是要用火做(数据库事务必须存在)
② 你不用写原生事务代码,但必须懂事务隔离级别
SpringBoot事务只管:原子性(成功失败)
不管:并发争抢、数据覆盖、超卖
3、重点:加了事务,为什么还会出现超卖、数据错乱?
举个最经典例子:商品库存只剩1件,两个人同时下单。
两个请求都加了@Transactional:
两个人同时查到库存=1
两个人都判断库存>0,都通过
两个人都减库存
最后库存变成-1,超卖了!
为什么加了事务还超卖?
因为事务只保证:每个人自己的操作要么全成功、要么全回滚。
事务不保证:两个人修改同一条数据互不冲突!
事务不处理并发排队问题!
八、悲观锁、乐观锁详细讲解(实战场景+SpringBoot适配)
1、什么时候必须加锁?
只要满足一句话,必须加锁,不加必出问题:
多用户并发修改同一条数据 → 只用事务不够,必须加锁!
比如:
库存扣减
订单创建
余额转账
积分变动
秒杀活动
2、悲观锁是什么?什么时候用?
核心思想:我先锁住,别人别改,我改完你再改。
每次操作数据,先加锁,独占资源,其他事务阻塞等待。
① 数据库原生悲观锁(for update)
// 查询的时候直接上锁select*from goods where id=1forupdate;② 特点
强一致性,绝对不会超卖
并发性能差,排队等待
容易死锁
③ 适用场景
并发不高、资金交易、对账、金额核心数据,必须保证绝对安全。
3、乐观锁是什么?什么时候用?
核心思想:我不上锁,我相信没人改,提交时校验版本号。
不加锁,通过version版本号控制更新,修改前判断版本号是否一致。
① 原理
查询数据时,带出version=1
更新时要求:where id=? and version=1
更新成功version+1
如果别人改过,version变了,更新行数=0,修改失败
② 特点
性能高,不上锁,不阻塞
并发高适合用
失败需要重试
③ 适用场景
高并发秒杀、库存扣减、抢购、商品热点数据。
十、SpringBoot悲观锁&乐观锁完整实操代码(直接复制运行,实测防超卖)
前面讲清了理论,这一节直接上生产级可落地完整代码,基于库存扣减经典超卖场景,分别实现:无锁事务(必超卖)、悲观锁(防超卖)、乐观锁(防超卖),搭配数据库表、Mapper、Service全套代码,导入就能测试,直观看到效果差异。
1、先准备数据库库存表(必备)
创建商品库存数据表,悲观锁无需额外字段,乐观锁必须新增version版本号字段做校验。
-- 商品库存表:悲观锁、乐观锁通用基础表CREATETABLEgoods_stock(idBIGINTPRIMARYKEYAUTO_INCREMENTCOMMENT'主键ID',goods_nameVARCHAR(100)NOTNULLCOMMENT'商品名称',stock_numINTNOTNULLDEFAULT0COMMENT'剩余库存数量',versionINTNOTNULLDEFAULT1COMMENT'乐观锁版本号(悲观锁也保留,不影响)',create_timeDATETIMEDEFAULTCURRENT_TIMESTAMP,update_timeDATETIMEDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='商品库存表';-- 初始化测试数据:库存仅10件,模拟限量抢购INSERTINTOgoods_stock(goods_name,stock_num,version)VALUES('爆款秒杀手机',10,1);2、实体类Entity代码(MyBatis/MyBatis-Plus通用)
@Data@TableName("goods_stock")publicclassGoodsStock{@TableId(type=IdType.AUTO)privateLongid;// 商品名称privateStringgoodsName;// 剩余库存数量privateIntegerstockNum;// 乐观锁版本号privateIntegerversion;}3、Mapper层接口代码
@MapperpublicinterfaceGoodsStockMapper{// ===================== 悲观锁专用SQL =====================// for update 行级悲观锁:查询当前商品库存并上锁,其他事务必须等待释放才能操作@Select("select * from goods_stock where id = #{goodsId} for update")GoodsStockgetStockByGoodsIdForUpdate(LonggoodsId);// 悲观锁库存扣减@Update("update goods_stock set stock_num = stock_num - 1 where id = #{goodsId}")intdeductStockPessimistic(LonggoodsId);// ===================== 乐观锁专用SQL =====================// 普通查询库存(不上锁)@Select("select * from goods_stock where id = #{goodsId}")GoodsStockgetStockByGoodsId(LonggoodsId);// 乐观锁库存扣减:必须匹配id+version,版本不对更新失败,实现并发控制@Update("update goods_stock set stock_num = stock_num - 1, version = version + 1 where id = #{goodsId} and version = #{version}")intdeductStockOptimistic(@Param("goodsId")LonggoodsId,@Param("version")Integerversion);}4、第一种:只加事务不加锁(演示:必超卖,反面案例)
只加@Transactional事务注解,没有任何锁,并发下单必然超卖,新手千万别这么写。
@ServicepublicclassStockServiceImplimplementsStockService{@AutowiredprivateGoodsStockMapperstockMapper;// 只加事务,无任何锁,并发必超卖!@Transactional(rollbackFor=Exception.class)@OverridepublicvoiddeductStockNoLock(LonggoodsId){// 1. 查询库存GoodsStockstock=stockMapper.getStockByGoodsId(goodsId);// 2. 判断库存是否充足if(stock.getStockNum()>0){// 3. 扣减库存stockMapper.deductStockPessimistic(goodsId);System.out.println("下单扣减成功,当前剩余库存:"+(stock.getStockNum()-1));}else{thrownewRuntimeException("库存不足,下单失败");}}}测试结果:开启多线程模拟并发抢购,库存会扣成负数,事务原子性生效,但并发冲突没解决,超卖必现。
5、第二种:SpringBoot + 悲观锁实操代码(资金核心业务专用,防超卖)
基于数据库for update行级悲观锁,事务内查询上锁,同一时间只有一个事务能操作数据,其他排队等待,绝对不会超卖,数据强一致。
@ServicepublicclassStockServiceImplimplementsStockService{@AutowiredprivateGoodsStockMapperstockMapper;// 事务+悲观锁,防止超卖,资金对账核心业务首选@Transactional(rollbackFor=Exception.class)@OverridepublicvoiddeductStockPessimisticLock(LonggoodsId){// 1. 查询库存 + for update悲观锁上锁// 只要事务没提交,其他线程查询该行数据直接阻塞等待GoodsStockstock=stockMapper.getStockByGoodsIdForUpdate(goodsId);// 2. 判断库存if(stock.getStockNum()>0){// 3. 扣减库存stockMapper.deductStockPessimistic(goodsId);System.out.println("悲观锁下单成功,当前剩余库存:"+(stock.getStockNum()-1));}else{thrownewRuntimeException("库存不足,下单失败");}// 事务提交瞬间,悲观锁自动释放,下一个线程开始执行}}核心关键点:锁在事务范围内生效,事务不提交,锁不释放;行级锁只锁当前商品数据,不锁全表,性能比表锁好。
测试结果:多线程并发请求,库存严格递减,不会出现负数,彻底杜绝超卖。
6、第三种:SpringBoot + 乐观锁实操代码(高并发秒杀专用,高性能防超卖)
无锁设计,依靠version版本号校验,更新时版本不匹配直接更新失败,不阻塞线程、并发性能高,适合秒杀、抢购等高并发场景。额外加循环重试机制,避免正常抢购失败。
@ServicepublicclassStockServiceImplimplementsStockService{@AutowiredprivateGoodsStockMapperstockMapper;// 事务+乐观锁+重试机制,高并发秒杀专用@Transactional(rollbackFor=Exception.class)@OverridepublicvoiddeductStockOptimisticLock(LonggoodsId){// 重试次数:并发更新失败自动重试3次,提升抢购成功率intretryCount=3;while(retryCount>0){// 1. 无锁查询库存和当前版本号GoodsStockstock=stockMapper.getStockByGoodsId(goodsId);// 2. 判断库存if(stock.getStockNum()<=0){thrownewRuntimeException("库存不足,下单失败");}// 3. 乐观锁更新:必须id和version同时匹配才更新成功introws=stockMapper.deductStockOptimistic(goodsId,stock.getVersion());// 4. 更新行数>0:更新成功,扣减完成if(rows>0){System.out.println("乐观锁下单成功,当前版本号:"+(stock.getVersion()+1));return;}// 更新行数=0:版本号已被修改,并发争抢失败,重试次数-1retryCount--;System.out.println("并发争抢冲突,自动重试剩余次数:"+retryCount);}// 重试完毕仍失败,抛出异常thrownewRuntimeException("抢购人数过多,下单失败,请重试");}}核心原理:多人同时抢购,只有一个人版本号匹配更新成功,其他人更新返回0,自动重试,不阻塞不排队,并发性能拉满,无超卖。
7、三种写法最终对比总结(开发直接照选)
只加事务无锁:代码最简单,并发必超卖,线上禁止使用。
事务+悲观锁:强数据一致,排队执行,资金、余额、对账业务必用。
事务+乐观锁:高并发高性能,无阻塞重试,秒杀、库存、抢购活动必用。
九、全文终极总结
@Transactional只管事务原子性(要么全成功全回滚),不管并发!
有注解事务,底层数据库事务依然生效,只是不用你手动写代码。
只用事务不加锁,并发必超卖,数据必错乱。
并发修改同一条数据:必须用悲观锁 or 乐观锁。
资金核心数据用悲观锁,高并发秒杀库存用乐观锁。
普通增删改事务:直接用
@Transactional\(rollbackFor = Exception\.class\)默认传播REQUIRED日志、流水、记录不随主业务回滚:传播机制用REQUIRES_NEW
查询业务:用SUPPORTS + readOnly = true
事务失效优先检查:public方法、是否内部调用、是否捕获异常、是否配rollbackFor