← Back to Blog

应用服务接口性能优化实践

优化方案

针对上述问题,我们制定了以下优化方案:

  • 批量获取数据,减少查询次数:针对N+1查询问题,我们决定将循环内的多次数据库查询改为一次批量查询。也就是,将原来按ID逐个查询改为利用数据库的IN子句一次性请求所有所需资源的数据。为了实现这一点,我们引入了数据库视图将原本分散的表关联起来,便于一次查询拿到完整结果。例如,创建了一个视图,将资源的基本信息、分类信息等需要的字段都整合出来。这样,我们可以通过一次查询视图,获取所有ID对应的记录,而不再需要反复访问多个表。
  • 调整结果排序方式:对于排序中依赖缓存的问题,我们选择简化排序逻辑。首先,去除排序过程中的缓存调用,将无法实时获取的排序依据设为默认值处理。其次,考虑到批量查询返回的数据顺序可能与原始顺序不一致(数据库IN查询通常不保证结果顺序与传入ID顺序相同),我们在应用层增加了结果顺序修正步骤。具体而言,在拿到批量查询结果后,按照最初获得的ID列表顺序重新排列结果列表,确保与未优化前的业务排序需求一致。这样做既避免了缓存调用,又保证了结果顺序的正确性。
  • 数据预加载和缓存(可选):在分析过程中我们还提出了预加载数据的思路。例如针对地理范围查询,可以提前加载特定范围内所有资源的数据到内存,或者利用现有的搜索索引直接获取完整数据,从而减少实时查询数据库的压力。不过,这一方案实现起来相对复杂,需要权衡数据新鲜度和内存占用,因此在本次优化中仅作为备选思路,并未立即实施。

视图聚合 + 批量查询

  • resource_basicresource_metaresource_extra 等表的关键字段通过 只读视图 vw_resource_full 聚合。
  • 采用 SELECT * FROM vw_resource_full WHERE resource_id IN (...) 一次拉取所有数据,避免 N+1 查询。
CREATE OR REPLACE VIEW vw_resource_full AS
SELECT b.resource_id,
       b.name,
       m.category,
       e.score,
       ...
FROM   resource_basic b
JOIN   resource_meta m   ON m.resource_id = b.resource_id
LEFT JOIN resource_extra e ON e.resource_id = b.resource_id
WHERE  b.status = 'ACTIVE';

顺序修正

  • 将批量查询结果映射为 dict[resource_id] -> data
  • 按照原始 id_list 顺序重新组装结果,保持业务一致性。
id_list = compute_ids_by_range(params)        # 原始顺序
rows = db.select_many(
    """SELECT * FROM vw_resource_full WHERE resource_id IN %(ids)s""",
    {'ids': tuple(id_list)}
)
row_map = {row['resource_id']: row for row in rows}
result = [row_map[rid] for rid in id_list if rid in row_map]

排序逻辑简化

  • 去除排序阶段对缓存的依赖,改为直接使用视图中的 score 字段。
  • 若仍需复杂排序,可在 SQL 中完成:ORDER BY score DESC, distance ASC.

新实现中,我们首先通过构建包含所有ID的单个SQL查询获取数据,极大减少了数据库交互次数。紧接着,将查询结果根据原ID列表排序,从而替代了原来的自定义排序过程。值得一提的是,如果业务需要根据某字段排序,我们可以在数据库查询时直接使用ORDER BY子句完成,这样也省去了在应用层循环排序的开销。

效果验证

指标 优化前 优化后
平均响应时间 800‑1200 ms 90‑120 ms
数据库查询次数 N+1 1
99th 延迟 >2 s <300 ms

优化完成后,我们对接口性能进行了验证。通过在测试环境对比优化前后的日志打印,以及线上监控工具的追踪,我们观察到响应时间有了显著下降:

  • 在本地调试时,对相同查询条件的请求进行多次测试,优化前每次请求耗时大约在数百毫秒到上千毫秒不等,而优化后普遍能够稳定在几十毫秒左右,性能提升明显。
  • 在线上环境的监控中,之前该接口的99th百分位响应时间曾经达到数秒,优化发布后,99th响应时间降到了亚秒级(显著低于1000ms)。大部分请求的耗时从原来的500ms降低到100ms以内,性能提升达数倍以上。且在高并发情况下,数据库压力也有所减轻,接口超时告警不再出现。

通过以上数据,可以确定本次优化达到了预期效果:接口性能提升的同时,功能与结果保持正确。

经验总结

  • 避免 N+1 查询 :批量查询或联表查询能显著降低数据库压力。
  • 排序放在数据库端 :让数据库做擅长的事情,减少应用层循环。
  • 顺序一致性 :批量查询后需注意顺序恢复,ORDER BY FIELD 或应用层 dict 重排均可。
  • 监控与验证 :每次优化都需要配合链路追踪和监控指标,确保收益量化。