# Perf API

> Use when optimizing 接口性能, N+1 SQL/RPC, 循环远程调用, Hikari 连接池, 批量写入, or 同步转异步. Do not use for GC (backend-jvm-prod), EXPLAIN (backend-mysql-index), or incident 排查顺序 (backend-observability).

- Skill: `1398281322-a11y/perf-api` (Agent Skill)
- Install (CLI): `npx skillmds@latest add 1398281322-a11y/perf-api`
- Raw SKILL.md: https://api.skillmd.com/api/skills/1398281322-a11y/perf-api/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- Author: 1398281322-a11y (https://skillmd.com/u/1398281322-a11y)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/1398281322-a11y/perf-api

---


# 接口性能

## When to Invoke

接口 RT 高、QPS 上不去、面试「怎么优化一个接口」「N+1」「连接池怎么设」「循环里调 Feign」。已经知道慢在 **业务代码路径**（多次 IO、池配错、同步多余活）。

## When NOT

还没定位瓶颈 → `observability`（TraceId、P99、先测再改）。单条 SQL 慢 / EXPLAIN → `mysql-index`。Full GC / 堆 → `jvm-prod`。线程池无界 → `thread-pool`。Feign 超时数值 → `rpc-remote`。缓存击穿 → `cache-trap`。

## 风险（面试考点）

**先测后改。** 没有火焰图/Trace 就加缓存、加线程，是面试扣分项。

N+1：列表 20 条，循环里再查子表或调一次 Feign = 1+20 次网络。MyBatis `<collection select="...">` 默认就是 N+1。嵌套结果 JOIN 或 **两次查询 + 内存拼**（主列表 + `IN` 子表）次数固定。

循环 `insert` / `save`：MyBatis-Plus `saveBatch` 仍可能只是 JDBC batch；MySQL 要 `rewriteBatchedStatements=true` 才会合成多值 INSERT。`foreach` 一条超大 SQL 会撞 `max_allowed_packet`，按 200～1000 行切开。

线程池 200、Hikari **默认 10**：请求线程堵在 `getConnection`，报 `Connection is not available, request timed out`。Hikari 作者：**小池更好**，起点约 `(CPU核×2)+盘`（常 10～20），不是跟 Tomcat 线程数对齐。一个线程持有多连接时按 `Tn×(Cm-1)+1` 防死锁。

`maxLifetime` 必须比 MySQL `wait_timeout`（及中间 NAT/LB 超时）**至少短 30s**，否则池里是已死连接，表现为偶发 Communications link failure。

用户请求线程里做：发短信、算报表、写一堆日志 JSON、`SELECT *` 回 50 列、深 `offset` 翻页。用户不等的活丢 MQ。热路径日志不要 `toJson(huge)`。

## 方案选型（轻量优先）

| 症状 | 默认 |
|------|------|
| 循环查库 / 循环 Feign | 批量 `IN` / 批量 RPC，内存 Map 组装 |
| 循环 insert | `saveBatch` + `rewriteBatchedStatements` + 分批 |
| 获取连接超时 | 查泄漏（事务/连接未关）；池不是先加到 100 |
| 用户 RT 含非关键 IO | 同步只做成功路径，其余 MQ |
| IN 列表过大 | 分批，不要上万 ID 一条 SQL |

```java
List<Order> orders = orderMapper.selectPage(uid, cursor, 20);
List<Long> ids = orders.stream().map(Order::getId).toList();
Map<Long, List<Item>> items = itemMapper.selectByOrderIds(ids).stream()
    .collect(Collectors.groupingBy(Item::getOrderId));
orders.forEach(o -> o.setItems(items.getOrDefault(o.getId(), List.of())));
```

```yaml
spring.datasource.hikari:
  maximum-pool-size: 10
  connection-timeout: 30000
  max-lifetime: 1800000   # < wait_timeout - 30s
  leak-detection-threshold: 20000
```

JDBC URL 加 `rewriteBatchedStatements=true`（仅当确有批量写）。

## 反例

错误：for 每条订单 `feign.getMerchant(id)`。
正确：一次 `listByIds`，或下单时冗余必要字段。

错误：接口慢就把 Hikari 调成 200。
正确：先看是 SQL、泄漏、还是下游 RT；库侧连接过多会更慢。

错误：列表接口 `SELECT *` + 循环补用户名 + 同步写操作日志到 DB。
正确：必要列、批量补、日志异步。

错误：没压测就上本地缓存「肯定快」。
正确：Trace 看时间花在哪一段。

## 验证

- 一页 20 条，SQL/RPC 次数不随 N 线性涨（大约 2～3 次）。
- 连接泄漏检测能抓到未关闭的连接。
- 压测拐点：再加池或线程，RT 上升而不是 QPS 上升则停。

## 评审清单

- [ ] 无循环 IO（SQL/RPC/Redis）
- [ ] 批量写真正 batch，且分批
- [ ] 连接池小于 wait_timeout；泄漏检测打开
- [ ] 非关键路径不在用户线程
---

