常见业务实战模式

本章将前面的命令组合成工作中常见的方案。示例用于理解原理,生产实现还需加入日志、监控、超时和测试。

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 就宣称实现了可靠任务队列。

模式选择原则