全栈工程课 · 一个相册系统的上线之路
课程平台Chapter 11 · 性能优化

CHAPTER 11 · PERFORMANCE & CACHE

11

相册只有 100 个用户时很快,10 万个用户以后为什么越来越慢?

性能问题可能发生在任何一层。优化前必须先测量,再定位瓶颈,而不是凭感觉直接加 Redis。

真实现象

“慢”可能来自很多地方。

首页

打开需要 5 秒

接口

相册列表需要 2 秒

图片

照片很多时页面卡顿

数据库

MySQL CPU 持续升高

Go 服务

内存不断增加

缓存

Redis 加上以后反而数据错误

慢的是前端渲染,还是后端接口?

接口慢,是 Go 代码慢还是 MySQL 慢?

图片加载慢,应该优化 React、OSS 还是网络?

没有测量数据,能不能直接开始优化?

先测量 找瓶颈 → 做优化 → 再验证

全链路视角

用户一次操作,会经过很多层。

用户操作React 页面网络请求NginxGo 服务Redis / MySQLOSS 图片
层级瓶颈常见问题

React

页面层

重复请求、重复渲染、资源加载过多、图片过大。

Go / MySQL

服务层

连接池配置错误、慢查询、N+1、深分页、返回字段过多。

Redis / OSS

外部依赖

缓存不一致、缓存击穿、原图过大、签名或网络耗时。

前端性能

相册系统最容易慢在请求和图片。

减少重复请求

检查页面初始化是否重复请求、useEffect 依赖是否错误、是否禁止重复提交、是否处理请求竞态。

分页与按需加载

首次加载 20 张,滚动或翻页时再加载下一批。前端把 1 万条全部请求回来再分页,没有意义。

图片优化

列表不要加载原图,应使用 OSS 缩略图、懒加载、固定图片尺寸,首屏只加载可见图片。

构建资源

关注生产构建、文件 Hash、浏览器缓存、按页面拆包,以及是否引入体积巨大的依赖。

页面只显示 300×200 的缩略图,就不应该下载一张 10 MB 的原图。

接口与网络

响应体越大、接口越多,页面越容易慢。

错误信号

SELECT * 后把无关字段、内部字段、OSS Object Key、审核信息全部返回给列表页。

两个极端

一个页面请求 30 个接口

一个接口返回整个系统全部数据

更合理

按页面需要聚合
字段按展示需求返回
列表、详情、统计分清边界

查询字段和接口字段都应该按实际页面需要设计。减少接口数量,不等于制造万能接口。

Go 服务端

不要先优化语言细节,先看时间花在哪里。

Go 业务代码10ms
MySQL 查询1600ms
OSS 请求390ms

总耗时 2 秒,其中 MySQL 1.6 秒,此时优化 Go 循环几乎没有意义。先解决最慢的一层。

问题结论说明

外部调用没有超时

危险

MySQL、Redis、OSS 或 HTTP 服务卡住,会占用请求和 Goroutine。

连接池只看单实例

不够

5 个 Go 实例,每个 MaxOpenConns=50,理论最大连接数就是 250。

慢就多开 Goroutine

误区

并发可能把 MySQL、OSS 或内存打爆,并不等于性能更好。

MySQL 性能

ORM 不能代替 SQL 性能审查。

深分页

LIMIT 20 OFFSET 200000

翻到很深时,数据库仍然要跳过大量数据。可以进一步考虑基于 created_at + id 的游标分页。

索引服务于查询

WHERE album_id = ?
ORDER BY created_at DESC
LIMIT 20

索引要围绕 WHERE、ORDER BY、稳定排序和返回数量设计。

EXPLAIN 重点看什么
是否使用索引
使用了哪个索引
预计扫描多少行
是否全表扫描
是否额外排序或临时表

N+1 查询

查询 20 个相册 → 20 次查询封面 → 20 次 COUNT 照片,最终可能执行 41 次 SQL。

要检查 GORM 最终生成的 SQL,而不是只看 Go 代码是否简洁。

Redis

MySQL 已经能保存数据,为什么还需要 Redis?

Redis 适合做加速层,而不是替代 MySQL 成为业务事实来源。

MySQL 是数据事实来源 + Redis 是加速层

场景是否适合缓存原因

热门公开相册、用户基础信息、照片数量统计

可以评估

高频读取、变化较少、允许短时间不一致。

每月访问一次的设置页

通常不需要

低频接口加缓存可能收益很小,复杂度却增加。

唯一主数据、密码、强一致账户余额

不适合简单缓存

这类数据不能依赖 Redis 作为唯一来源。

请求数据查询 Redis未命中查 MySQL写入 Redis返回结果

更新时先更新 MySQL,再删除 Redis 缓存;下次读取时重新构建。不要只更新 Redis。

缓存风险

缓存会让系统更快,也会让系统更复杂。

Redis 不是银弹。缓存会引入一致性、穿透、击穿和雪崩问题,低频接口或本来就很快的查询未必需要缓存。

Nginx 与静态资源

静态资源可以缓存,带权限的 API 不要乱缓存。

可以优化

gzip / Brotli、Keep-Alive、带 Hash 的 JS/CSS 长期缓存、HTML 短缓存。

要小心

/api 不应被错误缓存,尤其是用户私有数据和带权限的接口。

正确顺序

性能优化不是“感觉慢就加 Redis”。

1. 明确慢在哪里2. 建立基准数据3. 找主要瓶颈4. 优化主要瓶颈5. 重新测试6. 检查副作用

不合格决策

感觉慢 → 直接加 Redis
给 AI 的性能分析任务
请分析相册列表接口的性能问题,不要直接重写全部代码。

分析重点:
1. 梳理 React、Nginx、Go、MySQL 的完整链路。
2. 给出需要采集的耗时数据。
3. 检查重复请求、N+1、SELECT * 和无分页。
4. 检查 GORM 最终生成的 SQL。
5. 给出需要验证的 EXPLAIN 结果。
6. 只有确认高频重复读取时,才考虑 Redis。
7. Redis 必须说明 Key、TTL、更新和删除策略。
8. 给出优化前后的验证方法。
9. 不要为了性能牺牲权限校验和数据正确性。

本章总结

  1. 性能优化必须从测量开始。
  2. 前端、网络、Nginx、Go、MySQL、Redis 和 OSS 都可能是瓶颈。
  3. 前端应减少重复请求,并使用分页、懒加载和缩略图。
  4. Go 服务应控制超时、连接池和并发边界。
  5. MySQL 优化应围绕真实 SQL、索引和执行计划。
  6. ORM 不能代替 SQL 性能审查。
  7. Redis 是加速层,不是 MySQL 的替代品。
  8. 缓存会引入一致性、穿透、击穿和雪崩问题。
  9. 不是所有接口都需要 Redis。
  10. 优化完成后必须重新测量,证明它确实有效。
不要因为系统慢就直接加 Redis。先找到最慢的一层,再解决最重要的问题。