Record-API 性能优化实战:从“锁”到“快”的深度治理

一、问题背景

近期在生产环境中,我们发现 record-api 服务出现响应缓慢甚至频繁超时的问题。具体表现包括:

  • Record 单个删除接口 请求耗时超过 1 小时

  • 批量删除接口 长时间无响应;

  • 查询 Record 详情接口 耗时高达 10 分钟以上

  • 整个record-api 关联的数据库直接查询不了, 严重影响的电话业务.

随着多个区域客户陆续反馈服务不可用,我们立即展开排查。最终发现问题根源在于 PostgreSQL 数据库中 records 表频繁发生 行级锁竞争(Lock: tuple) ,极大地影响了整体读写性能,进而导致服务雪崩。进一步调查发现:是并发的 delete 请求和一个定时任务在数据库中产生了锁冲突,从而引发了严重的性能瓶颈,最终导致 PostgreSQL 数据库的 CPU 达到 99%,服务基本不可用。

目前Record的单表数据量已经达到了 2 千万, 之前创建了一个索引, 花费了半个小时... 已经从 4 千万数量删除到了 2千万, 删除了之前软删除的1 年之前的数据.

这个表中还有大量的 2 年之前的数据, 未来还可以考虑做数据归档.


二、根因分析:并发写请求与定时任务引发的锁冲突

排查过程中发现如下原因叠加导致锁冲突严重:

  1. 多个大客户高频调用 DELETE 接口(单个 + 批量),形成激烈并发, 它们直接使用 API 调用的方式, 疯狂调用删除;

  2. 后台定时任务 ExtraKeysScheduler 同时频繁更新 records 表中的 historical_keys_log 字段;

  3. 两者操作重叠,产生严重行级锁冲突,阻塞其他事务;

  4. 数据库未设置 socket_timeout,部分连接长期占用连接池资源,最终导致线程耗尽,服务完全雪崩。

异常日志如下所示:

复制代码
org.postgresql.util.PSQLException: ERROR: canceling statement due to lock timeout
Where: while updating tuple (656,9) in relation "records_account_xxx"

三、第一阶段:快速止血与初步优化

暂停高风险任务

  • 立即停止 ExtraKeysScheduler 定时任务,避免与 DELETE 接口抢占锁资源;

  • 稳住主业务服务,防止服务进一步雪崩。

调整 Record 单个删除接口的允许访问频率, 从每秒的50 个请求直接先调整为每秒 5 个请求.

为record-api与数据库的连接设置一个 10 分钟的套接字超时(Socket Timeout)

目的: 此举旨在防止应用在等待数据库响应时无限期地挂起。当数据库操作耗时过长(例如超过10分钟),该设置会强制中断网络连接,从而触发异常。这能确保应用连接可以被及时释放回连接池,避免因长时间的数据库交互导致连接资源耗尽和泄漏

在数据库事务中添加锁等待超时

在 PostgreSQL 数据库事务中配置 锁等待超时(Lock Timeout)

目的: 当一个事务试图获取一个被其他事务持有的锁时,它不会永久等待。该配置将使其在等待一个预设的时间后自动放弃。这可以自动中止因等待锁而阻塞的事务,防止死锁(Deadlocks)的发生,并快速释放数据库资源,提高系统的健壮性。

SQL 查询逻辑优化

1. 移除 SQL 中的动态函数调用,提升索引命中率

原查询示例:

复制代码
select * from extra_keys
where updated_at <= now() - interval '2 hour'
  and updated_at > created_at
  and counter <= 0

优化后:

复制代码
val threshold = now() - Duration.ofHours(2)
...
where updated_at <= :threshold

通过代码提前计算 threshold,提高了索引命中率,显著降低查询耗时。

2. 批量插入改为分批处理,降低事务压力

原逻辑:

复制代码
insert into historical_processor
select ... from records where updated_at between ...

优化方案:

  • 将 SQL 插入迁移至代码层控制;

  • 使用分页拉取数据 + 批量插入方式,缩小事务规模,减轻数据库锁压力。

3. 避免多表 update 导致锁表

原 SQL:

复制代码
update records t
set historical_keys_log = null
from historical_processor h
where h.record_id = t.id ...

优化后使用精准索引:

复制代码
UPDATE records
SET historical_keys_log = null
WHERE account_id = :accountId AND id IN (:recordIds)
  • 使用 (account_id, id) 精准索引;

  • 避免嵌套循环 JOIN 导致的大范围锁表。


四、第二阶段:任务调度重构与区域验证

初步优化上线后,加拿大区域表现稳定。但在美国和欧洲区域上线时暴露出新问题:

  • Job 被暂停数日,堆积 47 万条未处理记录

  • 一次性全量处理导致事务超大,再次出现超时问题。

定时任务重构方案

  1. 动态获取每个分表对应的 account_id

    SELECT regexp_replace(c.relname, '^records_v2_account_', '') AS account_id
    FROM pg_inherits i
    JOIN pg_class c ON i.inhrelid = c.oid
    WHERE i.inhparent = 'records_v2'::regclass

  2. 按 account_id 分批处理,每批处理 500 条记录

  3. 任务执行频率由每 10 分钟提升为每分钟

  4. 缩小事务粒度,显著降低锁竞争。

上线结果:

  • us-east-1 和 eu-central-1 成功跑完全部任务;

  • Job 单次执行耗时 < 1 秒;

  • 所有积压数据 1 天内全部清理完毕。


五、第三阶段:Delete 接口体系重构

旧架构问题

  • 高并发触发 DELETE 接口;

  • 同一 record_id 被多次重复删除;

  • 与定时任务竞争锁,导致服务冻结;

  • 无法保障数据一致性与稳定性。

重构方案

1. 引入 Redis 分布式锁
  • 按 record_id 加锁,避免并发冲突;

  • 设置锁超时为 5 秒,快速失败反馈。

2. 使用 FOR UPDATE SKIP LOCKED
  • 避免读取已被锁住的记录;

  • 仅处理可立即锁定的数据行。

3. 支持异步删除架构
  • 加入 Feature Flag:BULK_DELETE_RECORDS_BY_QUEUE

  • 将删除任务统一入 Redis 队列;

  • 后台异步线程消费处理;

  • 提升主线程响应速度与系统稳定性。

Redis 队列结构优化

  • deleteRecordsByIds → 入队 storeDeleteRecordsToQueue

  • deleteByPhoneNumbers / ExternalIds → 校验通过后入队;

  • 响应码设计优化:入队成功返回 202 Accepted


六、经验教训总结

成功经验

  • 快速定位问题 SQL,调整查询逻辑与索引使用方式;

  • 第一时间中止高风险任务,控制故障范围;

  • 架构从同步转异步,从粗放转精细,服务鲁棒性显著提升;

  • 合理引入 Redis 锁机制、限流控制与异步调度;

  • 使用 FOR UPDATE SKIP LOCKED 回避锁冲突,保障系统运行。

遗留问题与优化方向

  • DELETE 接口缺乏全局频控:将引入 API Rate Limiter 限流机制;

  • Job 执行缺乏断点续跑能力:计划引入状态记录与幂等重试逻辑;

  • 增强观测能力:增加事务耗时、锁等待、连接池状态等指标监控。


七、结语

此次 record-api 性能治理经历了从 性能雪崩 → 快速止血 → 架构优化 → 稳定运行 的全过程。

我们不仅解决了服务宕机问题,更通过深入分析和分层治理实现了系统的可持续优化。

这次实战过程中的技术方法和策略,希望能为其他团队应对高并发与数据库性能问题提供实用借鉴。

相关推荐
apocelipes39 分钟前
从源码角度解析C++20新特性如何简化线程超时取消
c++·性能优化·golang·并发·c++20·linux编程
摇滚侠41 分钟前
Redis 零基础到进阶,Redis 哨兵监控,笔记63-73
数据库·redis·笔记
利剑 -~1 小时前
mysql面试题整理
android·数据库·mysql
老华带你飞1 小时前
物流信息管理|基于springboot 物流信息管理系统(源码+数据库+文档)
数据库·vue.js·spring boot
程序员卷卷狗1 小时前
Redis事务与MySQL事务有什么区别?一文分清
数据库·redis·mysql
玩大数据的龙威1 小时前
农经权二轮延包—数据(新老农经权)比对软件更新
数据库·arcgis
保持低旋律节奏2 小时前
网络系统管理——期末复习
数据库
程序员佳佳2 小时前
2025年大模型终极横评:GPT-5.2、Banana Pro与DeepSeek V3.2实战硬核比拼(附统一接入方案)
服务器·数据库·人工智能·python·gpt·api
roo_13 小时前
github 获取构造图数据库的LNB数据集和使用说明
数据库
桦说编程3 小时前
并发编程高级技巧:运行时检测死锁,告别死锁焦虑
java·后端·性能优化