@Transactional 最佳实践

@Transactional 与数据库连接占用:原理、坑点与解决方案

核心认知:@Transactional 的本质不只是控制数据库原子性,更是控制数据库连接的"占用时长"。

一、核心前提:@Transactional 是如何绑定数据库连接的?

很多人误以为 @Transactional 只是控制 SQL 的原子性,实际上它的工作机制是:

  1. 进入事务:方法进入 @Transactional 作用范围时,Spring 立即从连接池获取一个数据库连接。
  2. 绑定线程:该连接被绑定到当前执行线程。
  3. 独占连接:在整个事务生命周期内,该连接被当前线程独占。
  4. 释放时机:直到事务 提交 / 回滚 后,连接才被释放回连接池。

⚠️ 关键误区纠正

错误认知 正确事实

SQL 执行完就释放连接 事务结束才释放连接

外部调用不影响连接 只要事务没结束,连接一直被占

✅ 哪怕 SQL 早已执行完毕,只要事务未提交,连接就一直被占用。

二、一个真实案例:慢 API 如何拖垮连接池

示例代码(常见写法)

@Service

public class OrderService {

复制代码
@Transactional
public void createOrder(OrderDTO dto) {
    // 步骤1:数据库操作(很快)
    orderMapper.insert(dto);
    inventoryMapper.reduceStock(dto.getGoodsId(), dto.getCount());

    // 步骤2:外部慢 API(5 秒)
    PaymentResult result = paymentApiClient.preCreate(dto);

    // 步骤3:再次 DB 操作
    orderMapper.updatePayStatus(dto.getOrderId(), result.getPayId());
}

}

连接占用时序表

时间点 操作 连接状态

0ms 进入 @Transactional,获取连接 ✅ 被独占

10ms SQL 执行完成 ❌ 仍被占用

10ms ~ 5010ms 等待外部 API 响应 ❌ 仍被占用

5010ms 事务提交 ✅ 释放回连接池

📌 SQL 只跑了 10ms,连接却被占了 5 秒,99.8% 的时间在"空等"。

三、带来的危害(非常致命)

1️⃣ 连接池吞吐量暴跌

假设:

• 连接池最大连接数:10

• 每个事务中有 5 秒 的外部调用

👉 每秒最多处理 2 个请求

即使你的数据库性能再强,也无济于事。

2️⃣ 线程池 / 连接池直接饿死

配置 现象

50 个业务线程 10 个请求占满连接

剩余 40 个线程 阻塞等待数据库连接

maxWait = 3s 3 秒后全部抛 获取连接超时

结果 💥 服务雪崩

3️⃣ 数据库侧连接被打满

• 应用侧连接不释放

• 数据库侧连接数耗尽

• 正常业务完全不可用

四、如何排查是否是这个问题?

✅ 数据库侧检查

Oracle

SELECT * FROM V$SESSION;

• STATUS = INACTIVE

• SQL 已执行完但仍占连接 → 高风险

PostgreSQL

SELECT * FROM pg_stat_activity;

• state = idle in transaction → 事务未提交

✅ 应用侧链路追踪

• Trace 显示:

• SQL 执行很快

• 后续长时间卡在 HTTP / RPC 调用

✅ 可 100% 确认问题

五、解决方案(按推荐程度排序)

✅ 核心原则(一句话)

@Transactional 方法里,只允许数据库操作,禁止一切外部 IO。

方案一:把外部调用移出事务(✅ 最推荐)

适用于 90% 的业务场景。

@Service

public class OrderService {

复制代码
public PaymentResult preCreateOrder(OrderDTO dto) {
    return paymentApiClient.preCreate(dto);
}

@Transactional
public void saveOrder(OrderDTO dto, PaymentResult result) {
    orderMapper.insert(dto);
    inventoryMapper.reduceStock(dto.getGoodsId(), dto.getCount());
    orderMapper.updatePayStatus(dto.getOrderId(), result.getPayId());
}

}

✅ 连接只占用几十毫秒

✅ 外部 API 不再消耗 DB 资源

方案二:事务提交后再执行外部调用

使用 TransactionSynchronizationManager

@Transactional

public void createOrder(OrderDTO dto) {

orderMapper.insert(dto);

inventoryMapper.reduceStock(dto.getGoodsId(), dto.getCount());

复制代码
TransactionSynchronizationManager.registerSynchronization(
    new TransactionSynchronization() {
        @Override
        public void afterCommit() {
            PaymentResult result = paymentApiClient.preCreate(dto);
            orderMapper.updatePayStatus(dto.getOrderId(), result.getPayId());
        }
    }
);

}

📌 适合:

• API 失败可接受

• 可通过补偿机制回滚

方案三:异步化外部调用(✅ 高并发首选)

@Transactional

public void createOrder(OrderDTO dto) {

orderMapper.insert(dto);

inventoryMapper.reduceStock(dto.getGoodsId(), dto.getCount());

复制代码
mqTemplate.send("pre-create-order", dto);

}

✅ 主流程极快

✅ 外部系统慢慢消费

✅ 彻底解耦事务与外部依赖

方案四:兜底优化(不推荐,但有时不得不做)

手段 说明

@Transactional(timeout = 3) 强制回滚,防止无限占用

缩短外部 API 超时 至少控制在 1~2 秒内

调大连接池 治标不治本

六、补充注意点(非常重要)

🔴 本质问题:长事务

以下行为都会造成同样问题:

• 外部 HTTP / RPC 调用

• 本地复杂计算

• Thread.sleep()

• 循环等待

🔴 连接池配置 ≠ 解决方案

把连接池从 10 调到 20,只是多扛一倍流量,解决不了根本问题。

🔴 测试环境骗了你

环境 表现

测试环境 数据少、API 快,问题隐藏

生产环境 数据量大、API 抖动 → 瞬间雪崩

✅ 总结一句话

凡是 @Transactional 方法里出现的非数据库操作,都是潜在的生产事故。

如果你愿意,我可以帮你:

• ✅ 把现有代码改成"事务最小化"

• ✅ 设计一个统一的事务 & MQ 架构

• ✅ 给你一套 Spring 事务规范 CheckList

随时告诉我 👍

相关推荐
唐青枫21 小时前
Java JDBC 实战指南:从 Connection 到事务和连接池
java
一个做软件开发的牛马1 天前
MyBatis-Plus 从零实战:完整搭建可运行 Demo,BaseMapper 零 SQL、Wrapper 条件构造、分页插件与代码生成器详解
java·后端
用户3721574261351 天前
Java 处理 PDF 图片:提取 PDF 中的图片,并压缩 PDF 图片体积
java
用户3721574261351 天前
Java 打印 Word 文档:从基础打印到高级设置
java
用户3521802454752 天前
当 Prompt 学会"热更新":Spring Boot × Nacos3 AI 实战
java·spring boot·ai编程
jump_jump2 天前
流式 HTML:从 htmx 片段装配到浏览器原生增量渲染
javascript·性能优化·前端工程化
昵称为空C2 天前
手撸一个动态 SQL 执行引擎:不重启服务,在线增删改查任意数据库
spring boot·后端
东坡白菜2 天前
破局全栈:一个前端开发的Java入门实战记录(1)
java·全栈
唐青枫2 天前
Java Tomcat 实战指南:从 Servlet 容器到 Spring Boot 部署
java
wsaaaqqq2 天前
roudan:自由选择实体、灵活操作数据、快速写入数据库的 Java 框架
java