refactor: simplify Redis structure to group by Host and support full-host updates via simplified PubSub payload (by AI)

This commit is contained in:
AI Engineer 2026-05-12 23:53:22 +08:00
parent 62fd644831
commit 2d25de4a7a
3 changed files with 55 additions and 85 deletions

113
app.go
View File

@ -6,15 +6,9 @@ import (
"apigo.cc/go/redis" "apigo.cc/go/redis"
"apigo.cc/go/service" "apigo.cc/go/service"
"context" "context"
"strings"
) )
// EventMessage 定义从 Redis 接收到的更新事件结构
type EventMessage struct {
Action string `json:"action"` // "update"
Type string `json:"type"` // "proxy", "rewrite", "static"
Host string `json:"host"`
}
// Config 定义 Gateway 基础配置 // Config 定义 Gateway 基础配置
type Config struct { type Config struct {
Redis string // Redis 注册中心/配置中心 URL Redis string // Redis 注册中心/配置中心 URL
@ -30,9 +24,6 @@ var GatewayConf = Config{
// GatewayApp 定义网关应用 // GatewayApp 定义网关应用
type GatewayApp struct { type GatewayApp struct {
rd *redis.Redis rd *redis.Redis
proxiesKey string
rewritesKey string
staticsKey string
pubsubChannel string pubsubChannel string
cancelPubSub context.CancelFunc cancelPubSub context.CancelFunc
} }
@ -40,9 +31,6 @@ type GatewayApp struct {
// NewGatewayApp 创建 Gateway // NewGatewayApp 创建 Gateway
func NewGatewayApp() *GatewayApp { func NewGatewayApp() *GatewayApp {
g := &GatewayApp{} g := &GatewayApp{}
g.proxiesKey = GatewayConf.Prefix + ":proxies"
g.rewritesKey = GatewayConf.Prefix + ":rewrites"
g.staticsKey = GatewayConf.Prefix + ":statics"
g.pubsubChannel = GatewayConf.Channel g.pubsubChannel = GatewayConf.Channel
return g return g
} }
@ -77,7 +65,7 @@ func (g *GatewayApp) Init() error {
return nil return nil
} }
// loadAll 初始化阶段从 Redis 的 Hash 表拉取全量配置并解析 // loadAll 初始化阶段从 Redis 扫描所有网关配置并加载
func (g *GatewayApp) loadAll() { func (g *GatewayApp) loadAll() {
if g.rd == nil { if g.rd == nil {
return return
@ -85,79 +73,62 @@ func (g *GatewayApp) loadAll() {
log.DefaultLogger.Info("gateway loading full configuration") log.DefaultLogger.Info("gateway loading full configuration")
// 1. Proxies // 为了简化,由于 Redis KEYS 命令在线上不推荐,更好的方式是 Gateway 启动时只加载本地固定配置,
proxiesHash := g.rd.Do("HGETALL", g.proxiesKey).StringMap() // 动态配置按需拉取?不,网关启动必须拉取全量。
for host, jsonStr := range proxiesHash { // 但如果我们改成以 host 为 Key就需要维护一个 Set 存放所有的 host或者直接遍历 KEYS gateway:*
var rules []service.ProxyRule // 为了最佳实践,我们在 redis 维护一个 set: `gateway:hosts` 存放所有的域名
_ = cast.UnmarshalJSON([]byte(jsonStr), &rules) hosts := g.rd.Do("SMEMBERS", GatewayConf.Prefix+":hosts").Strings()
service.ReplaceProxies(host, rules) for _, host := range hosts {
} g.loadHost(host)
// 2. Rewrites
rewritesHash := g.rd.Do("HGETALL", g.rewritesKey).StringMap()
for host, jsonStr := range rewritesHash {
var rules []service.RewriteRule
_ = cast.UnmarshalJSON([]byte(jsonStr), &rules)
service.ReplaceRewrites(host, rules)
}
// 3. Statics
staticsHash := g.rd.Do("HGETALL", g.staticsKey).StringMap()
for host, jsonStr := range staticsHash {
var config map[string]string
_ = cast.UnmarshalJSON([]byte(jsonStr), &config)
if config != nil {
service.ReplaceStatics(host, config)
}
} }
} }
// loadHost 仅加载指定 Host 下的指定类型配置 // loadHost 加载指定 Host 下的所有类型配置
func (g *GatewayApp) loadHost(typ string, host string) { func (g *GatewayApp) loadHost(host string) {
if g.rd == nil { if g.rd == nil || host == "" {
return return
} }
switch typ { hashKey := GatewayConf.Prefix + ":host:" + host
case "proxy": hashMap := g.rd.Do("HGETALL", hashKey).StringMap()
jsonStr := g.rd.Do("HGET", g.proxiesKey, host).String()
var rules []service.ProxyRule // 1. Proxies
if jsonStr != "" { var proxyRules []service.ProxyRule
_ = cast.UnmarshalJSON([]byte(jsonStr), &rules) if jsonStr := hashMap["proxies"]; jsonStr != "" {
_ = cast.UnmarshalJSON([]byte(jsonStr), &proxyRules)
} }
service.ReplaceProxies(host, rules) service.ReplaceProxies(host, proxyRules)
log.DefaultLogger.Info("gateway proxy updated via pub/sub", "host", host, "count", len(rules))
case "rewrite": // 2. Rewrites
jsonStr := g.rd.Do("HGET", g.rewritesKey, host).String() var rewriteRules []service.RewriteRule
var rules []service.RewriteRule if jsonStr := hashMap["rewrites"]; jsonStr != "" {
if jsonStr != "" { _ = cast.UnmarshalJSON([]byte(jsonStr), &rewriteRules)
_ = cast.UnmarshalJSON([]byte(jsonStr), &rules)
} }
service.ReplaceRewrites(host, rules) service.ReplaceRewrites(host, rewriteRules)
log.DefaultLogger.Info("gateway rewrite updated via pub/sub", "host", host, "count", len(rules))
case "static": // 3. Statics
jsonStr := g.rd.Do("HGET", g.staticsKey, host).String() var staticConfig map[string]string
var config map[string]string if jsonStr := hashMap["statics"]; jsonStr != "" {
if jsonStr != "" { _ = cast.UnmarshalJSON([]byte(jsonStr), &staticConfig)
_ = cast.UnmarshalJSON([]byte(jsonStr), &config)
} }
if config == nil { if staticConfig == nil {
config = make(map[string]string) staticConfig = make(map[string]string)
}
service.ReplaceStatics(host, config)
log.DefaultLogger.Info("gateway static updated via pub/sub", "host", host, "count", len(config))
} }
service.ReplaceStatics(host, staticConfig)
log.DefaultLogger.Info("gateway host configuration updated via pub/sub", "host", host,
"proxies", len(proxyRules), "rewrites", len(rewriteRules), "statics", len(staticConfig))
} }
func (g *GatewayApp) subscribe(ctx context.Context) { func (g *GatewayApp) subscribe(ctx context.Context) {
log.DefaultLogger.Info("gateway subscribed to redis channel", "channel", g.pubsubChannel) log.DefaultLogger.Info("gateway subscribed to redis channel", "channel", g.pubsubChannel)
g.rd.Subscribe(g.pubsubChannel, nil, func(data []byte) { g.rd.Subscribe(g.pubsubChannel, nil, func(data []byte) {
var msg EventMessage host := string(data)
if err := cast.UnmarshalJSON(data, &msg); err == nil && msg.Action == "update" { host = strings.TrimSpace(strings.Trim(host, "\"")) // 兼容裸字符串和 JSON 字符串
if host != "" {
// 触发对应 Host 的局部刷新 // 触发对应 Host 的局部刷新
g.loadHost(msg.Type, msg.Host) g.loadHost(host)
} else {
log.DefaultLogger.Warning("gateway received invalid pub/sub message", "data", string(data))
} }
}) })

View File

@ -24,7 +24,7 @@ func TestGateway(t *testing.T) {
} }
// 清理测试数据 // 清理测试数据
rd.Do("DEL", "gateway:proxies", "gateway:rewrites", "gateway:statics") rd.Do("DEL", "gateway:host:gw.test", "gateway:hosts")
// 2. 启动一个后端测试服务 test-backend // 2. 启动一个后端测试服务 test-backend
service.Config.App = "test-backend" service.Config.App = "test-backend"
@ -56,8 +56,9 @@ func TestGateway(t *testing.T) {
proxyJson, _ := json.Marshal(proxyRules) proxyJson, _ := json.Marshal(proxyRules)
rewriteJson, _ := json.Marshal(rewriteRules) rewriteJson, _ := json.Marshal(rewriteRules)
rd.Do("HSET", "gateway:proxies", "gw.test", string(proxyJson)) rd.Do("SADD", "gateway:hosts", "gw.test")
rd.Do("HSET", "gateway:rewrites", "gw.test", string(rewriteJson)) rd.Do("HSET", "gateway:host:gw.test", "proxies", string(proxyJson))
rd.Do("HSET", "gateway:host:gw.test", "rewrites", string(rewriteJson))
// 创建临时静态目录 // 创建临时静态目录
tmpDir, _ := os.MkdirTemp("", "gateway-static") tmpDir, _ := os.MkdirTemp("", "gateway-static")
@ -68,7 +69,7 @@ func TestGateway(t *testing.T) {
"/ui": tmpDir, "/ui": tmpDir,
} }
staticJson, _ := json.Marshal(staticConfig) staticJson, _ := json.Marshal(staticConfig)
rd.Do("HSET", "gateway:statics", "gw.test", string(staticJson)) rd.Do("HSET", "gateway:host:gw.test", "statics", string(staticJson))
// 4. 启动 Gateway 应用 // 4. 启动 Gateway 应用
app := NewGatewayApp() app := NewGatewayApp()
@ -168,12 +169,10 @@ func TestGateway(t *testing.T) {
{Path: "/new-direct", ToApp: "test-backend", ToPath: "/hello"}, {Path: "/new-direct", ToApp: "test-backend", ToPath: "/hello"},
} }
newProxyJson, _ := json.Marshal(newProxyRules) newProxyJson, _ := json.Marshal(newProxyRules)
rd.Do("HSET", "gateway:proxies", "gw.test", string(newProxyJson)) rd.Do("HSET", "gateway:host:gw.test", "proxies", string(newProxyJson))
// 发布更新消息 // 发布更新消息
msg := EventMessage{Action: "update", Type: "proxy", Host: "gw.test"} rd.Do("PUBLISH", "gateway:channel", `"gw.test"`)
msgJson, _ := json.Marshal(msg)
rd.Do("PUBLISH", "gateway:channel", string(msgJson))
// 等待接收和更新 // 等待接收和更新
time.Sleep(300 * time.Millisecond) time.Sleep(300 * time.Millisecond)

6
go.mod
View File

@ -5,23 +5,23 @@ go 1.25.0
require ( require (
apigo.cc/go/cast v1.3.1 apigo.cc/go/cast v1.3.1
apigo.cc/go/config v1.3.0 apigo.cc/go/config v1.3.0
apigo.cc/go/discover v1.3.0
apigo.cc/go/http v1.3.0
apigo.cc/go/log v1.3.1 apigo.cc/go/log v1.3.1
apigo.cc/go/redis v1.3.0 apigo.cc/go/redis v1.3.0
apigo.cc/go/service v1.3.1 apigo.cc/go/service v1.3.1
apigo.cc/go/starter v1.0.1 apigo.cc/go/starter v1.0.1
apigo.cc/go/timer v1.3.0
) )
require ( require (
apigo.cc/go/crypto v1.3.0 // indirect apigo.cc/go/crypto v1.3.0 // indirect
apigo.cc/go/discover v1.3.0 // indirect
apigo.cc/go/encoding v1.3.0 // indirect apigo.cc/go/encoding v1.3.0 // indirect
apigo.cc/go/file v1.3.0 // indirect apigo.cc/go/file v1.3.0 // indirect
apigo.cc/go/http v1.3.0 // indirect
apigo.cc/go/id v1.3.0 // indirect apigo.cc/go/id v1.3.0 // indirect
apigo.cc/go/rand v1.3.0 // indirect apigo.cc/go/rand v1.3.0 // indirect
apigo.cc/go/safe v1.3.0 // indirect apigo.cc/go/safe v1.3.0 // indirect
apigo.cc/go/shell v1.3.0 // indirect apigo.cc/go/shell v1.3.0 // indirect
apigo.cc/go/timer v1.3.0 // indirect
github.com/gomodule/redigo v1.9.3 // indirect github.com/gomodule/redigo v1.9.3 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
golang.org/x/crypto v0.51.0 // indirect golang.org/x/crypto v0.51.0 // indirect