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>
This commit is contained in:
lgphone 2025-12-18 10:37:09 +08:00 committed by GitHub
parent 09b9fa517b
commit 342aca0b21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 75 additions and 13 deletions

View File

@ -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) => {

View File

@ -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;
};

View File

@ -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