错误处理、重连与安全退出

“能连上 Redis”只是开始,生产代码还需要处理断线、超时、重连和进程退出。

必须监听 error

client.on("error", (error: Error) => {
  console.error("Redis client error", {
    name: error.name,
    message: error.message,
  });
});

Node.js 的 EventEmitter 对未监听的 error 事件有特殊处理,可能导致进程退出。仅在 connect() 外层写 try/catch 并不能代替事件监听。

常用连接事件

事件适合记录状态变化,不要在每次事件里执行复杂业务逻辑。

重连策略

官方默认策略采用有上限的指数退避,并加入随机抖动。自定义示例:

const client = createClient({
  socket: {
    reconnectStrategy(retries: number, cause: Error): number | false {
      console.warn("Redis reconnect", { retries, message: cause.message });

      if (retries >= 10) {
        return false;
      }

      const delay: number = Math.min(2 ** retries * 50, 2_000);
      const jitter: number = Math.floor(Math.random() * 200);
      return delay + jitter;
    },
  },
});

为什么需要抖动?如果大量应用实例同时断线,完全相同的重连间隔会让它们同时冲击 Redis。

请求级错误处理

缓存失败不一定等于整个接口失败:

async function readOptionalCache(key: string): Promise<string | null> {
  try {
    return await client.get(key);
  } catch (error: unknown) {
    console.error("读取缓存失败", error);
    return null;
  }
}

但不要对所有 Redis 用法都吞错:

错误策略必须由数据角色决定。

超时

连接超时限制“多久建立连接”,命令超时限制“单条命令等待多久”。当前官方配置支持默认命令超时:

const client = createClient({
  socket: {
    connectTimeout: 5_000,
  },
  commandOptions: {
    timeout: 2_000,
  },
});

具体配置以项目安装版本的类型声明和官方文档为准。

优雅退出

let closing: boolean = false;

async function shutdown(signal: NodeJS.Signals): Promise<void> {
  if (closing) {
    return;
  }

  closing = true;
  console.log(`收到 ${signal},准备退出`);

  try {
    if (client.isOpen) {
      await client.close();
    }
  } catch (error: unknown) {
    console.error("Redis 关闭失败", error);
    client.destroy();
  }
}

process.once("SIGINT", () => {
  void shutdown("SIGINT");
});

process.once("SIGTERM", () => {
  void shutdown("SIGTERM");
});

真实 Express 服务还应先停止接收新请求,再等待正在执行的请求结束,最后关闭 Redis、数据库等资源。

可观测性建议

至少记录或监控:

日志中不要输出 Redis 密码、完整连接 URL 或敏感缓存值。