使用 Redis 优化后端查询
Source: Dev.to
原始做法(舒适版)
接口逻辑很简单:
- 查询数据库
- 按分数排序用户
- 返回前 10 名
SELECT * FROM users ORDER BY score DESC LIMIT 10;
在有适当索引的情况下,小规模时还能正常工作,但排行榜具有以下特点:
- 访问频繁
- 更新频繁
- 竞争激烈、实时数据
每次请求都去数据库,很快就成了瓶颈。
第一次尝试:使用 Redis
Redis 看起来很完美:内存、快速、天生适合排名。
然而,在本地启动时出现错误:
Error: Address already in use
Port 6379 was already occupied.
尝试重启服务、杀掉进程都无效后,我决定把 Redis 隔离开来。
解决方案:Docker 化 Redis
docker run -d -p 6379:6379 --name redis-server redis
在容器中运行 Redis 使其:
- 被隔离
- 可移植
- 干净运行
- 易于重启
环境搞定后,我就可以继续前进了。
引入有序集合(ZSET)
Redis 有序集合会自动按分数对成员排序。
- 成员 → 用户 ID
- 分数 → 积分
这消除了 SQL 排序和大量数据库读取的需求。
更新用户分数
await redis.zadd("leaderboard", score, userId);
获取前 10 名
await redis.zrevrange("leaderboard", 0, 9, "WITHSCORES");
排名逻辑现在完全在内存中,延迟立刻提升。
我没预料到的隐藏瓶颈
在取到前 10 名的用户 ID 后,我还需要获取用户的其他信息(用户名、头像等):
for (let userId of topUsers) {
await redis.hgetall(`user:${userId}`);
}
这在 Redis 中引入了 N+1 问题:
- 1 次请求 → 获取排行榜
- 10 次请求 → 分别获取每个用户
结果是 11 次网络往返,增加约 100 ms。
真正的解决方案:Redis 管道(Pipelining)
Redis 管道可以批量发送命令,减少往返次数。
const pipeline = redis.pipeline();
for (let userId of topUsers) {
pipeline.hgetall(`user:${userId}`);
}
const users = await pipeline.exec();
现在只需要 一次 网络往返,消除了 N+1 带来的延迟。
结果
| 阶段 | 延迟 |
|---|---|
| 数据库排序 | ~200 ms |
| Redis(无管道) | ~120 ms |
| Redis + 管道 | ~20 ms |
整体提升约 10 倍,主要得益于削减网络调用。
我的收获
- 基础设施问题优先——如果 Redis 没跑起来,其他一切都无从谈起。
- 数据结构决定性能——ZSET 完全消除了重复排序。
- N+1 问题不只出现在数据库——任何远程系统都有可能出现。
- 网络延迟隐形却昂贵——即使是“快”的系统,调用次数过多也会变慢。
- Docker 简化后端生活——容器化依赖可以避免操作系统层面的冲突。
最终架构
- 分数更新 →
ZADD - 获取前 10 →
ZREVRANGE - 批量获取用户数据 → pipeline +
EXEC - 返回响应
不再访问数据库,全部在内存中完成,网络调用最少,响应时间约 20 ms。
结束语
优化不是把工具往问题上砸,而是找出时间真正花在哪里。在本例中,最大收益来自:
- 修复运行环境
- 选对数据结构
- 减少网络往返
解决了这三点,差距立现。