文章目录
- 一、先搞懂:什么是Spring事务?核心作用是什么?
- 二、SpringBoot开启事务基础配置(一步到位)
- 三、@Transactional注解完整实操用法(SpringBoot示例)
-
- 1、注解加在哪里?规范用法
- 2、最简单基础使用示例(转账业务场景)
-
- [① Service层业务代码(加事务注解)](#① Service层业务代码(加事务注解))
- [② 效果说明](#② 效果说明)
- 3、@Transactional核心常用参数详解(开发必配)
- 四、必看:@Transactional事务5大失效踩坑点(90%人都踩过)
- 五、Spring七大事务传播机制详解(核心重点\+场景\+代码示例)
-
- 1、什么是事务传播机制?一句话通俗理解
- 2、提前定义两个测试业务方法(统一演示所有传播机制)
- 3、七大传播机制逐个精讲(含义\+适用场景\+执行结果)
-
- [① REQUIRED(默认传播机制,开发最常用)](#① REQUIRED(默认传播机制,开发最常用))
- [② SUPPORTS(支持事务,非必需)](#② SUPPORTS(支持事务,非必需))
- [③ MANDATORY(强制要求事务)](#③ MANDATORY(强制要求事务))
- [④ REQUIRES\_NEW(新建独立事务,核心常用)](#④ REQUIRES_NEW(新建独立事务,核心常用))
- [⑤ NOT_SUPPORTED(强制非事务运行)](#⑤ NOT_SUPPORTED(强制非事务运行))
- [⑥ NEVER(强制非事务,有事务就报错)](#⑥ NEVER(强制非事务,有事务就报错))
- [⑦ NESTED(嵌套事务)](#⑦ NESTED(嵌套事务))
- [六、两大高频传播机制代码实战对比(REQUIRED vs REQUIRES_NEW)](#六、两大高频传播机制代码实战对比(REQUIRED vs REQUIRES_NEW))
-
- 1、场景需求
- 2、REQUIRED默认模式(错误用法,不满足需求)
- 3、REQUIRES_NEW新模式(正确用法,生产标配)
-
- [① 主业务方法(默认REQUIRED)](#① 主业务方法(默认REQUIRED))
- [② 日志子方法(REQUIRES_NEW独立事务)](#② 日志子方法(REQUIRES_NEW独立事务))
- [③ 最终执行效果](#③ 最终执行效果)
- 七、关键答疑:有了@Transactional注解,是不是就不用管数据库事务、不用管锁了?(悲观锁/乐观锁必看)
-
- [1、第一句话先定调:事务 和 锁,解决的是两个完全不同的问题](#1、第一句话先定调:事务 和 锁,解决的是两个完全不同的问题)
- 2、有了Spring事务,数据库事务还用不用管?
-
- [① Spring事务 ≠ 替代数据库事务](#① Spring事务 ≠ 替代数据库事务)
- [② 你不用写原生事务代码,但必须懂事务隔离级别](#② 你不用写原生事务代码,但必须懂事务隔离级别)
- 3、重点:加了事务,为什么还会出现超卖、数据错乱?
- 八、悲观锁、乐观锁详细讲解(实战场景+SpringBoot适配)
-
- 1、什么时候必须加锁?
- 2、悲观锁是什么?什么时候用?
-
-
- [① 数据库原生悲观锁(for update)](#① 数据库原生悲观锁(for update))
- [② 特点](#② 特点)
- [③ 适用场景](#③ 适用场景)
-
- 3、乐观锁是什么?什么时候用?
-
- [① 原理](#① 原理)
- [② 特点](#② 特点)
- [③ 适用场景](#③ 适用场景)
- 十、SpringBoot悲观锁&乐观锁完整实操代码(直接复制运行,实测防超卖)
-
- 1、先准备数据库库存表(必备)
- 2、实体类Entity代码(MyBatis/MyBatis-Plus通用)
- 3、Mapper层接口代码
- 4、第一种:只加事务不加锁(演示:必超卖,反面案例)
- [5、第二种:SpringBoot + 悲观锁实操代码(资金核心业务专用,防超卖)](#5、第二种:SpringBoot + 悲观锁实操代码(资金核心业务专用,防超卖))
- [6、第三种: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部分可省略,但生产环境建议必加,防止事务失效。
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个原因,开发直接避坑:
-
方法不是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)
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
-
两个人都判断库存>0,都通过
-
两个人都减库存
-
最后库存变成-1,超卖了!
为什么加了事务还超卖?
因为事务只保证:每个人自己的操作要么全成功、要么全回滚。
事务不保证:两个人修改同一条数据互不冲突!
事务不处理并发排队问题!
八、悲观锁、乐观锁详细讲解(实战场景+SpringBoot适配)
1、什么时候必须加锁?
只要满足一句话,必须加锁,不加必出问题:
多用户并发修改同一条数据 → 只用事务不够,必须加锁!
比如:
-
库存扣减
-
订单创建
-
余额转账
-
积分变动
-
秒杀活动
2、悲观锁是什么?什么时候用?
核心思想:我先锁住,别人别改,我改完你再改。
每次操作数据,先加锁,独占资源,其他事务阻塞等待。
① 数据库原生悲观锁(for update)
java
// 查询的时候直接上锁
select * from goods where id = 1 for update;
② 特点
-
强一致性,绝对不会超卖
-
并发性能差,排队等待
-
容易死锁
③ 适用场景
并发不高、资金交易、对账、金额核心数据,必须保证绝对安全。
3、乐观锁是什么?什么时候用?
核心思想:我不上锁,我相信没人改,提交时校验版本号。
不加锁,通过version版本号控制更新,修改前判断版本号是否一致。
① 原理
-
查询数据时,带出version=1
-
更新时要求:where id=? and version=1
-
更新成功version+1
-
如果别人改过,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、三种写法最终对比总结(开发直接照选)
-
只加事务无锁 :代码最简单,并发必超卖,线上禁止使用。
-
事务+悲观锁 :强数据一致,排队执行,资金、余额、对账业务必用。
-
事务+乐观锁 :高并发高性能,无阻塞重试,秒杀、库存、抢购活动必用。
九、全文终极总结
-
@Transactional只管事务原子性(要么全成功全回滚),不管并发!
-
有注解事务,底层数据库事务依然生效,只是不用你手动写代码。
-
只用事务不加锁,并发必超卖,数据必错乱。
-
并发修改同一条数据:必须用悲观锁 or 乐观锁。
-
资金核心数据用悲观锁,高并发秒杀库存用乐观锁。
-
普通增删改事务 :直接用
@Transactional\(rollbackFor = Exception\.class\)默认传播REQUIRED -
日志、流水、记录不随主业务回滚:传播机制用REQUIRES_NEW
-
查询业务:用SUPPORTS + readOnly = true
-
事务失效优先检查:public方法、是否内部调用、是否捕获异常、是否配rollbackFor