1-概念
@Transactional = 告诉 Spring:从这里开始,到方法结束,这段代码要么全成功,要么全失败
- 基于 AOP(代理)
- 基于 数据库事务
- 基于 线程绑定(ThreadLocal)
@Transactional引入路径:
import org.springframework.transaction.annotation.Transactional;
2-生效条件
- 必须是 Spring 管理的 Bean
@Service
public class OrderService {
}
- 方法必须是
public
@Transactional
public void save() {}
- 必须“通过代理调用”(注入Bean进来,然后调用方法)
orderService.save(); // ok
this.save(); // not ok
// 自调用 = 不走代理 = 没事务
- 数据库引擎必须支持事务
3-触发回滚
@Transactional默认行为:只回滚:
RuntimeExceptionError
不会回滚:
Exception- 自定义业务异常(如果继承 Exception)
所以一般这样写
// 方式一
@Transactional(rollbackFor = Exception.class)
// 方式二
@Transactional(rollbackFor = {Exception.class})
4-传播机制
默认是REQUIRED:
@Transactional(propagation = Propagation.REQUIRED)
// 意思是:有事务就加入,没有就新建
常见的:
| 类型 | 含义 | 场景 |
|---|---|---|
| REQUIRED | 用现有或新建 | 默认、最常用 |
| REQUIRES_NEW | 一定新开事务 | 日志、补偿 |
| SUPPORTS | 有就用,没有就算 | 只读查询 |
5-使用位置
错误用法,造成事务碎片化:
@Transactional
public void insert() {}
@Transactional
public void delete() {}
@Transactional
public void update() {}
正确使用,事务边界 = 业务边界,整个业务方法挂上去即可:
@Transactional
public void businessProcess() {
// 校验
validate();
// 数据库操作
delete();
insert();
update();
}
6-Spring及数据库事务
6-1.概念
对于以上代码:
@Transactional
public void businessProcess() {
// 校验
validate();
// 数据库操作
delete();
insert();
update();
test();
}
这里有两个事务上的区分:一个是Spring事务,一个是数据库的事务
-
Spring 事务(逻辑事务)的生命周期 = 方法进入 → 方法结束
-
数据库事务:
- 数据库事务开始:在第一次访问数据库时:由 Spring 获取 Connection 并执行
BEGIN/setAutoCommit(false) - 数据库事务结束:在方法结束时:由 Spring 统一执行
COMMIT或ROLLBACK
- 数据库事务开始:在第一次访问数据库时:由 Spring 获取 Connection 并执行
6-2.流程
本质是:
@Transactional
public void innerProcess() {
validate(); // 无事务、无连接,不占数据库
buildData(); // 无事务、无连接,不占数据库
deleteOld(); // 第一次 SQL → BEGIN,数据库事务真正开始
saveBatch(); // 同一事务
updateMain(); // 同一事务
test(); // 即使这里没有 SQL,事务仍未提交
} // 方法结束 → COMMIT / ROLLBACK
6-3.性能影响
Spring 事务本身几乎没有性能瓶颈,要多关注数据库事务
6-3-1.数据库连接占用
最基础、最直接的数据库事务瓶颈
- 一个事务 = 占用一个连接
- 连接池是
有限资源,默认可能是:10 / 20 / 50
如果事务时间 = 2 秒,并且并发 = 50,资源会被耗尽
后续请求:等连接、超时、整体 RT 飙升
6-3-2.锁持有时间
数据库事务没提交 = 锁不释放:行锁,间隙锁,Next-Key Lock
只要事务没提交: 锁还在那么其他事务会被阻塞
@Transactional
public void process() {
update order set status = 1 where id = 1;
Thread.sleep(2000);
}
这 2 秒:订单 id=1 被锁死 其他请求:等锁或直接死锁
6-3-3.undo/redo日志膨胀
InnoDB为数据库事务做了什么?
- 修改前数据 → undo log
- 修改后数据 → redo log
事务越长:
- undo log 越多
- redo log 写得越久
后果:
- 回滚慢
- 崩溃恢复慢
- I/O 压力大
6-3-4.事务冲突/死锁概率上升
数据库事务时间越长:
- 被别的事务撞上的概率越大
- 锁顺序不一致 → 死锁
死锁本质:两个事务互相等对方释放锁
长事务 = 死锁温床
7-注意点
- 尽量不要开新线程
- 不开新事务
- 不要catch了异常不抛出
- 在事务里做耗时操作
- 尽量前面做计算,不占连接
- 数据库操作尽量靠后、尽量集中
- 事务边界放在“最外层业务方法”
8-以后咋写
8-1.原则
- 事务内只放“必要的数据库操作”
@Transactional
public void process() {
// ❌ 不要放
// - 远程调用
// - sleep
// - 大量计算
// - IO 操作
// ✅ 可以放
// - insert / update / delete
}
- 先算好数据,再一次性落库
public void process() {
Data data = buildData(); // 无事务
saveInTransaction(data);
}
@Transactional
public void saveInTransaction(Data data) {
deleteOld();
saveBatch(data);
updateMain();
}
- 嵌套事务,主要看最外层
默认传播行为是 REQUIRED
outer() @Transactional
└── inner() @Transactional(REQUIRED)
- inner 不会新开事务
- 共用 outer 的事务
- 谁先执行 SQL,数据库事务就从那一刻开始
- 不要在事务里 catch 然后吞异常
@Transactional
public void process() {
try {
updateMain();
} catch (Exception e) {
// ❌ 吞了
}
}
这会导致:
- Spring 认为方法“正常结束”
- 事务被 commit
- 数据错了还回滚不了
8-2.示例
示例1:
@Service
public class XxxService {
@Transactional(rollbackFor = Exception.class)
public void process(...) {
// 1. 参数校验
validate();
// 2. 业务计算(不碰数据库)
List<Data> data = buildData();
// 3. 数据库操作(必须一致)
deleteOld();
saveBatch(data);
updateMain();
// 4. 不要 catch
}
}
示例2:
@Service
public class XxxService {
public void process() {
// 1. 参数校验(无事务)
validate();
// 2. 业务计算(无事务)
List<Data> data = buildData();
// 3. 落库(短事务)
saveInTransaction(data);
}
@Transactional(rollbackFor = Exception.class)
public void saveInTransaction(List<Data> data) {
deleteOld();
saveBatch(data);
updateMain();
}
}
示例3:
@Transactional
public void process() {
insert();
callRemote(); // 长耗时操作
update();
}
拆开:
// 拆开:
public void process() {
Long id = insertInTx(); // 短事务
callRemote(); // 无事务
updateInTx(id); // 短事务
}
@Transactional
public Long insertInTx() {
return insert();
}
@Transactional
public void updateInTx(Long id) {
update(id);
}
9-案例分析
9-1.性能分析
简单案例:
@Service
@RequiredArgsConstructor
@Slf4j
public class BlockchainServiceImpl implements BlockchainService {
private final BlockchainProperties blockchainProperties;
private final BillInfoService billInfoService;
private final BlockchainDataContentService blockchainDataContentService;
@Override
@Transactional(rollbackFor = Exception.class)
public void uploadFile(BlockchainRequest request) {
// 查库
BillInfo billInfo = billInfoService.getById(request.getId());
// ...
// 远程调用接口(耗时操作)
callRemote();
// 提取成功的正常响应
BlockchainResponseDTO responseDTO=getResult(resultStr);
// 存储和更新相关表
saveAndUpdateInfo(request, billInfo, responseDTO);
}
private void saveAndUpdateInfo(BlockchainRequest request, BillInfo billInfo, BlockchainResponseDTO responseDTO) {
// 存储文件数据
BlockchainDataContent blockchainDataContent = new BlockchainDataContent();
// ...构建参数
blockchainDataContentService.save(blockchainDataContent);
// 更新bill表的状态
LambdaUpdateWrapper<BillInfo> updateWrapper = Wrappers.lambdaUpdate();
updateWrapper.eq(BillInfo::getId, billInfo.getId())
.set(BillInfo::getHasChain, 1);
billInfoService.update(updateWrapper);
}
}
数据库事务具体流程如下:
BEGIN TRANSACTION
SELECT bill_info
(HTTP 调用 0~10 秒)
INSERT blockchain_data_content
UPDATE bill_info
COMMIT
方法内只要第一次操作到数据库了就会开启事务,即使上方是查询
所以上方的代码会出现问题,涉及到了远程调用操作,会导致事务变得非常长
只要涉及到了只要方法里包含:远程调用、MQ、HTTP、慢 IO等,以上情况,就要考虑一下长事务问题
9-2.处理方式
主流方式是,将数据库的操作抽取到另一个处理数据库的service中,然后原来的service将这个数据库的service注入,使用代理对象来操作
示例代码:
- 新建一个单独处理数据库的service:
@Service
public class BlockchainDbService {
@Transactional(rollbackFor = Exception.class)
public void saveAndUpdateInfo(...) {
insert();
update();
}
}
- 注入这个service,通过代理对象来使用方法
@Service
public class BlockchainService {
private final BlockchainDbService blockchainDbService;
// 这里就不要挂事务注解了
public void uploadFile(...) {
// 1. 查询 / 校验 / 远程调用(无事务)
BillInfo billInfo = billInfoService.getById(...);
// 远程调用接口(耗时操作)
callRemote();
// 2. 数据库一致性操作(有事务)
blockchainDbService.saveAndUpdateInfo(...);
}
}
10-总结
只要一个 @Transactional 的方法被 Spring 代理:
- 方法一进入 → Spring 建立“事务上下文”
- 方法内第一次访问数据库(无论增 / 删 / 改 / 查)→ 开启数据库事务(BEGIN)
这里中间如果涉及到了慢操作,就要注意事务的时间,看看能不能抽一下service等,不要涉及到长事务- 直到方法正常结束 → 提交事务(COMMIT)
- 发生异常 → 回滚(ROLLBACK)