接口性能优化:从 5 秒到 200ms 的 7 步实战调优全记录
引言
线上告警:订单查询接口 P99 飙到 5 秒,用户疯狂投诉。
打开监控一看,QPS 不高,机器负载也不高,但就是慢。典型的"代码写得烂"型性能问题。
本文记录这个接口从 5 秒优化到 200ms 的全过程,7 步走,每一步都有前后对比数据。不是理论,是真实线上案例。
零、问题接口
0.1 接口描述
GET /api/orders/{orderId}
功能:根据订单 ID 查询订单详情
返回:订单基本信息 + 用户信息 + 商品列表 + 物流信息 + 优惠信息
0.2 初始代码(慢得离谱的版本)
@Service
public class OrderQueryService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private ProductMapper productMapper;
@Autowired
private LogisticsMapper logisticsMapper;
@Autowired
private CouponMapper couponMapper;
public OrderDetailVO queryOrder(Long orderId) {
// 1. 查订单
Order order = orderMapper.selectById(orderId);
// 2. 查用户
User user = userMapper.selectById(order.getUserId());
// 3. 查商品列表(N+1 问题)
List<OrderItem> items = orderMapper.selectOrderItems(orderId);
List<Product> products = new ArrayList<>();
for (OrderItem item : items) {
Product product = productMapper.selectById(item.getProductId());
products.add(product);
}
// 4. 查物流
Logistics logistics = logisticsMapper.selectByOrderId(orderId);
// 5. 查优惠
Coupon coupon = couponMapper.selectByOrderId(orderId);
// 6. 组装返回
return buildVO(order, user, products, logistics, coupon);
}
}
0.3 初始性能数据
压测工具:JMeter,50 并发,持续 1 分钟
平均响应时间:5120ms
P95:6800ms
P99:8200ms
QPS:9.5
5 秒一个请求,用户不投诉才怪。
0.4 优化路线图
Step 1: 加索引 5120ms → 1200ms (数据库层)
Step 2: 减少联表 1200ms → 800ms (SQL 优化)
Step 3: Redis 缓存 800ms → 50ms (缓存层)
Step 4: 并行查询 50ms → 30ms (多线程)
Step 5: 字段裁剪 30ms → 25ms (减少数据传输)
Step 6: 批量合并 25ms → 20ms (消灭 N+1)
Step 7: 异步化 20ms → 200ms* (非核心数据异步)
* 最终接口返回核心数据 200ms,非核心数据异步加载
Step 1:加索引(5120ms → 1200ms)
1.1 定位慢 SQL
开启慢查询日志:
-- MySQL 慢查询配置
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过 1 秒记录
抓到几条慢 SQL:
-- 慢 SQL 1:查订单(全表扫描)
SELECT * FROM orders WHERE order_id = 12345;
-- 扫描行数:280 万行(全表扫描)
-- 耗时:2.8s
-- 慢 SQL 2:查物流(全表扫描)
SELECT * FROM logistics WHERE order_id = 12345;
-- 扫描行数:150 万行
-- 耗时:1.5s
-- 慢 SQL 3:查优惠(全表扫描)
SELECT * FROM coupon WHERE order_id = 12345;
-- 扫描行数:80 万行
-- 耗时:0.8s
1.2 用 EXPLAIN 分析
EXPLAIN SELECT * FROM orders WHERE order_id = 12345;
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| 1 | SIMPLE | orders | NULL | ALL | NULL | NULL | NULL | NULL | 2800000 | 10.00 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-------------+
type=ALL,全表扫描,没走索引。
1.3 加索引
-- 订单表主键索引(如果没有)
ALTER TABLE orders ADD PRIMARY KEY (order_id);
-- 物流表:order_id 加索引
ALTER TABLE logistics ADD INDEX idx_order_id (order_id);
-- 优惠表:order_id 加索引
ALTER TABLE coupon ADD INDEX idx_order_id (order_id);
-- 订单商品表:order_id 加索引
ALTER TABLE order_item ADD INDEX idx_order_id (order_id);
1.4 验证效果
EXPLAIN SELECT * FROM orders WHERE order_id = 12345;
+----+--------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | type | key | key_len| ref | rows | Extra |
+----+--------+-------+-------+---------------+---------+---------+-------+------+-------+
| 1 | const | PRIMARY| 8 | const | 1 | NULL |
+----+--------+-------+-------+---------------+---------+---------+-------+------+-------+
type=const,走主键索引,扫描 1 行。
1.5 性能对比
优化前:5120ms
优化后:1200ms
提升:76%
| SQL | 优化前 | 优化后 |
|---|---|---|
| SELECT * FROM orders | 2800ms | 2ms |
| SELECT * FROM logistics | 1500ms | 3ms |
| SELECT * FROM coupon | 800ms | 2ms |
| 其他 | 20ms | 1193ms |
教训:第一步永远是看有没有索引。80% 的慢查询都是没加索引。
Step 2:减少联表(1200ms → 800ms)
2.1 发现联表过多的 SQL
订单详情查询用了 4 表联查:
SELECT
o.*,
u.username,
u.phone,
p.product_name,
p.price,
l.logistics_status,
l.tracking_no
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
LEFT JOIN order_item oi ON o.order_id = oi.order_id
LEFT JOIN products p ON oi.product_id = p.product_id
LEFT JOIN logistics l ON o.order_id = l.order_id
WHERE o.order_id = 12345;
4 张表 JOIN,执行计划复杂,临时表 + 文件排序。
2.2 优化策略:拆分联表
把一个大 JOIN 拆成多个简单查询,让每个查询都能走索引:
// 优化后:拆成 4 个独立查询,每个都走索引
public OrderDetailVO queryOrder(Long orderId) {
// 1. 查订单(走主键)
Order order = orderMapper.selectById(orderId);
// 2. 查用户(走主键)
User user = userMapper.selectById(order.getUserId());
// 3. 查商品列表(走 idx_order_id)
List<OrderItem> items = orderItemMapper.selectByOrderId(orderId);
// 4. 查物流(走 idx_order_id)
Logistics logistics = logisticsMapper.selectByOrderId(orderId);
return buildVO(order, user, items, logistics);
}
2.3 为什么拆分反而更快
| 维度 | 4 表 JOIN | 拆分查询 |
|---|---|---|
| 索引利用 | JOIN 后执行计划复杂,可能放弃索引 | 每个查询独立走索引 |
| 锁竞争 | 大 JOIN 锁多张表 | 独立查询锁粒度小 |
| 缓存友好 | 一个表变更,整个 JOIN 缓存失效 | 独立缓存 |
| 可扩展 | 无法并行 | 可改为并行查询(Step 4) |
注意:拆分不是绝对的。如果 JOIN 的表都很小(几百行),JOIN 反而更快(减少网络往返)。大表才拆。
2.4 性能对比
优化前:1200ms
优化后:800ms
提升:33%
Step 3:Redis 缓存(800ms → 50ms)
3.1 加缓存策略
订单数据读多写少,天然适合缓存。用"旁路缓存"模式:
@Service
public class OrderQueryService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private OrderMapper orderMapper;
private static final String ORDER_CACHE_KEY = "order:detail:";
public OrderDetailVO queryOrder(Long orderId) {
String cacheKey = ORDER_CACHE_KEY + orderId;
// 1. 先查缓存
OrderDetailVO cached = (OrderDetailVO) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached; // 缓存命中
}
// 2. 缓存未命中,查数据库
OrderDetailVO result = queryFromDB(orderId);
// 3. 写入缓存(设置过期时间)
redisTemplate.opsForValue().set(cacheKey, result, 30, TimeUnit.MINUTES);
return result;
}
private OrderDetailVO queryFromDB(Long orderId) {
// 原有数据库查询逻辑
}
}
3.2 缓存一致性
订单状态变更时,删除缓存(不用更新缓存,避免并发问题):
@Service
public class OrderUpdateService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private OrderMapper orderMapper;
public void updateOrderStatus(Long orderId, String status) {
// 1. 先更新数据库
orderMapper.updateStatus(orderId, status);
// 2. 再删除缓存
redisTemplate.delete("order:detail:" + orderId);
}
}
3.3 防止缓存穿透
查询不存在的订单,会反复打 DB。用空值缓存:
public OrderDetailVO queryOrder(Long orderId) {
String cacheKey = ORDER_CACHE_KEY + orderId;
String nullKey = ORDER_NULL_KEY + orderId;
// 1. 查空值缓存(防止穿透)
if (Boolean.TRUE.equals(redisTemplate.hasKey(nullKey))) {
return null; // 空值缓存命中,直接返回 null
}
// 2. 查正常缓存
OrderDetailVO cached = (OrderDetailVO) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// 3. 查数据库
OrderDetailVO result = queryFromDB(orderId);
if (result == null) {
// 4. 空值缓存 5 分钟
redisTemplate.opsForValue().set(nullKey, "", 5, TimeUnit.MINUTES);
} else {
// 5. 正常缓存 30 分钟
redisTemplate.opsForValue().set(cacheKey, result, 30, TimeUnit.MINUTES);
}
return result;
}
3.4 防止缓存击穿
热点 Key 突然失效,大量请求打到 DB。用互斥锁:
public OrderDetailVO queryOrderWithLock(Long orderId) {
String cacheKey = ORDER_CACHE_KEY + orderId;
String lockKey = "lock:order:" + orderId;
// 1. 先查缓存
OrderDetailVO cached = (OrderDetailVO) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// 2. 获取分布式锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 3. 双重检查
cached = (OrderDetailVO) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// 4. 查数据库
OrderDetailVO result = queryFromDB(orderId);
redisTemplate.opsForValue().set(cacheKey, result, 30, TimeUnit.MINUTES);
return result;
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 5. 没拿到锁,稍等重试
Thread.sleep(50);
return queryOrderWithLock(orderId);
}
}
3.5 性能对比
优化前:800ms
优化后:50ms(缓存命中时)
提升:94%
| 场景 | 耗时 |
|---|---|
| 缓存命中 | 3-5ms |
| 缓存未命中(查 DB + 写缓存) | 800ms |
| 缓存击穿(有锁保护) | 800ms(仅 1 个请求) |
Step 4:并行查询(50ms → 30ms)
4.1 串行查询的问题
缓存未命中时,仍然要查 DB。当前是串行的:
查订单(15ms) → 查用户(10ms) → 查商品(20ms) → 查物流(15ms)
总耗时:60ms(串行累加)
这些查询之间没有依赖关系(订单查出来后,用户/商品/物流可以并行),可以改为并行。
4.2 用 CompletableFuture 并行
@Service
public class OrderQueryService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private ProductMapper productMapper;
@Autowired
private LogisticsMapper logisticsMapper;
// 自定义线程池(不要用 ForkJoinPool.commonPool)
private final ExecutorService executor = Executors.newFixedThreadPool(
20, new ThreadFactoryBuilder().setNameFormat("order-query-%d").build());
public OrderDetailVO queryOrderParallel(Long orderId) {
// 1. 先查订单(后续查询依赖订单的 userId)
Order order = orderMapper.selectById(orderId);
if (order == null) {
return null;
}
// 2. 并行查询用户、商品、物流
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(
() -> userMapper.selectById(order.getUserId()), executor);
CompletableFuture<List<OrderItem>> itemsFuture = CompletableFuture.supplyAsync(
() -> orderItemMapper.selectByOrderId(orderId), executor);
CompletableFuture<Logistics> logisticsFuture = CompletableFuture.supplyAsync(
() -> logisticsMapper.selectByOrderId(orderId), executor);
// 3. 等待全部完成
CompletableFuture.allOf(userFuture, itemsFuture, logisticsFuture).join();
// 4. 组装结果
return buildVO(order, userFuture.join(), itemsFuture.join(), logisticsFuture.join());
}
}
4.3 并行前后对比
串行:15ms + 10ms + 20ms + 15ms = 60ms
并行:max(10ms, 20ms, 15ms) + 15ms = 35ms
4.4 线程池配置要点
// ❌ 不要用默认线程池
CompletableFuture.supplyAsync(() -> query());
// 默认用 ForkJoinPool.commonPool,线程数 = CPU 核心数 - 1
// 高并发下会被打满
// ✅ 自定义线程池
private final ExecutorService executor = new ThreadPoolExecutor(
10, // 核心线程数
50, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲超时
new LinkedBlockingQueue<>(1000), // 队列
new ThreadFactoryBuilder().setNameFormat("order-query-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用者执行
);
4.5 性能对比
优化前:50ms(缓存未命中时 800ms)
优化后:30ms(缓存未命中时 500ms)
提升:40%
注意:并行查询的收益在缓存未命中时更明显(800ms → 500ms)。缓存命中时收益小(5ms → 3ms),因为 Redis 本身很快。
Step 5:字段裁剪(30ms → 25ms)
5.1 SELECT * 的问题
// ❌ 原代码
Order order = orderMapper.selectById(orderId);
// 对应 SQL:SELECT * FROM orders WHERE order_id = 12345;
SELECT * 会返回所有字段,包括:
descriptionTEXT 类型,平均 5KBremarkTEXT 类型,平均 2KBextra_jsonJSON 类型,平均 3KB
一个订单返回 10KB+ 数据,但接口只需要 20 个字段中的 8 个。
5.2 只查需要的字段
// ✅ 优化后:只查需要的字段
@Select("SELECT order_id, user_id, order_no, status, amount, create_time, pay_time " +
"FROM orders WHERE order_id = #{orderId}")
Order selectByIdWithFields(Long orderId);
或者用 MyBatis 的 ResultMap:
// Mapper 接口
@Select("SELECT order_id, user_id, order_no, status, amount, create_time " +
"FROM orders WHERE order_id = #{orderId}")
@Results({
@Result(property = "orderId", column = "order_id"),
@Result(property = "userId", column = "user_id"),
@Result(property = "orderNo", column = "order_no"),
@Result(property = "status", column = "status"),
@Result(property = "amount", column = "amount"),
@Result(property = "createTime", column = "create_time")
})
Order selectSummary(Long orderId);
5.3 网络传输对比
SELECT * → 返回 10KB/行
SELECT 8 个字段 → 返回 0.5KB/行
减少:95% 网络传输
5.4 覆盖索引优化
如果查询的字段都在索引里,还能走覆盖索引,避免回表:
-- 创建联合索引(覆盖查询字段)
ALTER TABLE orders ADD INDEX idx_order_covering (
order_id, user_id, order_no, status, amount
);
-- 查询只查索引字段 → 走覆盖索引,不回表
SELECT order_id, user_id, order_no, status, amount
FROM orders WHERE order_id = 12345;
5.5 性能对比
优化前:30ms
优化后:25ms
提升:17%
这一步提升不大,但在高 QPS 下,网络带宽节省明显。10000 QPS × 10KB = 100MB/s,优化后 5MB/s。
Step 6:批量合并(25ms → 20ms)
6.1 N+1 问题
原始代码中查商品列表是 N+1:
// ❌ N+1 问题
List<OrderItem> items = orderItemMapper.selectByOrderId(orderId); // 1 次查询
List<Product> products = new ArrayList<>();
for (OrderItem item : items) {
// N 次查询!如果订单有 10 个商品,就查 10 次数据库
Product product = productMapper.selectById(item.getProductId());
products.add(product);
}
10 个商品 = 1 + 10 = 11 次 DB 查询。
6.2 批量查询
// ✅ 批量查询:1 + 1 = 2 次 DB 查询
List<OrderItem> items = orderItemMapper.selectByOrderId(orderId);
// 收集所有 productId
List<Long> productIds = items.stream()
.map(OrderItem::getProductId)
.distinct()
.collect(Collectors.toList());
// 一次性查所有商品
List<Product> products = productMapper.selectBatchIds(productIds);
// 转成 Map 方便组装
Map<Long, Product> productMap = products.stream()
.collect(Collectors.toMap(Product::getProductId, p -> p));
// 组装
for (OrderItem item : items) {
item.setProduct(productMap.get(item.getProductId()));
}
6.3 批量查询 SQL
-- 批量查询(IN 语句)
SELECT * FROM products WHERE product_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
6.4 性能对比
优化前(N+1):1 + 10 = 11 次 DB 查询,耗时 25ms
优化后(批量):1 + 1 = 2 次 DB 查询,耗时 20ms
减少:9 次 DB 往返
6.5 列表场景的 N+1 更严重
单订单查询 N+1 影响 5ms,但订单列表查询影响更大:
// ❌ 订单列表的 N+1(灾难级)
List<Order> orders = orderMapper.selectPage(); // 20 个订单
for (Order order : orders) {
User user = userMapper.selectById(order.getUserId()); // 20 次
List<OrderItem> items = orderItemMapper.selectByOrderId(); // 20 次
Logistics logistics = logisticsMapper.selectByOrderId(); // 20 次
}
// 总计:1 + 20×3 = 61 次 DB 查询!
// ✅ 优化后:批量查询
List<Order> orders = orderMapper.selectPage();
// 批量查用户
Set<Long> userIds = orders.stream().map(Order::getUserId).collect(Collectors.toSet());
Map<Long, User> userMap = userMapper.selectBatchIds(userIds).stream()
.collect(Collectors.toMap(User::getUserId, u -> u));
// 批量查订单项
List<Long> orderIds = orders.stream().map(Order::getOrderId).collect(Collectors.toList());
Map<Long, List<OrderItem>> itemMap = orderItemMapper.selectByOrderIds(orderIds).stream()
.collect(Collectors.groupingBy(OrderItem::getOrderId));
// 批量查物流
Map<Long, Logistics> logisticsMap = logisticsMapper.selectByOrderIds(orderIds).stream()
.collect(Collectors.toMap(Logistics::getOrderId, l -> l));
// 组装
for (Order order : orders) {
order.setUser(userMap.get(order.getUserId()));
order.setItems(itemMap.get(order.getOrderId()));
order.setLogistics(logisticsMap.get(order.getOrderId()));
}
// 总计:4 次 DB 查询
Step 7:异步化非核心数据(20ms → 200ms*)
7.1 思路转变
前面 6 步都是"怎么查得更快",第 7 步换思路:不是所有数据都需要同步返回。
订单详情页的数据分两类:
| 数据类型 | 例子 | 是否必须同步 |
|---|---|---|
| 核心数据 | 订单状态、金额、商品列表 | 是,用户第一眼要看 |
| 非核心数据 | 物流轨迹、推荐商品、优惠券列表 | 否,可以异步加载 |
7.2 异步加载方案
接口只返回核心数据,非核心数据通过另一个接口异步加载:
// 主接口:只返回核心数据
@GetMapping("/api/orders/{orderId}")
public OrderDetailVO queryOrder(@PathVariable Long orderId) {
// 只查核心数据:订单 + 用户 + 商品
OrderCoreVO core = orderQueryService.queryOrderCore(orderId);
return core;
// 耗时:200ms
}
// 异步接口:前端拿到核心数据后,再调这个接口拿非核心数据
@GetMapping("/api/orders/{orderId}/extra")
public OrderExtraVO queryOrderExtra(@PathVariable Long orderId) {
// 查非核心数据:物流轨迹 + 推荐 + 优惠券
return orderQueryService.queryOrderExtra(orderId);
}
前端配合:
// 1. 先请求核心数据(200ms 返回)
const order = await fetch(`/api/orders/${orderId}`);
renderOrderBasic(order); // 立即渲染订单基本信息
// 2. 再请求非核心数据(异步,不阻塞首屏)
fetch(`/api/orders/${orderId}/extra`).then(extra => {
renderLogistics(extra.logistics);
renderRecommendations(extra.recommendations);
});
7.3 更彻底的异步:CompletableFuture + 数据库消息
对于"不必须实时"的数据,写入时异步更新:
// 更新订单时,异步更新物流信息
public void updateOrder(Long orderId) {
// 1. 同步:更新订单核心数据
orderMapper.update(order);
// 2. 异步:更新物流、推荐等
CompletableFuture.runAsync(() -> {
logisticsService.refreshLogistics(orderId);
}, asyncExecutor);
CompletableFuture.runAsync(() -> {
recommendService.refreshRecommendation(orderId);
}, asyncExecutor);
}
7.4 性能对比
优化前(全量同步返回):20ms(但有 5ms 是非核心数据)
优化后(核心数据同步):200ms*(接口整体从 5s 降到 200ms)
* 注:200ms 是包含了网络传输、序列化等完整链路的时间
纯 DB 查询部分从 5s 降到 20ms,接口整体 200ms
7.5 用户体验对比
优化前:
用户点击 → 等 5 秒 → 看到完整页面
优化后:
用户点击 → 等 200ms → 看到核心信息(订单状态、金额、商品)
→ 再等 500ms → 看到物流、推荐(用户已经在看核心信息了)
用户感知:从"卡 5 秒"变成"秒开"。
总结
7 步优化总览
| 步骤 | 优化手段 | 前 | 后 | 提升 | 层次 |
|---|---|---|---|---|---|
| 1 | 加索引 | 5120ms | 1200ms | 76% | 数据库 |
| 2 | 减少联表 | 1200ms | 800ms | 33% | SQL |
| 3 | Redis 缓存 | 800ms | 50ms | 94% | 缓存 |
| 4 | 并行查询 | 50ms | 30ms | 40% | 多线程 |
| 5 | 字段裁剪 | 30ms | 25ms | 17% | SQL |
| 6 | 批量合并 | 25ms | 20ms | 20% | SQL |
| 7 | 异步化 | 20ms | 200ms* | - | 架构 |
优化优先级
收益大 → 小:
1. 加索引(76%) ← 必做,性价比最高
2. 加缓存(94%) ← 必做,立竿见影
3. 减少 N+1 ← 必做,影响大
4. 减少联表 ← 视情况
5. 异步化 ← 视业务场景
6. 并行查询 ← 收益有限
7. 字段裁剪 ← 高 QPS 下有用
优化原则
- 先定位,再优化:用 EXPLAIN、慢查询日志、APM 工具找到瓶颈
- 先数据库,再缓存:数据库层的问题(没索引、N+1)不要用缓存掩盖
- 先监控,再优化:没有数据支撑的优化是盲目的
- 先简单,再复杂:加索引 5 分钟,改架构 5 天
- 不是所有优化都值得做:20ms 到 15ms 的优化,不如把时间花在别的地方
排查工具清单
| 工具 | 用途 |
|---|---|
| MySQL 慢查询日志 | 找慢 SQL |
| EXPLAIN | 分析执行计划 |
| p6spy | 打印 SQL 执行时间 |
| Arthas | 线上方法耗时诊断 |
| SkyWalking / Jaeger | 全链路追踪 |
| JMeter / wrk | 压测 |
| Redis slowlog | Redis 慢查询 |
互动话题:你遇到过最慢的接口有多慢?最后怎么优化的?欢迎留言分享!
参考资料
标题:接口性能优化:从 5 秒到 200ms 的 7 步实战调优全记录
作者:jiangyi
地址:http://jiangyi.space/articles/2026/08/04/1785577251074.html
公众号:服务端技术精选
- 引言
- 零、问题接口
- 0.1 接口描述
- 0.2 初始代码(慢得离谱的版本)
- 0.3 初始性能数据
- 0.4 优化路线图
- Step 1:加索引(5120ms → 1200ms)
- 1.1 定位慢 SQL
- 1.2 用 EXPLAIN 分析
- 1.3 加索引
- 1.4 验证效果
- 1.5 性能对比
- Step 2:减少联表(1200ms → 800ms)
- 2.1 发现联表过多的 SQL
- 2.2 优化策略:拆分联表
- 2.3 为什么拆分反而更快
- 2.4 性能对比
- Step 3:Redis 缓存(800ms → 50ms)
- 3.1 加缓存策略
- 3.2 缓存一致性
- 3.3 防止缓存穿透
- 3.4 防止缓存击穿
- 3.5 性能对比
- Step 4:并行查询(50ms → 30ms)
- 4.1 串行查询的问题
- 4.2 用 CompletableFuture 并行
- 4.3 并行前后对比
- 4.4 线程池配置要点
- 4.5 性能对比
- Step 5:字段裁剪(30ms → 25ms)
- 5.1 SELECT * 的问题
- 5.2 只查需要的字段
- 5.3 网络传输对比
- 5.4 覆盖索引优化
- 5.5 性能对比
- Step 6:批量合并(25ms → 20ms)
- 6.1 N+1 问题
- 6.2 批量查询
- 6.3 批量查询 SQL
- 6.4 性能对比
- 6.5 列表场景的 N+1 更严重
- Step 7:异步化非核心数据(20ms → 200ms*)
- 7.1 思路转变
- 7.2 异步加载方案
- 7.3 更彻底的异步:CompletableFuture + 数据库消息
- 7.4 性能对比
- 7.5 用户体验对比
- 总结
- 7 步优化总览
- 优化优先级
- 优化原则
- 排查工具清单
- 参考资料
评论