从项目实践中学习 Spring 事务范围优化

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调用外部服务)

问题识别

这种设计存在以下问题:

  1. 查询操作被包裹在事务中

    • 浪费数据库连接资源
    • 增加不必要的锁等待
  2. 外部服务调用在事务中执行

    • 可能耗时很长(2秒以上)
    • 长时间占用数据库连接
    • 降低数据库连接池的并发能力
  3. 并发性能下降

    • 数据库连接被长时间占用
    • 其他请求等待连接时间增加
    • 系统吞吐量下降

优化方案

方案对比

方案 优点 缺点 推荐度
方案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. 事务性能优化原则

核心原则:事务范围应该尽可能小

具体实践

  1. ✅ 只在数据库写操作上添加事务
  2. ✅ 避免在事务中执行耗时操作
  3. ✅ 将需要事务的操作提取到独立的 Service
  4. ✅ 事务方法应该是 public 的
  5. ❌ 避免在事务中调用远程服务
  6. ❌ 避免在事务中进行文件IO
  7. ❌ 避免在事务中执行复杂计算

性能对比

优化前

复制代码
事务开始
  ├─ 查询配置 (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);
}

设计原则

  1. 单一职责原则

    • 将事务操作提取到独立的 Service
    • 一个 Service 只负责一个事务边界
  2. 最小化事务范围

    • 事务只包裹必要的数据库写操作
    • 查询和计算操作在事务外执行
  3. 避免长事务

    • 在事务中避免调用远程服务
    • 在事务中避免进行文件IO
    • 在事务中避免复杂计算
  4. 利用独立 Service

    • 通过独立的 Service 划分事务边界
    • 保证事务方法能被 Spring 正确代理

常见问题

Q1: 为什么 private 方法上的 @Transactional 不生效?

A : Spring 的 @Transactional 基于 AOP 代理,只能代理 public 方法。private 方法无法被外部代理,因此事务不生效。

Q2: 同一个类中方法调用,事务为什么不生效?

A: 因为是内部调用,没有经过 Spring 代理对象。解决方案:

  • 方案1:将事务方法提取到独立的 Service
  • 方案2:通过 ApplicationContext 获取代理对象自己调用自己

Q3: 查询操作需要事务吗?

A: 一般不需要。除非有特殊的隔离级别需求(如避免脏读),否则查询操作不需要事务。

Q4: 如何判断事务范围是否合理?

A: 问自己以下几个问题:

  1. 这个操作是数据库写操作吗?
  2. 事务中是否有耗时操作?
  3. 事务持有时间是否超过 1 秒?
  4. 能否将部分操作移出事务?

如果事务中有查询、远程调用、文件IO等操作,说明范围过大。


总结

关键要点

  1. 明确事务边界:事务只应该包裹必要的数据库写操作
  2. 避免长事务:在事务中避免执行耗时操作
  3. 利用独立Service:将事务操作提取到独立的 Service 中
  4. 注意AOP限制 :private 方法上的 @Transactional 不会生效
  5. 关注性能:事务范围直接影响系统并发能力

记忆口诀

复制代码
事务范围要精确,
查询计算往外扔。
耗时操作事务外,
独立 Service 是真金。

参考资料

相关推荐
我的golang之路果然有问题18 小时前
mysql 个人笔记导出之-数据库时间戳问题以及增删改查
数据库·笔记·学习·mysql·分享·个人笔记
张永清-老清18 小时前
每周读书与学习->JMeter性能测试脚本编写实战(三)如何利用JMeter为MySQL数据库构造测试数据
数据库·测试工具·jmeter·压力测试·性能调优·jmeter性能测试·每周读书与学习
亮子AI18 小时前
注册成功的提示信息怎么写?
数据库·python
Clang's Blog18 小时前
使用 SQL Server Management Studio 还原 .bak 备份文件的完整指南
数据库·sqlserver
ybb_ymm18 小时前
如何通过跳板机链接mysql数据库
数据库·mysql
繁依Fanyi19 小时前
从初识到实战 | OpenTeleDB 安装迁移使用指南
开发语言·数据库·python
朱峥嵘(朱髯)19 小时前
数据库如何根据估计 NDV,以及通过分区 NDV 推导全局 NDV
数据库·算法
7ioik19 小时前
RC和RR隔离级别下MVCC的差异?
数据库·sql·mysql