mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-12 16:02:48 +08:00
🐛 fix(redis): 修复超过16个数据库无法展示
- 后端改为通过 CONFIG GET databases 动态获取 Redis 数据库数量 - 放宽单机和 Sentinel 模式的 RedisDB 索引限制,支持 db16 及以上 - 前端连接配置和持久化不再裁剪高编号 Redis 数据库 - 连接测试成功后按服务端返回的真实数据库列表展示可选 DB - 增加 Redis db31 展示、切换、保存和 URI 解析回归测试 Refs #558
This commit is contained in:
@@ -28,7 +28,7 @@ func normalizeRunConfig(config connection.ConnectionConfig, dbName string) conne
|
||||
runConfig.Database = name
|
||||
case "redis":
|
||||
runConfig.Database = name
|
||||
if idx, err := strconv.Atoi(name); err == nil && idx >= 0 && idx <= 15 {
|
||||
if idx, err := strconv.Atoi(name); err == nil && idx >= 0 {
|
||||
runConfig.RedisDB = idx
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -229,6 +229,19 @@ func TestNormalizeRunConfig_IRISUsesNamespaceFromTree(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRunConfig_RedisAllowsDatabaseIndexAboveDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
runConfig := normalizeRunConfig(connection.ConnectionConfig{
|
||||
Type: "redis",
|
||||
RedisDB: 0,
|
||||
}, "31")
|
||||
|
||||
if runConfig.Database != "31" || runConfig.RedisDB != 31 {
|
||||
t.Fatalf("expected Redis db31 from tree, got database=%q redisDB=%d", runConfig.Database, runConfig.RedisDB)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSchemaAndTable_IRISDoesNotTreatNamespaceAsSchema(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ type RedisValue struct {
|
||||
|
||||
// RedisDBInfo represents information about a Redis database
|
||||
type RedisDBInfo struct {
|
||||
Index int `json:"index"` // Database index (single: 0-15, cluster: logical 0-15)
|
||||
Index int `json:"index"` // Database index (single/sentinel: server configured range, cluster: logical 0-15)
|
||||
Keys int64 `json:"keys"` // Number of keys in this database
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ type RedisClientImpl struct {
|
||||
}
|
||||
|
||||
const (
|
||||
redisDefaultDatabaseCount = 16
|
||||
redisClusterLogicalDBCount = 16
|
||||
redisScanDefaultTargetCount int64 = 2000
|
||||
redisScanMaxTargetCount int64 = 10000
|
||||
redisScanMinStepCount int64 = 200
|
||||
@@ -263,10 +265,9 @@ func (r *RedisClientImpl) Connect(config connection.ConnectionConfig) error {
|
||||
config.Password = sanitizeRedisPassword(config.Password)
|
||||
config.RedisSentinelPassword = sanitizeRedisPassword(config.RedisSentinelPassword)
|
||||
r.config = config
|
||||
if r.config.RedisDB < 0 || r.config.RedisDB > 15 {
|
||||
if r.config.RedisDB < 0 {
|
||||
r.config.RedisDB = 0
|
||||
}
|
||||
r.currentDB = r.config.RedisDB
|
||||
r.forwarder = nil
|
||||
r.client = nil
|
||||
r.singleClient = nil
|
||||
@@ -282,6 +283,10 @@ func (r *RedisClientImpl) Connect(config connection.ConnectionConfig) error {
|
||||
topology := strings.ToLower(strings.TrimSpace(config.Topology))
|
||||
isSentinel := topology == "sentinel"
|
||||
r.isCluster = !isSentinel && (topology == "cluster" || len(seedAddrs) > 1)
|
||||
if r.isCluster && r.config.RedisDB >= redisClusterLogicalDBCount {
|
||||
r.config.RedisDB = 0
|
||||
}
|
||||
r.currentDB = r.config.RedisDB
|
||||
|
||||
if (r.isCluster || isSentinel) && config.UseSSH {
|
||||
return fmt.Errorf("Redis %s模式暂不支持 SSH 隧道,请关闭 SSH 后重试", redisTopologyDisplayName(topology))
|
||||
@@ -1349,8 +1354,8 @@ func (r *RedisClientImpl) ExecuteCommand(args []string) (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效数据库索引: %s", args[1])
|
||||
}
|
||||
if index < 0 || index > 15 {
|
||||
return nil, fmt.Errorf("数据库索引必须在 0-15 之间")
|
||||
if index < 0 || index >= redisClusterLogicalDBCount {
|
||||
return nil, fmt.Errorf("数据库索引必须在 0-%d 之间", redisClusterLogicalDBCount-1)
|
||||
}
|
||||
r.currentDB = index
|
||||
r.config.RedisDB = index
|
||||
@@ -1496,6 +1501,67 @@ func (r *RedisClientImpl) GetServerInfo() (map[string]string, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseRedisKeyspaceDatabaseKeys(info string) map[int]int64 {
|
||||
dbMap := make(map[int]int64)
|
||||
lines := strings.Split(info, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "db") {
|
||||
// Format: db0:keys=123,expires=0,avg_ttl=0
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
dbIndex, err := strconv.Atoi(strings.TrimPrefix(parts[0], "db"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
kvPairs := strings.Split(parts[1], ",")
|
||||
for _, kv := range kvPairs {
|
||||
if strings.HasPrefix(kv, "keys=") {
|
||||
keys, _ := strconv.ParseInt(strings.TrimPrefix(kv, "keys="), 10, 64)
|
||||
dbMap[dbIndex] = keys
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dbMap
|
||||
}
|
||||
|
||||
func parseRedisConfiguredDatabaseCount(config map[string]string) (int, bool) {
|
||||
for key, value := range config {
|
||||
if !strings.EqualFold(strings.TrimSpace(key), "databases") {
|
||||
continue
|
||||
}
|
||||
count, err := strconv.Atoi(strings.TrimSpace(value))
|
||||
if err == nil && count > 0 {
|
||||
return count, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (r *RedisClientImpl) resolveRedisDatabaseCount(ctx context.Context, dbMap map[int]int64) int {
|
||||
count := redisDefaultDatabaseCount
|
||||
if r.currentDB >= count {
|
||||
count = r.currentDB + 1
|
||||
}
|
||||
for index := range dbMap {
|
||||
if index >= count {
|
||||
count = index + 1
|
||||
}
|
||||
}
|
||||
config, err := r.client.ConfigGet(ctx, "databases").Result()
|
||||
if err != nil {
|
||||
return count
|
||||
}
|
||||
if configured, ok := parseRedisConfiguredDatabaseCount(config); ok && configured > count {
|
||||
count = configured
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// GetDatabases returns information about all databases
|
||||
func (r *RedisClientImpl) GetDatabases() ([]RedisDBInfo, error) {
|
||||
if r.client == nil {
|
||||
@@ -1521,8 +1587,8 @@ func (r *RedisClientImpl) GetDatabases() ([]RedisDBInfo, error) {
|
||||
logger.Warnf("Redis 集群获取 key 数量失败,回退为 0: %v", err)
|
||||
totalKeys = 0
|
||||
}
|
||||
result := make([]RedisDBInfo, 16)
|
||||
for i := 0; i < 16; i++ {
|
||||
result := make([]RedisDBInfo, redisClusterLogicalDBCount)
|
||||
for i := 0; i < redisClusterLogicalDBCount; i++ {
|
||||
result[i] = RedisDBInfo{Index: i, Keys: 0}
|
||||
}
|
||||
result[0].Keys = totalKeys
|
||||
@@ -1535,36 +1601,10 @@ func (r *RedisClientImpl) GetDatabases() ([]RedisDBInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse keyspace info
|
||||
dbMap := make(map[int]int64)
|
||||
lines := strings.Split(info, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "db") {
|
||||
// Format: db0:keys=123,expires=0,avg_ttl=0
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
dbIndex, err := strconv.Atoi(strings.TrimPrefix(parts[0], "db"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Parse keys count
|
||||
kvPairs := strings.Split(parts[1], ",")
|
||||
for _, kv := range kvPairs {
|
||||
if strings.HasPrefix(kv, "keys=") {
|
||||
keys, _ := strconv.ParseInt(strings.TrimPrefix(kv, "keys="), 10, 64)
|
||||
dbMap[dbIndex] = keys
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return all 16 databases (0-15)
|
||||
result := make([]RedisDBInfo, 16)
|
||||
for i := 0; i < 16; i++ {
|
||||
dbMap := parseRedisKeyspaceDatabaseKeys(info)
|
||||
databaseCount := r.resolveRedisDatabaseCount(ctx, dbMap)
|
||||
result := make([]RedisDBInfo, databaseCount)
|
||||
for i := 0; i < databaseCount; i++ {
|
||||
result[i] = RedisDBInfo{
|
||||
Index: i,
|
||||
Keys: dbMap[i], // Will be 0 if not in map
|
||||
@@ -1581,16 +1621,16 @@ func (r *RedisClientImpl) SelectDB(index int) error {
|
||||
}
|
||||
|
||||
if r.isCluster {
|
||||
if index < 0 || index > 15 {
|
||||
return fmt.Errorf("数据库索引必须在 0-15 之间")
|
||||
if index < 0 || index >= redisClusterLogicalDBCount {
|
||||
return fmt.Errorf("数据库索引必须在 0-%d 之间", redisClusterLogicalDBCount-1)
|
||||
}
|
||||
r.currentDB = index
|
||||
r.config.RedisDB = index
|
||||
return nil
|
||||
}
|
||||
|
||||
if index < 0 || index > 15 {
|
||||
return fmt.Errorf("数据库索引必须在 0-15 之间")
|
||||
if index < 0 {
|
||||
return fmt.Errorf("数据库索引必须大于等于 0")
|
||||
}
|
||||
|
||||
nextConfig := r.config
|
||||
|
||||
@@ -2,17 +2,101 @@ package redis
|
||||
|
||||
import (
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
"net"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
goredis "github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func startRedisProtocolTestServer(t *testing.T, handler func([]string) string) string {
|
||||
t.Helper()
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen redis test server failed: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = listener.Close()
|
||||
})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
reader := bufio.NewReader(conn)
|
||||
for {
|
||||
args, err := readRedisProtocolArray(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
response := handler(args)
|
||||
if response == "" {
|
||||
response = "+OK\r\n"
|
||||
}
|
||||
if _, err := conn.Write([]byte(response)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
return listener.Addr().String()
|
||||
}
|
||||
|
||||
func readRedisProtocolArray(reader *bufio.Reader) ([]string, error) {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, "*") {
|
||||
return nil, fmt.Errorf("expected array, got %q", line)
|
||||
}
|
||||
count, err := strconv.Atoi(strings.TrimPrefix(line, "*"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args := make([]string, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
bulkHeader, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bulkHeader = strings.TrimSpace(bulkHeader)
|
||||
if !strings.HasPrefix(bulkHeader, "$") {
|
||||
return nil, fmt.Errorf("expected bulk string, got %q", bulkHeader)
|
||||
}
|
||||
size, err := strconv.Atoi(strings.TrimPrefix(bulkHeader, "$"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := make([]byte, size+2)
|
||||
if _, err := io.ReadFull(reader, buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = append(args, string(buf[:size]))
|
||||
}
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func redisBulkString(value string) string {
|
||||
return fmt.Sprintf("$%d\r\n%s\r\n", len(value), value)
|
||||
}
|
||||
|
||||
// 回归保护:HGETALL 在 RESP3 下返回 map[interface{}]interface{}(go-redis v9 默认 RESP3),
|
||||
// 这种类型 encoding/json 无法序列化,原值穿透到 Wails RPC 会让 Windows 进程退出(用户感知闪退)。
|
||||
// formatCommandResult 必须把 map 平展成交错 [k1, v1, k2, v2, ...] array,前端按 array 渲染。
|
||||
@@ -277,6 +361,47 @@ func TestRedisClusterKeepsSSHValidation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisGetDatabasesUsesConfiguredDatabaseCountAboveDefault(t *testing.T) {
|
||||
keyspaceInfo := "# Keyspace\r\ndb0:keys=1,expires=0,avg_ttl=0\r\ndb31:keys=2,expires=0,avg_ttl=0\r\n"
|
||||
addr := startRedisProtocolTestServer(t, func(args []string) string {
|
||||
command := strings.ToUpper(strings.TrimSpace(args[0]))
|
||||
switch command {
|
||||
case "HELLO":
|
||||
return "-ERR unknown command 'HELLO'\r\n"
|
||||
case "CLIENT":
|
||||
return "-ERR unknown subcommand\r\n"
|
||||
case "CONFIG":
|
||||
if len(args) >= 3 && strings.EqualFold(args[1], "GET") && strings.EqualFold(args[2], "databases") {
|
||||
return "*2\r\n$9\r\ndatabases\r\n$2\r\n32\r\n"
|
||||
}
|
||||
case "INFO":
|
||||
return redisBulkString(keyspaceInfo)
|
||||
}
|
||||
return "+OK\r\n"
|
||||
})
|
||||
|
||||
rawClient := goredis.NewClient(&goredis.Options{
|
||||
Addr: addr,
|
||||
Protocol: 2,
|
||||
})
|
||||
client := &RedisClientImpl{
|
||||
client: rawClient,
|
||||
singleClient: rawClient,
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
dbs, err := client.GetDatabases()
|
||||
if err != nil {
|
||||
t.Fatalf("GetDatabases returned error: %v", err)
|
||||
}
|
||||
if len(dbs) != 32 {
|
||||
t.Fatalf("expected 32 redis databases, got %d (%#v)", len(dbs), dbs)
|
||||
}
|
||||
if dbs[31].Index != 31 || dbs[31].Keys != 2 {
|
||||
t.Fatalf("expected db31 with 2 keys, got %#v", dbs[31])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisSelectDBReconnectsWithSentinelConfig(t *testing.T) {
|
||||
oldConnect := redisDBSwitchConnect
|
||||
defer func() {
|
||||
@@ -317,12 +442,12 @@ func TestRedisSelectDBReconnectsWithSentinelConfig(t *testing.T) {
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if err := client.SelectDB(6); err != nil {
|
||||
if err := client.SelectDB(31); err != nil {
|
||||
t.Fatalf("SelectDB returned error: %v", err)
|
||||
}
|
||||
|
||||
if captured.RedisDB != 6 || client.currentDB != 6 {
|
||||
t.Fatalf("expected RedisDB/currentDB=6, captured=%d current=%d", captured.RedisDB, client.currentDB)
|
||||
if captured.RedisDB != 31 || client.currentDB != 31 {
|
||||
t.Fatalf("expected RedisDB/currentDB=31, captured=%d current=%d", captured.RedisDB, client.currentDB)
|
||||
}
|
||||
if captured.Topology != "sentinel" {
|
||||
t.Fatalf("expected sentinel topology to be preserved, got %q", captured.Topology)
|
||||
|
||||
Reference in New Issue
Block a user