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 性能治理经历了从 性能雪崩 → 快速止血 → 架构优化 → 稳定运行 的全过程。

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

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

相关推荐
DemonAvenger1 天前
Kafka性能调优:从参数配置到硬件选择的全方位指南
性能优化·kafka·消息队列
桦说编程2 天前
实战分析 ConcurrentHashMap.computeIfAbsent 的锁冲突问题
java·后端·性能优化
爱可生开源社区2 天前
2026 年,优秀的 DBA 需要具备哪些素质?
数据库·人工智能·dba
随逸1772 天前
《从零搭建NestJS项目》
数据库·typescript
加号32 天前
windows系统下mysql多源数据库同步部署
数据库·windows·mysql
シ風箏2 天前
MySQL【部署 04】Docker部署 MySQL8.0.32 版本(网盘镜像及启动命令分享)
数据库·mysql·docker
李慕婉学姐2 天前
Springboot智慧社区系统设计与开发6n99s526(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
百锦再2 天前
Django实现接口token检测的实现方案
数据库·python·django·sqlite·flask·fastapi·pip
tryCbest2 天前
数据库SQL学习
数据库·sql
jnrjian2 天前
ORA-01017 查找机器名 用户名 以及library cache lock 参数含义
数据库·oracle