SCAN 与批量处理

为什么不要使用 KEYS *

KEYS 会一次遍历匹配键空间。在数据量大时,它可能长时间占用 Redis 主线程,并一次返回大量数据。

生产代码应使用增量迭代的 SCANSCAN 每次返回一部分键和下一次游标,不会要求一次处理全部数据。

Node-Redis 的异步迭代器

for await (const keys of client.scanIterator({
  MATCH: "nloop:user-cache:*",
  COUNT: 100,
})) {
  if (keys.length === 0) {
    continue;
  }

  const values: Array<string | null> = await client.mGet(keys);
  console.log(keys, values);
}

官方示例中的 scanIterator() 每次迭代得到一批键,而不是单个键。

COUNT 是服务端每轮工作的提示值,不保证每次准确返回相同数量。

其他数据类型的扫描

for await (const { field, value } of client.hScanIterator("nloop:user:42")) {
  console.log(field, value);
}

for await (const member of client.sScanIterator("nloop:tags")) {
  console.log(member);
}

for await (const { score, value } of client.zScanIterator("nloop:ranking")) {
  console.log(score, value);
}

它们分别对应 HSCANSSCANZSCAN

SCAN 的语义限制

扫描期间数据仍可能变化,因此:

SCAN 适合后台维护、诊断和渐进式迁移,不适合生成要求强一致的业务报表。

安全删除一类键

async function deleteByPattern(pattern: string): Promise<number> {
  let deleted: number = 0;

  for await (const keys of client.scanIterator({
    MATCH: pattern,
    COUNT: 100,
  })) {
    if (keys.length === 0) {
      continue;
    }

    deleted += await client.del(keys);
  }

  return deleted;
}

删除大键时,DEL 释放内存也可能产生明显停顿。Redis 支持 UNLINK 异步回收值占用的内存:

await client.unlink(keys);

是否使用 UNLINK 应结合服务端版本和运维要求。

keyPrefix 一起使用的陷阱

如果客户端配置了 keyPrefix: "nloop:"

批量维护任务通常使用不配置自动前缀的专用客户端更清晰。

批量任务最佳实践