Redis 实战:从入门到线上故障
Redis 用了三年,踩过不少坑。记录一下经验。
基础用法
缓存
最常见的场景:
async function getUser(id) {
const cacheKey = `user:${id}`;
// 先查缓存
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 查数据库
const user = await db.users.find(id);
// 写缓存,过期 1 小时
await redis.setex(cacheKey, 3600, JSON.stringify(user));
return user;
}
分布式锁
async function withLock(key, ttl, fn) {
const lockKey = `lock:${key}`;
const token = crypto.randomUUID();
// SET NX EX 原子操作
const acquired = await redis.set(lockKey, token, 'NX', 'EX', ttl);
if (!acquired) {
throw new Error('获取锁失败');
}
try {
return await fn();
} finally {
// Lua 脚本保证原子性
await redis.eval(`
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
end
return 0
`, [lockKey, token]);
}
}
限流
async function rateLimit(key, limit, window) {
const now = Date.now();
const windowStart = now - window * 1000;
// 使用 ZSET 实现滑动窗口
await redis
.multi()
.zremrangebyscore(key, 0, windowStart)
.zadd(key, now, `${now}-${Math.random()}`)
.zcard(key)
.expire(key, window)
.exec();
const count = await redis.zcard(key);
return count <= limit;
}
数据结构选型
| 结构 | 场景 | 复杂度 |
|---|---|---|
| String | 缓存、计数器、锁 | O(1) |
| Hash | 对象缓存 | O(1) |
| List | 队列、最新列表 | O(N) |
| Set | 去重、标签 | O(1) |
| ZSet | 排行榜、延迟队列 | O(log N) |
| Stream | 消息队列 | O(1) |
线上故障案例
案例 1:缓存穿透
现象:数据库 CPU 飙升,Redis 命中率只有 10%。
原因:大量请求查询不存在的 key,缓存里没有,全打到数据库。
解决:
async function getUser(id) {
const cacheKey = `user:${id}`;
const cached = await redis.get(cacheKey);
if (cached === 'null') {
// 缓存了空值,直接返回
return null;
}
if (cached) {
return JSON.parse(cached);
}
const user = await db.users.find(id);
if (user) {
await redis.setex(cacheKey, 3600, JSON.stringify(user));
} else {
// 空值也缓存,过期时间短一点
await redis.setex(cacheKey, 60, 'null');
}
return user;
}
案例 2:缓存雪崩
现象:凌晨 3 点,服务突然大量超时。
原因:大量缓存同时过期,瞬间压力打到数据库。
解决:过期时间加随机值:
const baseExpire = 3600;
const randomExpire = Math.random() * 600; // 0-10 分钟随机
await redis.setex(key, baseExpire + randomExpire, value);
案例 3:大 Key
现象:Redis 偶尔卡顿,info 看到 used_memory 持续增长。
原因:有个 List 存了 100 万条数据,每次 lrange 操作都很慢。
解决:
- 拆分大 Key
- 使用 hash 结构分片
// 之前:一个大 List
await redis.lpush('messages', ...messages);
// 之后:按日期分片
await redis.lpush(`messages:${date}`, ...messages);
案例 4:热 Key
现象:某个接口响应慢,Redis 单节点 CPU 高。
原因:一个热点 Key 被高频访问。
解决:
// 本地缓存 + Redis 多级缓存
const localCache = new Map();
async function getHotData(key) {
// 先查本地
if (localCache.has(key)) {
return localCache.get(key);
}
// 再查 Redis
const data = await redis.get(key);
if (data) {
localCache.set(key, data);
setTimeout(() => localCache.delete(key), 5000); // 本地缓存 5 秒
}
return data;
}
性能优化
Pipeline 批量操作
// 不好:多次网络往返
for (const item of items) {
await redis.set(`item:${item.id}`, JSON.stringify(item));
}
// 好:一次网络往返
const pipeline = redis.pipeline();
for (const item of items) {
pipeline.set(`item:${item.id}`, JSON.stringify(item));
}
await pipeline.exec();
Lua 脚本
复杂逻辑用 Lua 保证原子性:
-- 扣库存
local stock = redis.call('get', KEYS[1])
if tonumber(stock) >= tonumber(ARGV[1]) then
redis.call('decrby', KEYS[1], ARGV[1])
return 1
end
return 0
监控指标
| 指标 | 告警阈值 |
|---|---|
| 内存使用率 | > 80% |
| 命令延迟 | > 10ms |
| 连接数 | > 80% maxclients |
| 键空间命中率 | < 80% |
| 持久化延迟 | > 1s |
总结
Redis 很强,但用好用坏差距很大。关键点:
- 理解数据结构,选对的
- 防止缓存穿透/雪崩/击穿
- 避免大 Key、热 Key
- 做好监控告警
出问题通常不是 Redis 本身的锅,是用法有问题。建议熟读官方文档,理解原理。