返回首页

旧代码优化:将处理时间减少 99%

遗留系统中优化 75 千记录处理的真实案例分解。展示如何通过 4 步消除冗余操作、设置批量处理和安全扩展,将执行时间从 105 秒减少到 1.05 秒。

真实案例:旧代码优化,从 105 秒到 1.05 秒
Advertisement 728x90

优化遗留代码:真实案例中如何将处理时间缩短 99%

与外部系统实现 REST 集成时,经常会遇到性能问题。本文将详细分解我们如何通过 4 个步骤加速处理 75,000 条记录,将执行时间缩短 99%。这个案例基于一个真实的银行系统与外部合作伙伴集成的任务,受严格约束。

问题:与合作伙伴约束下的 REST 集成

初始条件:

  • 需要通过 REST API 将用户数据发送到合作伙伴系统
  • 合作伙伴仅接受单条请求(不允许批量处理)
  • 执行时间限制:凌晨 5 点窗口期以最小化负载
  • 数据量:75,000 条 NEW 状态记录
  • 不允许异步处理或水平扩展

原始代码使用 Spring Data Pageable 分页加载,每页 2000 条记录。关键错误是将所有修改的实体累积在内存中,直到执行单一的 saveAll() 操作,这导致:

Google AdInline article slot
  • 每次 SELECT 前自动上下文刷新
  • 每页生成 2000 个 UPDATE 查询
  • HTTP 调用期间的事务锁

初始基准测试显示,在 Apple M4(16 GB 内存)上处理时间 ≈105 秒。模拟外部系统 1 ms 延迟,使其成为生产环境的关键指标。

第一步:消除冗余数据库操作

SQL 日志分析揭示了致命缺陷:Hibernate 生成 2000 个单独的 UPDATE,而不是批量操作。原因是跨循环迭代在持久化上下文中持有实体。解决方案涉及两个关键更改:

  • 将过程结束时的保存替换为中间提交:
content.forEach(this::registerUser);
userRepository.saveAll(content);
  • 实现用于批量状态更新的原生查询:
@Modifying
@Query(value = """
UPDATE users SET status = CAST(:status AS text)
WHERE id IN (:ids)
""", nativeQuery = true)
int batchUpdateStatus(@Param("ids") Collection<Long> ids, @Param("status") String status);

这些更改带来了初步结果——时间减少 6%(至 98.4 秒)。关键要点:标准 Spring Data JPA 方法在大规模数据量下会产生隐藏开销。

Google AdInline article slot

第二步:微调处理参数

下一次优化聚焦于批次大小。测试显示:

  • 大小 500:时间增加 8.41%(106.68 秒)
  • 大小 2000:基准 98.4 秒
  • 大小 5000:改善 1.86%(96.58 秒)

重要的是理解没有银弹。最优大小取决于:

  • 硬件规格(磁盘 IOPS、网络带宽)
  • 处理循环中业务逻辑的复杂性
  • 数据库连接池设置

推荐方法:在类似生产的环境中运行 A/B 测试,并进行资源剖析(CPU、内存、磁盘 I/O)。在我们的案例中,5000 条记录证明是最优的,减少了事务提交次数。

Google AdInline article slot

第三步:安全的水平扩展

尝试双线程处理导致灾难——所有操作重复(150,000 个查询而不是 75,000 个)。解决方案需要更改数据访问策略:

  • 实现 SELECT FOR UPDATE SKIP LOCKED:
SELECT * FROM users WHERE status = 'NEW' ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 5000
  • 实现事务循环:
@Transactional
public void processBatch() {
  List<User> batch = userRepository.lockAndSelectBatch(5000);
  batch.forEach(this::registerUser);
  userRepository.batchUpdateStatus(
    batch.stream().map(User::getId).collect(Collectors.toList()),
    UserStatus.PROCESSED.name()
  );
}

这种方法确保:

  • 保证一次处理
  • 最小表锁
  • 能够运行 N 个并行实例

实施后,处理时间降至 1.05 秒——比原来减少 99%。

关键优化阶段

  • 分析隐藏开销

- 启用 show-sql 跟踪生成的查询

- 检查循环中持久化上下文行为

  • 切换到批量操作

- 将 saveAll() 替换为中间提交

- 使用原生查询进行批量更新

  • 实验参数

- 测试各种批次大小

- 测试期间监控系统指标

  • 准备扩展

- 为并发访问实现 SKIP LOCKED

- 将事务分解为原子块

关键要点

  • SELECT 前 Hibernate 刷新——大规模数据量下的关键问题。每个数据库查询都会触发自动刷新,导致 UPDATE 级联。
  • 批次大小并非固定。需要根据工作负载具体情况进行实证调优。测试显示 5000 条记录最优。
  • 用于扩展的事务隔离。SELECT FOR UPDATE SKIP LOCKED 实现无重复的并发处理,但会增加锁持有时间。
  • 剖析必不可少。没有类似生产的基准测试,优化可能适得其反(比如将批次大小缩小到 500)。
  • 遗留约束兼容性。解决方案必须考虑外部限制(例如合作伙伴禁止批量请求),从而排除简单的异步处理。

最终实现提供了稳定的 1.05±0.05 秒处理时间,在负载测试 100,000 条记录下。成功的关键是将技术优化与对外部系统契约的深刻理解相结合。这种方法适用于任何有请求大小限制和可靠性要求的 REST API 集成场景。

— Editorial Team

Advertisement 728x90

继续阅读