Files
opsxdb/server/internal/db/redis.go
T

1176 lines
33 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package db
import (
"context"
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/redis/go-redis/v9"
)
func redisDBIndex(c ConnReq) int {
db := 0
if c.Database != "" {
if n, err := strconv.Atoi(c.Database); err == nil {
db = n
}
}
return db
}
func normalizeRedisMode(c ConnReq) string {
m := strings.ToLower(strings.TrimSpace(c.RedisMode))
if m == "" {
return "standalone"
}
return m
}
// parseRedisAddrs 解析 host:port 列表(换行 / 逗号 / 分号 / 空白分隔)。
func parseRedisAddrs(raw string) []string {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
raw = strings.ReplaceAll(raw, "\r\n", "\n")
raw = strings.ReplaceAll(raw, "\r", "\n")
raw = strings.ReplaceAll(raw, ",", " ")
raw = strings.ReplaceAll(raw, ";", " ")
raw = strings.ReplaceAll(raw, "\n", " ")
parts := strings.Fields(raw)
out := make([]string, 0, len(parts))
seen := map[string]struct{}{}
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
if _, ok := seen[p]; ok {
continue
}
seen[p] = struct{}{}
out = append(out, p)
}
return out
}
func redisStandaloneOpts(c ConnReq) (*redis.Options, error) {
host := c.Host
if host == "" {
host = "127.0.0.1"
}
port := c.Port
if port == 0 {
port = 6379
}
opts := &redis.Options{
Addr: fmt.Sprintf("%s:%d", host, port),
Password: c.Password,
Username: c.User,
DB: redisDBIndex(c),
DialTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
if SSLEnabled(c) {
tlsCfg, err := tlsConfigFrom(c)
if err != nil {
return nil, err
}
opts.TLSConfig = tlsCfg
}
return opts, nil
}
// openRedis 按 redisMode 建 UniversalClient(单机 / Sentinel / Cluster)。
func openRedis(c ConnReq) (redis.UniversalClient, error) {
mode := normalizeRedisMode(c)
switch mode {
case "sentinel":
addrs := parseRedisAddrs(c.RedisNodes)
if len(addrs) == 0 {
return nil, fmt.Errorf("Sentinel 模式请填写至少一个 Sentinel 节点(host:port)")
}
master := strings.TrimSpace(c.MasterName)
if master == "" {
return nil, fmt.Errorf("Sentinel 模式请填写主节点名 masterName")
}
opts := &redis.FailoverOptions{
MasterName: master,
SentinelAddrs: addrs,
Password: c.Password,
Username: c.User,
SentinelPassword: c.SentinelPassword,
DB: redisDBIndex(c),
DialTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
if SSLEnabled(c) {
tlsCfg, err := tlsConfigFrom(c)
if err != nil {
return nil, err
}
opts.TLSConfig = tlsCfg
}
return redis.NewFailoverClient(opts), nil
case "cluster":
addrs := parseRedisAddrs(c.RedisNodes)
if len(addrs) == 0 {
return nil, fmt.Errorf("Cluster 模式请填写至少一个种子节点(host:port)")
}
opts := &redis.ClusterOptions{
Addrs: addrs,
Password: c.Password,
Username: c.User,
DialTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
if SSLEnabled(c) {
tlsCfg, err := tlsConfigFrom(c)
if err != nil {
return nil, err
}
opts.TLSConfig = tlsCfg
}
return redis.NewClusterClient(opts), nil
default:
opts, err := redisStandaloneOpts(c)
if err != nil {
return nil, err
}
return redis.NewClient(opts), nil
}
}
// redisClientFor:会话请求复用会话级单例,非会话请求新建并随请求关闭。
func redisClientFor(c ConnReq) (cli redis.UniversalClient, done func(), err error) {
if c.SessionID != "" {
s, err := GetSession(c.SessionID)
if err != nil {
return nil, nil, err
}
// 请求携带的 Database 决定用哪个 DB 的客户端(对象树切库时逐请求变化)
cli, err := s.redisClientFor(c)
if err != nil {
return nil, nil, err
}
return cli, func() {}, nil
}
cli, err = openRedis(c)
if err != nil {
return nil, nil, err
}
return cli, func() { _ = cli.Close() }, nil
}
func reqCtxRedis(parent context.Context, d time.Duration) (context.Context, context.CancelFunc) {
if parent == nil {
parent = context.Background()
}
return context.WithTimeout(parent, d)
}
func testRedis(c ConnReq) error {
cli, err := openRedis(c)
if err != nil {
return err
}
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return cli.Ping(ctx).Err()
}
func listDatabasesRedis(parent context.Context, c ConnReq) ([]string, error) {
// INFO keyspace 可拿已用 DB;统一暴露 0-15 便于切换,先 Ping 探活
cli, done, err := redisClientFor(c)
if err != nil {
return nil, err
}
defer done()
ctx, cancel := reqCtxRedis(parent, 5*time.Second)
defer cancel()
if err := cli.Ping(ctx).Err(); err != nil {
return nil, err
}
// Cluster 不支持 SELECT 多库,只暴露 0
if normalizeRedisMode(c) == "cluster" {
return []string{"0"}, nil
}
out := make([]string, 16)
for i := 0; i < 16; i++ {
out[i] = strconv.Itoa(i)
}
return out, nil
}
func listTablesRedis(parent context.Context, c ConnReq) ([]string, error) {
cli, done, err := redisClientFor(c)
if err != nil {
return nil, err
}
defer done()
ctx, cancel := reqCtxRedis(parent, 8*time.Second)
defer cancel()
var keys []string
var cursor uint64
for {
batch, next, err := cli.Scan(ctx, cursor, "*", 200).Result()
if err != nil {
return nil, err
}
keys = append(keys, batch...)
cursor = next
if cursor == 0 || len(keys) >= 500 {
break
}
}
if keys == nil {
keys = []string{}
}
return keys, nil
}
// redisDangerCmds 需要 confirmDanger 确认后才执行的命令组:
// FLUSHALL/FLUSHDB/CONFIG 破坏性;KEYS 大库阻塞。
var redisDangerCmds = map[string]bool{
"FLUSHALL": true,
"FLUSHDB": true,
"CONFIG": true,
"KEYS": true,
}
func redisOK(start time.Time) ExecResult {
return ExecResult{
Columns: []string{"affected_rows"},
Rows: []map[string]interface{}{{"affected_rows": 1}},
RowCount: 1,
TimeMs: time.Since(start).Milliseconds(),
}
}
func redisAffected(start time.Time, n int64) ExecResult {
return ExecResult{
Columns: []string{"affected_rows"},
Rows: []map[string]interface{}{{"affected_rows": n}},
RowCount: 1,
TimeMs: time.Since(start).Milliseconds(),
}
}
func redisErr(start time.Time, err error) ExecResult {
return ExecResult{Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()}
}
func redisUsage(start time.Time, usage string) ExecResult {
return ExecResult{Error: usage, TimeMs: time.Since(start).Milliseconds()}
}
func execRedis(parent context.Context, c ConnReq) ExecResult {
start := time.Now()
sql := strings.TrimSpace(c.SQL)
if sql == "" {
return ExecResult{Error: "empty sql", TimeMs: 0}
}
fields := splitRedisCmd(sql)
if len(fields) == 0 {
return ExecResult{Error: "empty command", TimeMs: time.Since(start).Milliseconds()}
}
cmd := strings.ToUpper(fields[0])
args := fields[1:]
// 危险命令二段确认:未确认不执行
if redisDangerCmds[cmd] && !c.ConfirmDanger {
return ExecResult{
Error: "检测到危险命令: " + cmd,
NeedConfirm: true,
TimeMs: time.Since(start).Milliseconds(),
}
}
cli, done, err := redisClientFor(c)
if err != nil {
return redisErr(start, err)
}
defer done()
ctx, cancel := reqCtxRedis(parent, 10*time.Second)
defer cancel()
switch cmd {
case "PING":
val, err := cli.Ping(ctx).Result()
if err != nil {
return redisErr(start, err)
}
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": val}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()}
case "GET":
if len(args) < 1 {
return redisUsage(start, "GET key")
}
val, err := cli.Get(ctx, args[0]).Result()
if err == redis.Nil {
return ExecResult{Columns: []string{"key", "value"}, Rows: []map[string]interface{}{}, RowCount: 0, TimeMs: time.Since(start).Milliseconds()}
}
if err != nil {
return redisErr(start, err)
}
return ExecResult{Columns: []string{"key", "value"}, Rows: []map[string]interface{}{{"key": args[0], "value": val}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()}
case "SET":
// 参数已按 shell 词法切分,value 即 args[1](不再 Join 破坏引号分组);
// 支持可选 EX 秒数:SET key value [EX n]
if len(args) < 2 {
return redisUsage(start, "SET key value [EX seconds]")
}
exp := time.Duration(0)
if len(args) >= 4 && strings.ToUpper(args[2]) == "EX" {
n, err := strconv.Atoi(args[3])
if err != nil || n <= 0 {
return redisUsage(start, "SET key value EX seconds(秒数需为正整数)")
}
exp = time.Duration(n) * time.Second
}
if err := cli.Set(ctx, args[0], args[1], exp).Err(); err != nil {
return redisErr(start, err)
}
return redisOK(start)
case "DEL":
if len(args) < 1 {
return redisUsage(start, "DEL key [key...]")
}
n, err := cli.Del(ctx, args...).Result()
if err != nil {
return redisErr(start, err)
}
return redisAffected(start, n)
case "KEYS":
// 危险组已在上面对 confirmDanger 把关,此处直接执行
pat := "*"
if len(args) > 0 {
pat = args[0]
}
keys, err := cli.Keys(ctx, pat).Result()
if err != nil {
return redisErr(start, err)
}
rows := make([]map[string]interface{}, 0, len(keys))
for _, k := range keys {
rows = append(rows, map[string]interface{}{"key": k})
}
return ExecResult{Columns: []string{"key"}, Rows: rows, RowCount: len(rows), TimeMs: time.Since(start).Milliseconds()}
case "FLUSHALL", "FLUSHDB":
// 危险组,已经过 confirmDanger 把关
var err error
if cmd == "FLUSHALL" {
err = cli.FlushAll(ctx).Err()
} else {
err = cli.FlushDB(ctx).Err()
}
if err != nil {
return redisErr(start, err)
}
return redisOK(start)
case "CONFIG":
// 危险组,已经过 confirmDanger 把关;通用透传(CONFIG GET/SET ...)
if len(args) < 1 {
return redisUsage(start, "CONFIG GET|SET parameter [value]")
}
doArgs := append([]interface{}{"CONFIG"}, strsToAnys(args)...)
val, err := cli.Do(ctx, doArgs...).Result()
if err != nil {
return redisErr(start, err)
}
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": normalize(fmt.Sprintf("%v", val))}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()}
case "SCAN":
pat := "*"
count := int64(200)
i := 0
if len(args) > 0 {
// SCAN cursor [MATCH pattern] [COUNT n] —— 忽略 cursor,内部自扫
i = 1
}
for i < len(args) {
up := strings.ToUpper(args[i])
if up == "MATCH" && i+1 < len(args) {
pat = args[i+1]
i += 2
continue
}
if up == "COUNT" && i+1 < len(args) {
if n, err := strconv.ParseInt(args[i+1], 10, 64); err == nil && n > 0 {
count = n
}
i += 2
continue
}
i++
}
names, truncated, err := scanAll(ctx, cli, pat, count, redisKeyListCap)
if err != nil {
return redisErr(start, err)
}
if names == nil {
names = []string{}
}
rows := make([]map[string]interface{}, 0, len(names))
for _, k := range names {
rows = append(rows, map[string]interface{}{"key": k})
}
return ExecResult{Columns: []string{"key"}, Rows: rows, RowCount: len(rows), TimeMs: time.Since(start).Milliseconds(), Truncated: truncated}
case "EXPIRE":
if len(args) < 2 {
return redisUsage(start, "EXPIRE key seconds")
}
sec, err := strconv.Atoi(args[1])
if err != nil {
return redisUsage(start, "EXPIRE key seconds(秒数需为整数)")
}
ok, err := cli.Expire(ctx, args[0], time.Duration(sec)*time.Second).Result()
if err != nil {
return redisErr(start, err)
}
if !ok {
return redisAffected(start, 0)
}
return redisAffected(start, 1)
case "PERSIST":
if len(args) < 1 {
return redisUsage(start, "PERSIST key")
}
ok, err := cli.Persist(ctx, args[0]).Result()
if err != nil {
return redisErr(start, err)
}
if !ok {
return redisAffected(start, 0)
}
return redisAffected(start, 1)
case "HSET":
if len(args) < 3 || len(args)%2 == 0 {
return redisUsage(start, "HSET key field value [field value...]")
}
n, err := cli.HSet(ctx, args[0], strsToAnys(args[1:])...).Result()
if err != nil {
return redisErr(start, err)
}
return redisAffected(start, n)
case "HDEL":
if len(args) < 2 {
return redisUsage(start, "HDEL key field [field...]")
}
n, err := cli.HDel(ctx, args[0], args[1:]...).Result()
if err != nil {
return redisErr(start, err)
}
return redisAffected(start, n)
case "SADD":
if len(args) < 2 {
return redisUsage(start, "SADD key member [member...]")
}
n, err := cli.SAdd(ctx, args[0], strsToAnys(args[1:])...).Result()
if err != nil {
return redisErr(start, err)
}
return redisAffected(start, n)
case "SREM":
if len(args) < 2 {
return redisUsage(start, "SREM key member [member...]")
}
n, err := cli.SRem(ctx, args[0], strsToAnys(args[1:])...).Result()
if err != nil {
return redisErr(start, err)
}
return redisAffected(start, n)
case "ZADD":
// ZADD key score member [score member...]
if len(args) < 3 || len(args)%2 == 0 {
return redisUsage(start, "ZADD key score member [score member...]")
}
var zs []redis.Z
for i := 1; i+1 < len(args); i += 2 {
score, err := strconv.ParseFloat(args[i], 64)
if err != nil {
return redisUsage(start, "ZADD score 需为数字: "+args[i])
}
zs = append(zs, redis.Z{Score: score, Member: args[i+1]})
}
n, err := cli.ZAdd(ctx, args[0], zs...).Result()
if err != nil {
return redisErr(start, err)
}
return redisAffected(start, n)
case "ZREM":
if len(args) < 2 {
return redisUsage(start, "ZREM key member [member...]")
}
n, err := cli.ZRem(ctx, args[0], strsToAnys(args[1:])...).Result()
if err != nil {
return redisErr(start, err)
}
return redisAffected(start, n)
case "LPUSH", "RPUSH":
if len(args) < 2 {
return redisUsage(start, cmd+" key value [value...]")
}
var n int64
var err error
if cmd == "LPUSH" {
n, err = cli.LPush(ctx, args[0], strsToAnys(args[1:])...).Result()
} else {
n, err = cli.RPush(ctx, args[0], strsToAnys(args[1:])...).Result()
}
if err != nil {
return redisErr(start, err)
}
return redisAffected(start, n)
case "LSET":
if len(args) < 3 {
return redisUsage(start, "LSET key index value")
}
idx, err := strconv.ParseInt(args[1], 10, 64)
if err != nil {
return redisUsage(start, "LSET key index value(index 需为整数)")
}
if err := cli.LSet(ctx, args[0], idx, args[2]).Err(); err != nil {
return redisErr(start, err)
}
return redisOK(start)
case "INCR", "DECR":
if len(args) < 1 {
return redisUsage(start, cmd+" key")
}
var n int64
var err error
if cmd == "INCR" {
n, err = cli.Incr(ctx, args[0]).Result()
} else {
n, err = cli.Decr(ctx, args[0]).Result()
}
if err != nil {
return redisErr(start, err)
}
return ExecResult{Columns: []string{"value"}, Rows: []map[string]interface{}{{"value": n}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()}
case "MGET":
if len(args) < 1 {
return redisUsage(start, "MGET key [key...]")
}
vals, err := cli.MGet(ctx, args...).Result()
if err != nil {
return redisErr(start, err)
}
rows := make([]map[string]interface{}, 0, len(args))
for i, v := range vals {
rows = append(rows, map[string]interface{}{"key": args[i], "value": normalize(v)})
}
return ExecResult{Columns: []string{"key", "value"}, Rows: rows, RowCount: len(rows), TimeMs: time.Since(start).Milliseconds()}
case "MSET":
if len(args) < 2 || len(args)%2 != 0 {
return redisUsage(start, "MSET key value [key value...]")
}
if err := cli.MSet(ctx, strsToAnys(args)...).Err(); err != nil {
return redisErr(start, err)
}
return redisOK(start)
case "RENAME":
if len(args) < 2 {
return redisUsage(start, "RENAME key newkey")
}
if err := cli.Rename(ctx, args[0], args[1]).Err(); err != nil {
return redisErr(start, err)
}
return redisOK(start)
case "HGETALL":
if len(args) < 1 {
return redisUsage(start, "HGETALL key")
}
c2 := c
c2.Key = args[0]
c2.Limit = redisValueRowCap
v := GetRedisKey(ctx, c2)
if v.Error != "" && v.Type == "" {
return ExecResult{Error: v.Error, TimeMs: time.Since(start).Milliseconds()}
}
return ExecResult{Columns: v.Columns, Rows: v.Rows, RowCount: v.RowCount, TimeMs: time.Since(start).Milliseconds(), Truncated: v.Truncated, Limit: v.Limit, NextCursor: v.NextCursor, Error: v.Error}
case "LRANGE":
if len(args) < 1 {
return redisUsage(start, "LRANGE key [start stop]")
}
c2 := c
c2.Key = args[0]
c2.Limit = redisValueRowCap
v := GetRedisKey(ctx, c2)
return ExecResult{Columns: v.Columns, Rows: v.Rows, RowCount: v.RowCount, TimeMs: time.Since(start).Milliseconds(), Truncated: v.Truncated, Limit: v.Limit, NextCursor: v.NextCursor, Error: v.Error}
case "SMEMBERS":
if len(args) < 1 {
return redisUsage(start, "SMEMBERS key")
}
c2 := c
c2.Key = args[0]
v := GetRedisKey(ctx, c2)
return ExecResult{Columns: v.Columns, Rows: v.Rows, RowCount: v.RowCount, TimeMs: time.Since(start).Milliseconds(), Truncated: v.Truncated, Limit: v.Limit, NextCursor: v.NextCursor, Error: v.Error}
case "ZRANGE":
if len(args) < 1 {
return redisUsage(start, "ZRANGE key [start stop]")
}
c2 := c
c2.Key = args[0]
v := GetRedisKey(ctx, c2)
return ExecResult{Columns: v.Columns, Rows: v.Rows, RowCount: v.RowCount, TimeMs: time.Since(start).Milliseconds(), Truncated: v.Truncated, Limit: v.Limit, NextCursor: v.NextCursor, Error: v.Error}
case "TYPE":
if len(args) < 1 {
return redisUsage(start, "TYPE key")
}
t, err := cli.Type(ctx, args[0]).Result()
if err != nil {
return redisErr(start, err)
}
return ExecResult{Columns: []string{"key", "type"}, Rows: []map[string]interface{}{{"key": args[0], "type": t}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()}
case "TTL":
if len(args) < 1 {
return redisUsage(start, "TTL key")
}
n, err := cli.TTL(ctx, args[0]).Result()
if err != nil {
return redisErr(start, err)
}
return ExecResult{Columns: []string{"key", "ttl_sec"}, Rows: []map[string]interface{}{{"key": args[0], "ttl_sec": int64(n.Seconds())}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()}
default:
// 未点名的命令走通用 Do,避免 CLI 只能跑白名单。
doArgs := append([]interface{}{cmd}, strsToAnys(args)...)
val, err := cli.Do(ctx, doArgs...).Result()
if err == redis.Nil {
return ExecResult{
Columns: []string{"result"},
Rows: []map[string]interface{}{{"result": "(nil)"}},
RowCount: 1,
TimeMs: time.Since(start).Milliseconds(),
}
}
if err != nil {
return redisErr(start, err)
}
return formatRedisReply(start, val)
}
}
func formatRedisReply(start time.Time, val interface{}) ExecResult {
ms := time.Since(start).Milliseconds()
switch v := val.(type) {
case nil:
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": "(nil)"}}, RowCount: 1, TimeMs: ms}
case string:
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": v}}, RowCount: 1, TimeMs: ms}
case []byte:
return formatRedisReply(start, string(v))
case int64:
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": v}}, RowCount: 1, TimeMs: ms}
case float64:
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": v}}, RowCount: 1, TimeMs: ms}
case bool:
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": v}}, RowCount: 1, TimeMs: ms}
case error:
return ExecResult{Error: v.Error(), TimeMs: ms}
case []interface{}:
if len(v) == 0 {
return ExecResult{Columns: []string{"value"}, Rows: []map[string]interface{}{}, RowCount: 0, TimeMs: ms}
}
if kvRows := redisPairRows(v); kvRows != nil {
return ExecResult{Columns: []string{"field", "value"}, Rows: kvRows, RowCount: len(kvRows), TimeMs: ms}
}
rows := make([]map[string]interface{}, 0, len(v))
for i, item := range v {
rows = append(rows, map[string]interface{}{"index": i, "value": normalizeRedisReply(item)})
}
return ExecResult{Columns: []string{"index", "value"}, Rows: rows, RowCount: len(rows), TimeMs: ms}
case map[string]interface{}:
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
rows := make([]map[string]interface{}, 0, len(keys))
for _, k := range keys {
rows = append(rows, map[string]interface{}{"field": k, "value": normalizeRedisReply(v[k])})
}
return ExecResult{Columns: []string{"field", "value"}, Rows: rows, RowCount: len(rows), TimeMs: ms}
default:
return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": normalizeRedisReply(v)}}, RowCount: 1, TimeMs: ms}
}
}
func redisPairRows(v []interface{}) []map[string]interface{} {
if len(v) < 2 || len(v)%2 != 0 {
return nil
}
for _, item := range v {
switch item.(type) {
case string, []byte:
default:
return nil
}
}
rows := make([]map[string]interface{}, 0, len(v)/2)
for i := 0; i+1 < len(v); i += 2 {
rows = append(rows, map[string]interface{}{
"field": normalizeRedisReply(v[i]),
"value": normalizeRedisReply(v[i+1]),
})
}
return rows
}
func normalizeRedisReply(v interface{}) interface{} {
switch t := v.(type) {
case nil:
return "(nil)"
case []byte:
return string(t)
case []interface{}:
parts := make([]string, len(t))
for i, x := range t {
parts[i] = fmt.Sprintf("%v", normalizeRedisReply(x))
}
return strings.Join(parts, " ")
default:
return normalize(t)
}
}
func strsToAnys(ss []string) []interface{} {
out := make([]interface{}, len(ss))
for i, s := range ss {
out[i] = s
}
return out
}
// splitRedisCmd 按 shell 词法切分:空格分隔,单/双引号分组,反斜杠转义。
func splitRedisCmd(s string) []string {
var out []string
var cur strings.Builder
inS, inD := false, false
esc := false
for _, r := range s {
switch {
case esc:
cur.WriteRune(r)
esc = false
case r == '\\':
esc = true
case r == '"' && !inS:
inD = !inD
case r == '\'' && !inD:
inS = !inS
case !inS && !inD && (r == ' ' || r == '\t' || r == '\n'):
if cur.Len() > 0 {
out = append(out, cur.String())
cur.Reset()
}
default:
cur.WriteRune(r)
}
}
if cur.Len() > 0 {
out = append(out, cur.String())
}
return out
}
func isRedis(d string) bool {
return driverKey(d) == "redis"
}
// IsRedisDriver exported for router guards.
func IsRedisDriver(d string) bool {
return isRedis(d)
}
func listColumnsRedis(parent context.Context, c ConnReq, key string) ([]ColumnInfo, error) {
cli, done, err := redisClientFor(c)
if err != nil {
return nil, err
}
defer done()
ctx, cancel := reqCtxRedis(parent, 5*time.Second)
defer cancel()
typ, err := cli.Type(ctx, key).Result()
if err != nil {
return nil, err
}
ttl, _ := cli.TTL(ctx, key).Result()
return []ColumnInfo{
{Name: key, Type: typ, Comment: fmt.Sprintf("ttl=%ds", int64(ttl.Seconds()))},
}, nil
}
const redisKeyListCap = 500
const redisValueRowCap = 500
func redisKeyName(c ConnReq) string {
if c.Key != "" {
return c.Key
}
return c.Table
}
func redisScanPattern(c ConnReq) string {
p := strings.TrimSpace(c.Pattern)
if p == "" {
return "*"
}
return p
}
func redisValueLimit(c ConnReq) int {
n := c.Limit
if n <= 0 {
n = redisValueRowCap
}
if n > MaxRowLimit {
n = MaxRowLimit
}
return n
}
func ttlSeconds(d time.Duration) int64 {
// go-redis maps Redis TTL -1 (no expire) / -2 (missing) to -1ns/-2ns
// (some versions use -1s/-2s). Truncating with d/time.Second turns those into 0.
switch d {
case time.Duration(-1), -1 * time.Second:
return -1
case time.Duration(-2), -2 * time.Second:
return -2
}
return int64(d / time.Second)
}
// scanAll 全量 SCAN 到 limit 截断。集群模式遍历所有 master 分片 ——
// ClusterClient.Scan 只命中单个节点,会漏掉其他分片的键。
func scanAll(ctx context.Context, cli redis.UniversalClient, pat string, count int64, limit int) (names []string, truncated bool, err error) {
scanOne := func(c scanClient) error {
var cursor uint64
for {
batch, next, err := c.Scan(ctx, cursor, pat, count).Result()
if err != nil {
return err
}
names = append(names, batch...)
cursor = next
if len(names) >= limit {
truncated = true
return nil
}
if cursor == 0 {
return nil
}
}
}
if cc, ok := cli.(*redis.ClusterClient); ok {
err = cc.ForEachMaster(ctx, func(ctx context.Context, mc *redis.Client) error {
return scanOne(mc)
})
if err == nil {
// 跨分片归并排序,保证键树稳定
sort.Strings(names)
if len(names) > limit {
names = names[:limit]
}
}
return names, truncated, err
}
err = scanOne(cli)
return names, truncated, err
}
// scanClient Scan 的最小接口(*redis.Client 与 UniversalClient 都满足)
type scanClient interface {
Scan(ctx context.Context, cursor uint64, match string, count int64) *redis.ScanCmd
}
// ListRedisKeys SCAN + pipeline TYPE/TTL. Cap ~500.
func ListRedisKeys(parent context.Context, c ConnReq) RedisKeysResult {
start := time.Now()
cli, done, err := redisClientFor(c)
if err != nil {
return RedisKeysResult{Keys: []RedisKeyInfo{}, Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()}
}
defer done()
ctx, cancel := reqCtxRedis(parent, 12*time.Second)
defer cancel()
pat := redisScanPattern(c)
names, truncated, err := scanAll(ctx, cli, pat, 200, redisKeyListCap)
if err != nil {
return RedisKeysResult{Keys: []RedisKeyInfo{}, Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()}
}
if names == nil {
names = []string{}
}
out := make([]RedisKeyInfo, len(names))
if len(names) == 0 {
return RedisKeysResult{Keys: out, Truncated: truncated, TimeMs: time.Since(start).Milliseconds()}
}
// Pipeline TYPE + TTL (avoid N+1)
pipe := cli.Pipeline()
typeCmds := make([]*redis.StatusCmd, len(names))
ttlCmds := make([]*redis.DurationCmd, len(names))
for i, k := range names {
typeCmds[i] = pipe.Type(ctx, k)
ttlCmds[i] = pipe.TTL(ctx, k)
}
_, _ = pipe.Exec(ctx)
for i, k := range names {
typ := "none"
if t, err := typeCmds[i].Result(); err == nil {
typ = t
}
ttl := int64(-2)
if d, err := ttlCmds[i].Result(); err == nil {
ttl = ttlSeconds(d)
}
out[i] = RedisKeyInfo{Name: k, Type: typ, TTL: ttl}
}
return RedisKeysResult{Keys: out, Truncated: truncated, TimeMs: time.Since(start).Milliseconds()}
}
// GetRedisKey typed value → ResultPane columns/rows.
func GetRedisKey(parent context.Context, c ConnReq) RedisKeyValue {
start := time.Now()
key := redisKeyName(c)
if key == "" {
return RedisKeyValue{Error: "缺少 key", TimeMs: time.Since(start).Milliseconds()}
}
cli, done, err := redisClientFor(c)
if err != nil {
return RedisKeyValue{Name: key, Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()}
}
defer done()
ctx, cancel := reqCtxRedis(parent, 10*time.Second)
defer cancel()
typ, err := cli.Type(ctx, key).Result()
if err != nil {
return RedisKeyValue{Name: key, Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()}
}
ttl := int64(-2)
if d, err := cli.TTL(ctx, key).Result(); err == nil {
ttl = ttlSeconds(d)
}
limit := redisValueLimit(c)
base := RedisKeyValue{Name: key, Type: typ, TTL: ttl, Limit: limit}
switch typ {
case "none":
base.Columns = []string{"value"}
base.Rows = []map[string]interface{}{}
base.RowCount = 0
base.Error = "key 不存在"
base.TimeMs = time.Since(start).Milliseconds()
return base
case "string":
val, err := cli.Get(ctx, key).Result()
if err == redis.Nil {
base.Columns = []string{"value"}
base.Rows = []map[string]interface{}{}
base.RowCount = 0
base.TimeMs = time.Since(start).Milliseconds()
return base
}
if err != nil {
base.Error = errMsg(err)
base.TimeMs = time.Since(start).Milliseconds()
return base
}
base.Columns = []string{"value"}
base.Rows = []map[string]interface{}{{"value": val}}
base.RowCount = 1
base.TimeMs = time.Since(start).Milliseconds()
return base
case "hash":
// HSCAN 游标分页:cursor 回传续载(0=起点/到底),页内按 field 排序保持观感
pairs, next, err := cli.HScan(ctx, key, uint64(c.Cursor), "*", int64(limit)).Result()
if err != nil {
base.Error = errMsg(err)
base.TimeMs = time.Since(start).Milliseconds()
return base
}
base.Columns = []string{"field", "value"}
rows := make([]map[string]interface{}, 0, len(pairs)/2)
for i := 0; i+1 < len(pairs); i += 2 {
rows = append(rows, map[string]interface{}{"field": pairs[i], "value": pairs[i+1]})
}
sort.Slice(rows, func(i, j int) bool { return fmt.Sprint(rows[i]["field"]) < fmt.Sprint(rows[j]["field"]) })
base.Rows = rows
base.RowCount = len(rows)
base.NextCursor = int64(next)
base.Truncated = next != 0
base.TimeMs = time.Since(start).Milliseconds()
return base
case "list":
// 偏移分页:cursor = 起始下标
offset := c.Cursor
if offset < 0 {
offset = 0
}
vals, err := cli.LRange(ctx, key, offset, offset+int64(limit)-1).Result()
if err != nil {
base.Error = errMsg(err)
base.TimeMs = time.Since(start).Milliseconds()
return base
}
n, _ := cli.LLen(ctx, key).Result()
base.Columns = []string{"index", "value"}
rows := make([]map[string]interface{}, 0, len(vals))
for i, v := range vals {
rows = append(rows, map[string]interface{}{"index": offset + int64(i), "value": v})
}
base.Rows = rows
base.RowCount = len(rows)
if offset+int64(len(vals)) < n {
base.NextCursor = offset + int64(len(vals))
base.Truncated = true
}
base.TimeMs = time.Since(start).Milliseconds()
return base
case "set":
// SSCAN 游标分页:每页 limit,cursor 回传续载
batch, next, err := cli.SScan(ctx, key, uint64(c.Cursor), "*", int64(limit)).Result()
if err != nil {
base.Error = errMsg(err)
base.TimeMs = time.Since(start).Milliseconds()
return base
}
base.Columns = []string{"member"}
rows := make([]map[string]interface{}, 0, len(batch))
for _, m := range batch {
rows = append(rows, map[string]interface{}{"member": m})
}
base.Rows = rows
base.RowCount = len(rows)
base.NextCursor = int64(next)
base.Truncated = next != 0
base.TimeMs = time.Since(start).Milliseconds()
return base
case "zset":
// 偏移分页(按 score 升序):cursor = 起始排名
zoffset := c.Cursor
if zoffset < 0 {
zoffset = 0
}
vals, err := cli.ZRangeWithScores(ctx, key, zoffset, zoffset+int64(limit)-1).Result()
if err != nil {
base.Error = errMsg(err)
base.TimeMs = time.Since(start).Milliseconds()
return base
}
n, _ := cli.ZCard(ctx, key).Result()
if zoffset+int64(len(vals)) < n {
base.NextCursor = zoffset + int64(len(vals))
base.Truncated = true
}
base.Columns = []string{"score", "member"}
rows := make([]map[string]interface{}, 0, len(vals))
for _, z := range vals {
rows = append(rows, map[string]interface{}{"score": z.Score, "member": z.Member})
}
base.Rows = rows
base.RowCount = len(rows)
base.TimeMs = time.Since(start).Milliseconds()
return base
default:
// stream / recon / module types: meta only
base.Columns = []string{"info"}
base.Rows = []map[string]interface{}{{"info": fmt.Sprintf("类型 %s 暂不展开,可用命令查看", typ)}}
base.RowCount = 1
base.TimeMs = time.Since(start).Milliseconds()
return base
}
}
// DeleteRedisKey DEL one key.
func DeleteRedisKey(parent context.Context, c ConnReq) ExecResult {
start := time.Now()
key := redisKeyName(c)
if key == "" {
return ExecResult{Error: "缺少 key", TimeMs: time.Since(start).Milliseconds()}
}
cli, done, err := redisClientFor(c)
if err != nil {
return redisErr(start, err)
}
defer done()
ctx, cancel := reqCtxRedis(parent, 5*time.Second)
defer cancel()
n, err := cli.Del(ctx, key).Result()
if err != nil {
return redisErr(start, err)
}
return redisAffected(start, n)
}
// RedisDelPatternResult 按模式批量删除(目录删除)结果。
type RedisDelPatternResult struct {
Deleted int `json:"deleted"`
Scanned int `json:"scanned"`
Truncated bool `json:"truncated"`
Error string `json:"error"`
TimeMs int64 `json:"timeMs"`
}
// 目录删除安全上限:超出即拒绝,防止误配 pattern 造成删库风暴。
const redisDelPatternCap = 10000
// DeleteRedisPattern 按模式批量删除键(SCAN + UNLINK 分批)。用于「删除目录」:
// 前端传 `目录前缀:*`。拒绝全库 `*`;扫描量超上限拒绝。
func DeleteRedisPattern(parent context.Context, c ConnReq) RedisDelPatternResult {
start := time.Now()
fail := func(msg string) RedisDelPatternResult {
return RedisDelPatternResult{Error: msg, TimeMs: time.Since(start).Milliseconds()}
}
pat := redisScanPattern(c)
if pat == "*" || pat == "" {
return fail("拒绝全库模式删除(*),请指定目录前缀")
}
cli, done, err := redisClientFor(c)
if err != nil {
return fail(errMsg(err))
}
defer done()
ctx, cancel := reqCtxRedis(parent, 60*time.Second)
defer cancel()
names, truncated, err := scanAll(ctx, cli, pat, 500, redisDelPatternCap+1)
if err != nil {
return fail(errMsg(err))
}
if len(names) > redisDelPatternCap {
return fail(fmt.Sprintf("匹配键 %d 个超过上限 %d,请缩小目录范围", len(names), redisDelPatternCap))
}
total := 0
for i := 0; i < len(names); i += 500 {
end := i + 500
if end > len(names) {
end = len(names)
}
n, err := cli.Unlink(ctx, names[i:end]...).Result()
if err != nil {
return RedisDelPatternResult{
Deleted: total, Scanned: len(names), Truncated: truncated,
Error: errMsg(err), TimeMs: time.Since(start).Milliseconds(),
}
}
total += int(n)
}
return RedisDelPatternResult{
Deleted: total, Scanned: len(names), Truncated: truncated,
TimeMs: time.Since(start).Milliseconds(),
}
}