🐛 fix(redis-scan): 修复大数据量下命名空间加载不完整问题

- 前后端 Redis SCAN 游标统一为字符串传递,避免 Number 精度丢失
- RedisScanKeys 增加 string/number 游标兼容解析,异常游标降级并告警
- 新增游标解析单测
- refs #135
This commit is contained in:
Syngnat
2026-02-28 12:32:22 +08:00
parent 439625a49c
commit 4de3f408c5
7 changed files with 167 additions and 15 deletions

View File

@@ -281,6 +281,23 @@ const getRedisScanLoadCount = (pattern: string, append: boolean): number => {
return append ? REDIS_KEY_SEARCH_LOAD_MORE_COUNT : REDIS_KEY_SEARCH_INITIAL_LOAD_COUNT;
};
const normalizeRedisCursor = (value: unknown): string => {
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed === '' ? '0' : trimmed;
}
if (typeof value === 'number') {
if (!Number.isFinite(value)) {
return '0';
}
return Math.trunc(value).toString();
}
if (typeof value === 'bigint') {
return value.toString();
}
return '0';
};
const normalizeKeySegment = (segment: string): string => {
return segment === '' ? EMPTY_SEGMENT_LABEL : segment;
};
@@ -384,7 +401,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
const [keys, setKeys] = useState<RedisKeyInfo[]>([]);
const [loading, setLoading] = useState(false);
const [searchPattern, setSearchPattern] = useState('*');
const [cursor, setCursor] = useState<number>(0);
const [cursor, setCursor] = useState<string>('0');
const [hasMore, setHasMore] = useState(false);
const [selectedKey, setSelectedKey] = useState<string | null>(null);
const [keyValue, setKeyValue] = useState<RedisValue | null>(null);
@@ -433,7 +450,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
const loadKeys = useCallback(async (
pattern: string = '*',
fromCursor: number = 0,
fromCursor: string = '0',
append: boolean = false,
targetCount?: number
) => {
@@ -454,7 +471,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
if (res.success) {
const result = res.data;
const scannedKeys = Array.isArray(result?.keys) ? result.keys : [];
const nextCursor = Number(result?.cursor || 0);
const nextCursor = normalizeRedisCursor(result?.cursor);
if (append) {
setKeys(prev => {
const keyMap = new Map<string, RedisKeyInfo>();
@@ -466,7 +483,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
setKeys(scannedKeys);
}
setCursor(nextCursor);
setHasMore(nextCursor !== 0);
setHasMore(nextCursor !== '0');
} else {
message.error('加载 Key 失败: ' + res.message);
}
@@ -483,14 +500,14 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
}, [getConfig]);
useEffect(() => {
loadKeys(searchPattern, 0, false, getRedisScanLoadCount(searchPattern, false));
loadKeys(searchPattern, '0', false, getRedisScanLoadCount(searchPattern, false));
}, [redisDB]);
const handleSearch = (value: string) => {
const pattern = value.trim() || '*';
setSearchPattern(pattern);
setCursor(0);
loadKeys(pattern, 0, false, getRedisScanLoadCount(pattern, false));
setCursor('0');
loadKeys(pattern, '0', false, getRedisScanLoadCount(pattern, false));
};
const handleLoadMore = () => {
@@ -501,8 +518,8 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
};
const handleRefresh = () => {
setCursor(0);
loadKeys(searchPattern, 0, false, getRedisScanLoadCount(searchPattern, false));
setCursor('0');
loadKeys(searchPattern, '0', false, getRedisScanLoadCount(searchPattern, false));
};
const loadKeyValue = async (key: string) => {

View File

@@ -137,7 +137,7 @@ export interface RedisKeyInfo {
export interface RedisScanResult {
keys: RedisKeyInfo[];
cursor: number;
cursor: string;
}
export interface RedisValue {

View File

@@ -124,7 +124,7 @@ export function RedisListSet(arg1:connection.ConnectionConfig,arg2:string,arg3:n
export function RedisRenameKey(arg1:connection.ConnectionConfig,arg2:string,arg3:string):Promise<connection.QueryResult>;
export function RedisScanKeys(arg1:connection.ConnectionConfig,arg2:string,arg3:number,arg4:number):Promise<connection.QueryResult>;
export function RedisScanKeys(arg1:connection.ConnectionConfig,arg2:string,arg3:string|number,arg4:number):Promise<connection.QueryResult>;
export function RedisSelectDB(arg1:connection.ConnectionConfig,arg2:number):Promise<connection.QueryResult>;