From 342aca0b2139e51c5367d4832356eaaaa1049aed Mon Sep 17 00:00:00 2001 From: lgphone Date: Thu, 18 Dec 2025 10:37:09 +0800 Subject: [PATCH] perf: replace redis KEYS with SCAN (#6101) * perf: replace redis KEYS with SCAN * test: add redis scan mock to fix unit tests * Fix formatting in redis.ts mock functions * fix comment word Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/service/common/cache/index.ts | 49 ++++++++++++++++++++++---- packages/service/common/redis/index.ts | 29 ++++++++++++--- test/mocks/common/redis.ts | 10 +++++- 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/packages/service/common/cache/index.ts b/packages/service/common/cache/index.ts index fb66539bb..562b7d947 100644 --- a/packages/service/common/cache/index.ts +++ b/packages/service/common/cache/index.ts @@ -12,21 +12,56 @@ const cachePrefix = `VERSION_KEY:`; * @param key SystemCacheKeyEnum * @param id string (teamId, tmbId, etc), if '*' is used, all keys will be refreshed */ -export const refreshVersionKey = async (key: `${SystemCacheKeyEnum}`, id?: string | '*') => { +export const refreshVersionKey = async ( + key: `${SystemCacheKeyEnum}`, + id?: string | '*' +) => { const redis = getGlobalRedisConnection(); if (!global.systemCache) initCache(); const val = randomUUID(); const versionKey = id ? `${cachePrefix}${key}:${id}` : `${cachePrefix}${key}`; + if (id === '*') { const pattern = `${cachePrefix}${key}:*`; - const keys = await redis.keys(pattern); - if (keys.length > 0) { - await redis.del(keys); - } - } else { - await redis.set(versionKey, val); + + let cursor = '0'; + const batchSize = 1000; // SCAN 每次取多少 + const delChunk = 500; // 每次 pipeline 删除多少(可按需调) + + let buffer: string[] = []; + + const flush = async () => { + if (buffer.length === 0) return; + const pipeline = redis.pipeline(); + for (const k of buffer) pipeline.del(k); + await pipeline.exec(); + buffer = []; + }; + + do { + const [nextCursor, keys] = await redis.scan( + cursor, + 'MATCH', + pattern, + 'COUNT', + batchSize + ); + cursor = nextCursor; + + for (const k of keys) { + buffer.push(k); + if (buffer.length >= delChunk) { + await flush(); + } + } + } while (cursor !== '0'); + + await flush(); + return; } + + await redis.set(versionKey, val); }; export const getVersionKey = async (key: `${SystemCacheKeyEnum}`, id?: string) => { diff --git a/packages/service/common/redis/index.ts b/packages/service/common/redis/index.ts index 55f8126ba..efbcbeb24 100644 --- a/packages/service/common/redis/index.ts +++ b/packages/service/common/redis/index.ts @@ -45,8 +45,27 @@ export const getGlobalRedisConnection = () => { export const getAllKeysByPrefix = async (key: string) => { const redis = getGlobalRedisConnection(); - const keys = (await redis.keys(`${FASTGPT_REDIS_PREFIX}${key}:*`)).map((key) => - key.replace(FASTGPT_REDIS_PREFIX, '') - ); - return keys; -}; + const prefix = FASTGPT_REDIS_PREFIX; + const pattern = `${prefix}${key}:*`; + + let cursor = '0'; + const batchSize = 1000; // SCAN 每次取多少 + const results: string[] = []; + + do { + const [nextCursor, keys] = await redis.scan( + cursor, + 'MATCH', + pattern, + 'COUNT', + batchSize + ); + cursor = nextCursor; + + for (const k of keys) { + results.push(k.startsWith(prefix) ? k.slice(prefix.length) : k); + } + } while (cursor !== '0'); + + return results; +}; \ No newline at end of file diff --git a/test/mocks/common/redis.ts b/test/mocks/common/redis.ts index 8d1d3dccd..6acfd039e 100644 --- a/test/mocks/common/redis.ts +++ b/test/mocks/common/redis.ts @@ -17,6 +17,7 @@ const createMockRedisClient = () => ({ del: vi.fn().mockResolvedValue(1), exists: vi.fn().mockResolvedValue(0), keys: vi.fn().mockResolvedValue([]), + scan: vi.fn().mockResolvedValue(['0', []]), // Hash operations hget: vi.fn().mockResolvedValue(null), @@ -53,7 +54,14 @@ const createMockRedisClient = () => ({ sadd: vi.fn().mockResolvedValue(1), srem: vi.fn().mockResolvedValue(1), smembers: vi.fn().mockResolvedValue([]), - sismember: vi.fn().mockResolvedValue(0) + sismember: vi.fn().mockResolvedValue(0), + + // pipeline + pipeline: vi.fn(() => ({ + del: vi.fn().mockReturnThis(), + unlink: vi.fn().mockReturnThis(), + exec: vi.fn().mockResolvedValue([]) + })) }); // Mock Redis connections to prevent connection errors in tests