前端转全栈 11:缓存与性能——Redis、索引、N+1 与限流
系列目录:本文是「前端转全栈:Next.js + Supabase 实战」系列的第 11 篇。功能对了、安全也守了,但用户一多就卡——这一篇解决「慢」。
前端关心首屏、打包体积;全栈还要关心服务端和数据库的吞吐。性能问题大多来自同一个根源:对数据库打了太多、太慢的查询。
一、N+1 查询:最常见的隐形杀手
假设要展示 10 篇文章,每篇下面显示作者名:
// ❌ N+1:先查 10 篇文章,再为每篇单独查一次作者 → 1 + 10 = 11 次查询
const posts = await db.posts.findMany()
for (const p of posts) {
p.author = await db.users.find(p.authorId) // 循环里又查了 10 次
}
解法:一次 JOIN / 联表查回来。
// Supabase 一次拿文章+作者
supabase.from("posts").select("title, users(name)") // 内部 JOIN,1 次查询
ORM 里用 include / with 预加载(Prisma 的 include、Drizzle 的 with)。凡是「循环里查库」几乎都是 N+1,必须消除。
二、索引:第 3 篇埋下的伏笔
WHERE post_slug = ... 高频查询,没有索引就会全表扫描。回顾第 3 篇:
CREATE INDEX idx_comments_post_slug ON comments (post_slug);
经验:
- 高频
WHERE/JOIN/ORDER BY的列要建索引。 - 外键列一定要建索引(否则关联查询爆炸)。
- 但索引拖慢写入、占空间,按需建,别滥建。
三、Redis:把热点数据放内存
数据库再快也是磁盘 IO。把「读多写少」的热点(首页列表、热门文章、配置)放 Redis(内存 KV),速度提升一个数量级。
import Redis from "ioredis"
const redis = new Redis()
// 读:先查缓存,没有再查库并回填
async function getHotPosts() {
const cached = await redis.get("hot_posts")
if (cached) return JSON.parse(cached)
const posts = await db.posts.findHot()
await redis.setex("hot_posts", 60, JSON.stringify(posts)) // 缓存 60 秒
return posts
}
缓存三件套:查缓存 → 未命中查库 → 回填缓存。失效策略用 setex 设 TTL,避免数据永远不更新。
本站用 Next.js 的
unstable_cache做渲染层缓存(第 5 篇),和 Redis 是不同层。小项目 Next.js 内置缓存通常够;要跨服务共享热点才上 Redis。
四、限流:保护你的服务不被打爆
恶意刷接口或流量突增,可能拖垮数据库。限流(Rate Limit)给每个用户/IP 设阈值:
// 简易内存限流(生产用 Redis 更好)
const hits = new Map<string, number>()
function rateLimit(ip: string, limit = 10) {
const count = (hits.get(ip) ?? 0) + 1
hits.set(ip, count)
return count <= limit
}
// 在 Route Handler 里:if (!rateLimit(req.ip)) return ApiError("请求过快", 429)
返回 429 Too Many Requests 是标准做法。生产环境推荐 @upstash/ratelimit + Redis,或用云平台的 WAF/CDN 层限流。
五、性能优化 checklist(由浅入深)
- 消除 N+1(最优先,性价比最高)
- 加对索引(外键、高频查询列)
- 缓存热点(Next.js 渲染缓存 / Redis)
- 限流防刷(429)
- 数据库层面:分页(
LIMIT/OFFSET或游标)、只查需要的列(select指定字段,别select *) - 监控慢查询(Supabase 有查询性能面板)
总结
- N+1 是头号杀手,用联表/预加载一次查回。
- 索引加速查询,但按需建。
- Redis 缓存热点,注意 TTL 失效。
- 限流返回 429,保护服务。
- 优化顺序:N+1 → 索引 → 缓存 → 限流 → 分页/监控。
下一篇,我们给代码加「质量保险」——全栈测试:单元 / 集成 / E2E。
练习:打开你的项目,搜所有「在循环里 await 数据库查询」的地方,改成一次联表查询;再挑一个高频查询列确认它已经有索引。
Comments
Sign in to leave a comment.