Spring 事务范围优化最佳实践
引言
在实际开发中,我们经常使用 @Transactional 注解来管理事务。但是,事务注解放置的位置会直接影响系统性能。本文通过一个真实的生产案例,深入讨论如何优化事务范围,提升系统性能。
问题场景
初始代码
在某个业务功能中,最初的实现是这样的:
java
@Override
@Transactional(rollbackFor = Exception.class)
public void generateReportData(LocalDate localDate) {
// 1. 查询配置(SELECT)
LambdaQueryWrapper<Parameter> queryWrapper = new LambdaQueryWrapper<>();
List<Parameter> parameters = parameterMapper.selectList(queryWrapper);
// 2. 批量获取数据(调用外部服务)
Map<String, List<Parameter>> groupedByDataSource = parameters.stream()
.collect(Collectors.groupingBy(Parameter::getDataSource));
List<ReportData> allDataList = groupedByDataSource.entrySet().stream()
.flatMap(entry -> {
// 通过 Spring 容器获取对应的策略 bean
DataStrategy strategy = applicationContext.getBean(
entry.getKey(), DataStrategy.class);
// 批量调用策略获取数据(可能调用外部服务,耗时长)
List<ReportData> dataList = strategy.batchFetchData(
entry.getValue(), localDate);
return dataList != null ? dataList.stream() : Stream.empty();
})
.collect(Collectors.toList());
// 3. 保存数据(INSERT/UPDATE)
if (!allDataList.isEmpty()) {
batchSaveDataValues(allDataList);
}
}
private void batchSaveDataValues(List<ReportData> dataList) {
dataList.forEach(data -> {
// 保存逻辑
});
}
深入分析
事务的作用是什么?
事务(Transaction)主要用于保证数据库写操作的 ACID 特性:
- Atomicity(原子性):要么全部成功,要么全部失败
- Consistency(一致性):数据始终保持一致状态
- Isolation(隔离性):并发事务之间相互隔离
- Durability(持久性):提交后永久生效
在这个场景中:
- ✅ 步骤3(INSERT/UPDATE)需要事务保证数据一致性
- ❌ 步骤1(SELECT 查询)不需要事务
- ❌ 步骤2(外部服务调用)不需要事务,且可能耗时很长(网络IO调用外部服务)
问题识别
这种设计存在以下问题:
-
查询操作被包裹在事务中
- 浪费数据库连接资源
- 增加不必要的锁等待
-
外部服务调用在事务中执行
- 可能耗时很长(2秒以上)
- 长时间占用数据库连接
- 降低数据库连接池的并发能力
-
并发性能下降
- 数据库连接被长时间占用
- 其他请求等待连接时间增加
- 系统吞吐量下降
优化方案
方案对比
| 方案 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| 方案1:独立Service | 职责清晰、事务边界明确、符合单一职责原则 | 需要新建类 | ⭐⭐⭐⭐⭐ |
| 方案2:self-invocation | 无需新建类 | 需要注入自己、代码不够优雅 | ⭐⭐⭐ |
推荐方案1:创建独立的 Service 来处理需要事务的操作。
优化实现
步骤1:创建独立的数据保存 Service
接口定义:
java
package com.example.biz.service;
import com.example.biz.model.entity.ReportData;
import java.util.List;
/**
* 数据保存 Service
*/
public interface DataSaveService {
/**
* 保存数据
*
* @param dataList 数据列表
*/
void saveData(List<ReportData> dataList);
}
实现类:
java
package com.example.biz.serviceImpl;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.biz.mapper.ReportDataMapper;
import com.example.biz.model.entity.ReportData;
import com.example.biz.service.DataSaveService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 数据保存 Service 实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DataSaveServiceImpl implements DataSaveService {
private final ReportDataMapper dataMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void saveData(List<ReportData> dataList) {
dataList.forEach(data -> {
// 查询是否已存在数据
LambdaQueryWrapper<ReportData> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ReportData::getParameterId, data.getParameterId())
.eq(ReportData::getDataDate, data.getDataDate());
ReportData existData = dataMapper.selectOne(queryWrapper);
if (existData != null) {
// 更新已存在的数据
existData.setDataValue(data.getDataValue());
dataMapper.updateById(existData);
} else {
// 插入新数据
dataMapper.insert(data);
}
});
log.info("数据保存完成: total={}", dataList.size());
}
}
步骤2:修改主业务 Service
java
@Slf4j
@Service
@RequiredArgsConstructor
public class ReportServiceImpl implements ReportService {
private final ParameterMapper parameterMapper;
private final ApplicationContext applicationContext;
private final DataSaveService dataSaveService; // 新增
@Override
// 移除 @Transactional 注解
public void generateReportData(LocalDate localDate) {
log.info("开始生成数据, 日期: {}", localDate);
// 1. 查询配置 - 不在事务中
LambdaQueryWrapper<Parameter> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.isNotNull(Parameter::getDataSource);
List<Parameter> parameters = parameterMapper.selectList(queryWrapper);
// 2. 批量获取数据 - 不在事务中
List<ReportData> allDataList = groupedByDataSource.entrySet().stream()
.flatMap(entry -> {
DataStrategy strategy = applicationContext.getBean(
entry.getKey(), DataStrategy.class);
List<ReportData> dataList = strategy.batchFetchData(
entry.getValue(), localDate);
return dataList != null ? dataList.stream() : Stream.empty();
})
.collect(Collectors.toList());
// 3. 保存数据 - 调用独立的事务方法
if (!allDataList.isEmpty()) {
dataSaveService.saveData(allDataList);
log.info("数据批量保存完成: date={}, total={}", localDate, allDataList.size());
}
}
}
核心知识点
1. 事务的作用范围
需要事务的操作:
- ✅ INSERT(插入)
- ✅ UPDATE(更新)
- ✅ DELETE(删除)
不需要事务的操作:
- ❌ SELECT(查询)- 只读操作,不需要事务保护
- ❌ 外部服务调用 - 不在数据库事务范围内
- ❌ 计算逻辑 - 纯内存操作
2. Spring AOP 的限制
Spring 的 @Transactional 基于 AOP 代理机制,有以下限制:
java
// ✅ 可以被代理(外部调用)
@Transactional
public void publicMethod() {
// 事务生效
}
// ❌ 无法被代理(private 方法)
@Transactional
private void privateMethod() {
// 事务不生效
}
// ❌ 内部调用不生效
public void method1() {
method2(); // 内部调用,method2 的事务不生效
}
@Transactional
public void method2() {
// 事务不生效(因为是内部调用)
}
重要结论:
- private 方法上的
@Transactional不会生效 - 同一个类中内部方法调用,
@Transactional不会生效 - 必须通过 Spring 代理的对象调用,事务才会生效
3. 事务性能优化原则
核心原则:事务范围应该尽可能小
具体实践:
- ✅ 只在数据库写操作上添加事务
- ✅ 避免在事务中执行耗时操作
- ✅ 将需要事务的操作提取到独立的 Service
- ✅ 事务方法应该是 public 的
- ❌ 避免在事务中调用远程服务
- ❌ 避免在事务中进行文件IO
- ❌ 避免在事务中执行复杂计算
性能对比
优化前
事务开始
├─ 查询配置 (50ms)
├─ 调用外部服务 (2000ms) ⚠️ 长时间占用连接
└─ 保存数据 (100ms)
事务结束
总耗时: 2150ms
事务持有时间: 2150ms
数据库连接占用: 2150ms
优化后
查询配置 (50ms) ✅ 不占用连接
调用外部服务 (2000ms) ✅ 不占用连接
事务开始
└─ 保存数据 (100ms)
事务结束
总耗时: 2150ms
事务持有时间: 100ms
数据库连接占用: 100ms
性能提升
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 事务持有时间 | 2150ms | 100ms | 21.5倍 |
| 数据库连接占用 | 2150ms | 100ms | 21.5倍 |
| 并发处理能力 | 基准 | 约20倍提升 | - |
实际影响:
- 假设数据库连接池大小为 10
- 优化前:每 2150ms 处理 10 个请求 = 约 279 请求/分钟
- 优化后:每 2150ms 处理约 210 个请求 = 约 5860 请求/分钟
- 吞吐量提升约 21 倍
最佳实践总结
代码对比
java
// ❌ 不推荐:事务范围过大
@Transactional
public void businessMethod() {
// 查询操作
List<Entity> data = mapper.selectList(wrapper);
// 外部服务调用(慢操作)
ExternalData extData = externalService.fetchData();
// 保存操作
mapper.insert(data);
}
// ✅ 推荐:事务范围精确
public void businessMethod() {
// 查询操作 - 不在事务中
List<Entity> data = mapper.selectList(wrapper);
// 外部服务调用 - 不在事务中
ExternalData extData = externalService.fetchData();
// 保存操作 - 独立事务
transactionalService.saveData(data);
}
设计原则
-
单一职责原则
- 将事务操作提取到独立的 Service
- 一个 Service 只负责一个事务边界
-
最小化事务范围
- 事务只包裹必要的数据库写操作
- 查询和计算操作在事务外执行
-
避免长事务
- 在事务中避免调用远程服务
- 在事务中避免进行文件IO
- 在事务中避免复杂计算
-
利用独立 Service
- 通过独立的 Service 划分事务边界
- 保证事务方法能被 Spring 正确代理
常见问题
Q1: 为什么 private 方法上的 @Transactional 不生效?
A : Spring 的 @Transactional 基于 AOP 代理,只能代理 public 方法。private 方法无法被外部代理,因此事务不生效。
Q2: 同一个类中方法调用,事务为什么不生效?
A: 因为是内部调用,没有经过 Spring 代理对象。解决方案:
- 方案1:将事务方法提取到独立的 Service
- 方案2:通过 ApplicationContext 获取代理对象自己调用自己
Q3: 查询操作需要事务吗?
A: 一般不需要。除非有特殊的隔离级别需求(如避免脏读),否则查询操作不需要事务。
Q4: 如何判断事务范围是否合理?
A: 问自己以下几个问题:
- 这个操作是数据库写操作吗?
- 事务中是否有耗时操作?
- 事务持有时间是否超过 1 秒?
- 能否将部分操作移出事务?
如果事务中有查询、远程调用、文件IO等操作,说明范围过大。
总结
关键要点
- 明确事务边界:事务只应该包裹必要的数据库写操作
- 避免长事务:在事务中避免执行耗时操作
- 利用独立Service:将事务操作提取到独立的 Service 中
- 注意AOP限制 :private 方法上的
@Transactional不会生效 - 关注性能:事务范围直接影响系统并发能力
记忆口诀
事务范围要精确,
查询计算往外扔。
耗时操作事务外,
独立 Service 是真金。