feat(backend): redis key scan service - cursor pagination + cluster + CLI fix

Modeled on opsxdb's redis scan design:
- opaque base64 scan cursor {pattern,db,type,nodes,index,cursor}; server
  validates search conditions match, rejects stale cursors (400)
- soft page cap 200 keys (whole scan batches returned, no key loss),
  max 20 SCAN calls per page; next_cursor until all shards exhausted
- pattern rule: no glob chars -> *kw* contains match; non-glob input
  also probed with TYPE for exact hit (returned as exact, slot-routed
  so cluster/NAT safe)
- pipeline batch TYPE/TTL replaces per-key serial N+1; type filter now
  effective server-side
- cluster: per-master direct SCAN merged across shards (MasterAddrs on
  wrapper), DB0-only, MOVED errors hint at switching deploy mode;
  SlowLog/ClientList aggregate across masters (were cluster-broken)
- GET /redis/:id/dbs (0-15, cluster -> [0])
- POST /redis/:id/keys/:key/field: hash row-level HSET/HDEL (no whole-
  key rewrite)
- RedisExecuteCommand fixed (was always-403): accepts {db, command}
  full-string, quote-aware tokenizer, whitelist on first word, expanded
  read-only set
- RedisUpdateKey preserves previous TTL unless explicitly set/cleared
  (was silently dropping TTL); string type skips the DEL

Verified live against local redis: 200/page no overlap across pages,
cursor 400 on condition change, exact hit, type filter, hash field
edit round-trip, CLI DBSIZE/HGET ok, SET rejected
This commit is contained in:
Logan.Li committed 2026-10-26 18:27:11 +08:00
1 parent 6945c53879
commit 6604fdf48e
3 files changed
+521 -129

No files matched your search

+3 -1
View File
@@ -93,7 +93,9 @@ func Setup(r *gin.Engine) {
// Redis
auth.GET("/redis/:id/info", handler.RedisInfo)
auth.GET("/redis/:id/dbsize", handler.RedisDbsize)
auth.GET("/redis/:id/keys", handler.RedisScanKeys)
auth.GET("/redis/:id/dbs", handler.RedisDbs)
auth.GET("/redis/:id/keys", handler.RedisScanKeysPaged)
auth.POST("/redis/:id/keys/:key/field", handler.RedisUpdateKeyField)
auth.GET("/redis/:id/keys/:key", handler.RedisGetKeyDetail)
auth.GET("/redis/:id/slowlog", handler.RedisSlowlog)
auth.GET("/redis/:id/clients", handler.RedisClients)