常见业务实战模式
本章将前面的命令组合成工作中常见的方案。示例用于理解原理,生产实现还需加入日志、监控、超时和测试。
1. 验证码
async function saveCode(phone: string, code: string): Promise<void> {
await client.set(`nloop:verify:${phone}`, code, { EX: 300 });
}
async function verifyCode(phone: string, input: string): Promise<boolean> {
const key: string = `nloop:verify:${phone}`;
const saved: string | null = await client.get(key);
if (saved === null || saved !== input) {
return false;
}
await client.del(key);
return true;
}
比较与删除存在并发窗口。严格的一次性消费应使用 Lua 脚本原子完成。
此外应限制发送频率、校验次数,并避免把手机号原样写进日志。
2. 固定窗口限流
interface RateLimitResult {
allowed: boolean;
remaining: number;
}
async function rateLimit(ip: string): Promise<RateLimitResult> {
const windowSeconds: number = 60;
const limit: number = 100;
const window: number = Math.floor(Date.now() / 1000 / windowSeconds);
const key: string = `nloop:rate:${ip}:${window}`;
const count: number = await client.incr(key);
if (count === 1) {
await client.expire(key, windowSeconds + 1);
}
return {
allowed: count <= limit,
remaining: Math.max(0, limit - count),
};
}
INCR 和首次 EXPIRE 分成两个命令,进程可能在中间崩溃,留下无 TTL 的键。生产实现可用事务或 Lua 脚本保证两步原子完成。
固定窗口在边界处可能允许突发流量。更平滑的限流可使用滑动窗口、令牌桶或成熟库。
3. 幂等标记
async function beginRequest(requestId: string): Promise<boolean> {
const result: string | null = await client.set(
`nloop:idempotency:${requestId}`,
"processing",
{
NX: true,
EX: 300,
},
);
return result === "OK";
}
还要设计:处理成功后保存什么、失败是否删除、处理中超时怎么办,以及重复请求如何获得第一次的结果。
对订单创建等关键业务,数据库唯一约束通常仍是最终防线。
4. 排行榜
async function addScore(userId: number, score: number): Promise<void> {
await client.zIncrBy("nloop:leaderboard:weekly", score, `user:${userId}`);
}
async function getTopTen(): Promise<string[]> {
return client.zRange("nloop:leaderboard:weekly", 0, 9, {
REV: true,
});
}
每周榜可在键名中加入自然周,例如 leaderboard:2026-W32,并给历史榜单设置合理 TTL。
5. 缓存热点对象
async function getCachedProduct(id: number): Promise<Product | null> {
const key: string = `nloop:product:${id}`;
try {
const cached: string | null = await client.get(key);
if (cached !== null) {
return parseProduct(cached);
}
} catch (error: unknown) {
console.error("Redis cache read failed", error);
}
const product: Product | null = await findProductFromDatabase(id);
if (product === null) {
return null;
}
try {
await client.set(key, JSON.stringify(product), {
EX: 300 + Math.floor(Math.random() * 60),
});
} catch (error: unknown) {
console.error("Redis cache write failed", error);
}
return product;
}
这里将缓存视为可选优化,所以 Redis 失败时仍返回数据库结果。但数据库必须有保护措施,防止 Redis 故障导致所有流量无上限回源。
6. 简单延迟任务索引
Sorted Set 的分数可保存执行时间戳:
await client.zAdd("nloop:delayed-jobs", {
score: Date.now() + 60_000,
value: "job:123",
});
消费者读取到期成员并处理。但完整队列还需要解决并发领取、确认、重试、死信和任务内容持久化。不要只用 ZRANGE + ZREM 就宣称实现了可靠任务队列。
模式选择原则
- 能用单个原子命令就不要先读后写。
- 多步并发逻辑要检查原子性。
- 临时数据必须考虑 TTL。
- 关键业务仍要有数据库约束或可靠消息系统兜底。
- 先写清楚 Redis 故障时的业务行为,再写正常路径。