前端转全栈 11:缓存与性能——Redis、索引、N+1 与限流

前端全栈tutorial性能缓存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(由浅入深)

  1. 消除 N+1(最优先,性价比最高)
  2. 加对索引(外键、高频查询列)
  3. 缓存热点(Next.js 渲染缓存 / Redis)
  4. 限流防刷(429)
  5. 数据库层面:分页(LIMIT/OFFSET 或游标)、只查需要的列(select 指定字段,别 select *
  6. 监控慢查询(Supabase 有查询性能面板)

总结

  • N+1 是头号杀手,用联表/预加载一次查回。
  • 索引加速查询,但按需建。
  • Redis 缓存热点,注意 TTL 失效。
  • 限流返回 429,保护服务。
  • 优化顺序:N+1 → 索引 → 缓存 → 限流 → 分页/监控。

下一篇,我们给代码加「质量保险」——全栈测试:单元 / 集成 / E2E


练习:打开你的项目,搜所有「在循环里 await 数据库查询」的地方,改成一次联表查询;再挑一个高频查询列确认它已经有索引。

Comments

Sign in to leave a comment.