From 095b22951e71c4fffdcd01a9edd11d3846ce8455 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 27 Feb 2026 13:26:28 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(redis-viewer):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=A4=A7=E6=95=B0=E6=8D=AE=E9=87=8F=E5=9C=BA=E6=99=AF?= =?UTF-8?q?=20Key=20=E5=8A=A0=E8=BD=BD=E4=B8=8D=E5=AE=8C=E6=95=B4=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端 ScanKeys 改为按目标数量多轮聚合扫描,不再只依赖单轮返回结果 - 新增扫描目标数/步长/轮次上限,避免扫描过少或无限循环 - 前端首屏加载、搜索、刷新统一按较大批次请求,避免回退到几百条 - 加载更多改为按固定批次继续拉取并保持去重合并 - refs #129 --- frontend/src/components/RedisViewer.tsx | 34 ++++--- internal/redis/redis_impl.go | 124 ++++++++++++++++++------ 2 files changed, 117 insertions(+), 41 deletions(-) diff --git a/frontend/src/components/RedisViewer.tsx b/frontend/src/components/RedisViewer.tsx index cb102ed..d998950 100644 --- a/frontend/src/components/RedisViewer.tsx +++ b/frontend/src/components/RedisViewer.tsx @@ -14,6 +14,8 @@ const REDIS_TREE_KEY_TYPE_WIDTH = 92; const REDIS_TREE_KEY_TYPE_WIDTH_NARROW = 84; const REDIS_TREE_KEY_TTL_WIDTH = 92; const REDIS_TREE_HIDE_TTL_THRESHOLD = 460; +const REDIS_KEY_INITIAL_LOAD_COUNT = 2000; +const REDIS_KEY_LOAD_MORE_COUNT = 2000; interface RedisViewerProps { connectionId: string; @@ -462,27 +464,34 @@ const RedisViewer: React.FC = ({ connectionId, redisDB }) => { }; }, [connection, redisDB]); - const loadKeys = useCallback(async (pattern: string = '*', fromCursor: number = 0, append: boolean = false) => { + const loadKeys = useCallback(async ( + pattern: string = '*', + fromCursor: number = 0, + append: boolean = false, + targetCount: number = REDIS_KEY_INITIAL_LOAD_COUNT + ) => { const config = getConfig(); if (!config) return; setLoading(true); try { - const res = await (window as any).go.app.App.RedisScanKeys(config, pattern, fromCursor, 100); + const res = await (window as any).go.app.App.RedisScanKeys(config, pattern, fromCursor, targetCount); if (res.success) { const result = res.data; + const scannedKeys = Array.isArray(result?.keys) ? result.keys : []; + const nextCursor = Number(result?.cursor || 0); if (append) { setKeys(prev => { const keyMap = new Map(); prev.forEach(item => keyMap.set(item.key, item)); - result.keys.forEach((item: RedisKeyInfo) => keyMap.set(item.key, item)); + scannedKeys.forEach((item: RedisKeyInfo) => keyMap.set(item.key, item)); return Array.from(keyMap.values()); }); } else { - setKeys(result.keys); + setKeys(scannedKeys); } - setCursor(result.cursor); - setHasMore(result.cursor !== 0); + setCursor(nextCursor); + setHasMore(nextCursor !== 0); } else { message.error('加载 Key 失败: ' + res.message); } @@ -494,23 +503,26 @@ const RedisViewer: React.FC = ({ connectionId, redisDB }) => { }, [getConfig]); useEffect(() => { - loadKeys(searchPattern, 0, false); + loadKeys(searchPattern, 0, false, REDIS_KEY_INITIAL_LOAD_COUNT); }, [redisDB]); const handleSearch = (value: string) => { const pattern = value.trim() || '*'; setSearchPattern(pattern); setCursor(0); - loadKeys(pattern, 0, false); + loadKeys(pattern, 0, false, REDIS_KEY_INITIAL_LOAD_COUNT); }; const handleLoadMore = () => { - loadKeys(searchPattern, cursor, true); + if (!hasMore || loading) { + return; + } + loadKeys(searchPattern, cursor, true, REDIS_KEY_LOAD_MORE_COUNT); }; const handleRefresh = () => { setCursor(0); - loadKeys(searchPattern, 0, false); + loadKeys(searchPattern, 0, false, REDIS_KEY_INITIAL_LOAD_COUNT); }; const loadKeyValue = async (key: string) => { @@ -1777,7 +1789,7 @@ const RedisViewer: React.FC = ({ connectionId, redisDB }) => { {hasMore && (
- +
)} diff --git a/internal/redis/redis_impl.go b/internal/redis/redis_impl.go index bd32aea..03ee844 100644 --- a/internal/redis/redis_impl.go +++ b/internal/redis/redis_impl.go @@ -22,6 +22,14 @@ type RedisClientImpl struct { forwarder *ssh.LocalForwarder } +const ( + redisScanDefaultTargetCount int64 = 2000 + redisScanMaxTargetCount int64 = 10000 + redisScanMinStepCount int64 = 200 + redisScanMaxStepCount int64 = 2000 + redisScanMaxRounds = 64 +) + // NewRedisClient creates a new Redis client instance func NewRedisClient() RedisClient { return &RedisClientImpl{} @@ -108,21 +116,70 @@ func (r *RedisClientImpl) ScanKeys(pattern string, cursor uint64, count int64) ( if pattern == "" { pattern = "*" } + targetCount := normalizeRedisScanTargetCount(count) + scanStepCount := normalizeRedisScanStepCount(targetCount) + currentCursor := cursor + round := 0 + + keys := make([]string, 0, int(targetCount)) + seen := make(map[string]struct{}, int(targetCount)) + + for len(keys) < int(targetCount) { + batch, nextCursor, err := r.client.Scan(ctx, currentCursor, pattern, scanStepCount).Result() + if err != nil { + return nil, err + } + + for _, key := range batch { + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + keys = append(keys, key) + if len(keys) >= int(targetCount) { + break + } + } + + currentCursor = nextCursor + round++ + if currentCursor == 0 || round >= redisScanMaxRounds { + break + } + } + + return &RedisScanResult{ + Keys: r.loadRedisKeyInfos(ctx, keys), + Cursor: currentCursor, + }, nil +} + +func normalizeRedisScanTargetCount(count int64) int64 { if count <= 0 { - count = 100 + return redisScanDefaultTargetCount + } + if count > redisScanMaxTargetCount { + return redisScanMaxTargetCount + } + return count +} + +func normalizeRedisScanStepCount(targetCount int64) int64 { + if targetCount < redisScanMinStepCount { + return redisScanMinStepCount + } + if targetCount > redisScanMaxStepCount { + return redisScanMaxStepCount + } + return targetCount +} + +func (r *RedisClientImpl) loadRedisKeyInfos(ctx context.Context, keys []string) []RedisKeyInfo { + result := make([]RedisKeyInfo, 0, len(keys)) + if len(keys) == 0 { + return result } - keys, nextCursor, err := r.client.Scan(ctx, cursor, pattern, count).Result() - if err != nil { - return nil, err - } - - result := &RedisScanResult{ - Keys: make([]RedisKeyInfo, 0, len(keys)), - Cursor: nextCursor, - } - - // Get type and TTL for each key pipe := r.client.Pipeline() typeResults := make([]*redis.StatusCmd, len(keys)) ttlResults := make([]*redis.DurationCmd, len(keys)) @@ -132,37 +189,44 @@ func (r *RedisClientImpl) ScanKeys(pattern string, cursor uint64, count int64) ( ttlResults[i] = pipe.TTL(ctx, key) } - _, err = pipe.Exec(ctx) + _, err := pipe.Exec(ctx) if err != nil && err != redis.Nil { - // Fallback: get info one by one for _, key := range keys { - keyType, _ := r.GetKeyType(key) - ttl, _ := r.GetTTL(key) - result.Keys = append(result.Keys, RedisKeyInfo{ + keyType, typeErr := r.client.Type(ctx, key).Result() + if typeErr != nil && typeErr != redis.Nil { + keyType = "" + } + ttlValue, ttlErr := r.client.TTL(ctx, key).Result() + if ttlErr != nil && ttlErr != redis.Nil { + ttlValue = -2 + } + result = append(result, RedisKeyInfo{ Key: key, Type: keyType, - TTL: ttl, + TTL: toRedisTTLSeconds(ttlValue), }) } - return result, nil + return result } for i, key := range keys { - keyType := typeResults[i].Val() - ttl := int64(ttlResults[i].Val().Seconds()) - if ttlResults[i].Val() == -1 { - ttl = -1 - } else if ttlResults[i].Val() == -2 { - ttl = -2 - } - result.Keys = append(result.Keys, RedisKeyInfo{ + result = append(result, RedisKeyInfo{ Key: key, - Type: keyType, - TTL: ttl, + Type: typeResults[i].Val(), + TTL: toRedisTTLSeconds(ttlResults[i].Val()), }) } + return result +} - return result, nil +func toRedisTTLSeconds(ttl time.Duration) int64 { + if ttl == -1 { + return -1 + } + if ttl == -2 { + return -2 + } + return int64(ttl.Seconds()) } // GetKeyType returns the type of a key