212 lines
4.8 KiB
Go
212 lines
4.8 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/os/glog"
|
||
"github.com/gogf/gf/v2/util/gconv"
|
||
"github.com/hashicorp/consul/api"
|
||
)
|
||
|
||
var (
|
||
configCache = make(map[string]interface{}) // 动态配置缓存
|
||
configMu sync.RWMutex // 读写锁
|
||
consulClient *api.Client // Consul客户端
|
||
startOnce sync.Once // 确保只启动一次监听
|
||
)
|
||
|
||
// InitConsulWatcher 初始化Consul配置监听
|
||
func InitConsulWatcher(ctx context.Context) error {
|
||
consulAddr := g.Cfg().MustGet(ctx, "consul.address").String()
|
||
if consulAddr == "" {
|
||
glog.Warning(ctx, "Consul未配置,使用本地config.yml")
|
||
return nil
|
||
}
|
||
|
||
config := api.DefaultConfig()
|
||
config.Address = consulAddr
|
||
client, err := api.NewClient(config)
|
||
if err != nil {
|
||
glog.Errorf(ctx, "Consul客户端初始化失败: %v", err)
|
||
return err
|
||
}
|
||
consulClient = client
|
||
|
||
startOnce.Do(func() {
|
||
go watchAllConfigsV2(ctx)
|
||
glog.Info(ctx, "Consul配置监听已启动")
|
||
})
|
||
|
||
return nil
|
||
}
|
||
|
||
// watchAllConfigsV2 监听Consul前缀下所有配置
|
||
func watchAllConfigsV2(ctx context.Context) {
|
||
const prefix = "customerService/"
|
||
kv := consulClient.KV()
|
||
var lastIndex uint64
|
||
|
||
for {
|
||
pairs, meta, err := kv.List(prefix, &api.QueryOptions{
|
||
WaitIndex: lastIndex,
|
||
WaitTime: 5 * time.Minute,
|
||
})
|
||
|
||
if err != nil {
|
||
glog.Errorf(ctx, "Consul查询失败: %v", err)
|
||
time.Sleep(5 * time.Second)
|
||
continue
|
||
}
|
||
|
||
if meta.LastIndex != lastIndex {
|
||
lastIndex = meta.LastIndex
|
||
updateConfigCacheDiff(ctx, pairs)
|
||
}
|
||
}
|
||
}
|
||
|
||
// updateConfigCacheDiff 增量更新配置缓存
|
||
func updateConfigCacheDiff(ctx context.Context, pairs api.KVPairs) {
|
||
configMu.Lock()
|
||
defer configMu.Unlock()
|
||
|
||
for _, pair := range pairs {
|
||
key := strings.TrimPrefix(pair.Key, "customerService/")
|
||
key = strings.ReplaceAll(key, "/", ".")
|
||
|
||
newVal := parseValue(pair.Value)
|
||
oldVal, exists := configCache[key]
|
||
|
||
if !exists || oldVal != newVal {
|
||
logConfigChange(ctx, key, gconv.String(oldVal), gconv.String(newVal))
|
||
configCache[key] = newVal
|
||
}
|
||
}
|
||
}
|
||
|
||
// parseValue 自动推断配置值类型
|
||
func parseValue(value []byte) interface{} {
|
||
str := string(value)
|
||
|
||
if i, err := strconv.Atoi(str); err == nil {
|
||
return i
|
||
}
|
||
|
||
if b, err := strconv.ParseBool(str); err == nil {
|
||
return b
|
||
}
|
||
|
||
var arr []interface{}
|
||
if err := json.Unmarshal(value, &arr); err == nil {
|
||
return arr
|
||
}
|
||
|
||
return str
|
||
}
|
||
|
||
// GetConfigInt 读取int配置
|
||
func GetConfigInt(ctx context.Context, key string) int {
|
||
configMu.RLock()
|
||
val, ok := configCache[key]
|
||
configMu.RUnlock()
|
||
|
||
if ok {
|
||
return gconv.Int(val)
|
||
}
|
||
|
||
return g.Cfg().MustGet(ctx, key).Int()
|
||
}
|
||
|
||
// GetConfigString 读取string配置
|
||
func GetConfigString(ctx context.Context, key string) string {
|
||
configMu.RLock()
|
||
val, ok := configCache[key]
|
||
configMu.RUnlock()
|
||
|
||
if ok {
|
||
return gconv.String(val)
|
||
}
|
||
|
||
return g.Cfg().MustGet(ctx, key).String()
|
||
}
|
||
|
||
// GetConfigBool 读取bool配置
|
||
func GetConfigBool(ctx context.Context, key string) bool {
|
||
configMu.RLock()
|
||
val, ok := configCache[key]
|
||
configMu.RUnlock()
|
||
|
||
if ok {
|
||
return gconv.Bool(val)
|
||
}
|
||
|
||
return g.Cfg().MustGet(ctx, key).Bool()
|
||
}
|
||
|
||
// GetConfigStringSlice 读取字符串数组配置
|
||
func GetConfigStringSlice(ctx context.Context, key string) []string {
|
||
configMu.RLock()
|
||
val, ok := configCache[key]
|
||
configMu.RUnlock()
|
||
|
||
if ok {
|
||
if arr, ok := val.([]interface{}); ok {
|
||
result := make([]string, len(arr))
|
||
for i, v := range arr {
|
||
result[i] = gconv.String(v)
|
||
}
|
||
return result
|
||
}
|
||
}
|
||
|
||
return g.Cfg().MustGet(ctx, key).Strings()
|
||
}
|
||
|
||
// GetInstanceConfigStringSlice 读取实例级字符串数组配置(支持实例级负载隔离)
|
||
// 优先级:实例专用配置 > 全局Consul配置 > config.yml
|
||
func GetInstanceConfigStringSlice(ctx context.Context, key string) []string {
|
||
// 获取实例ID(环境变量优先,hostname备用)
|
||
instanceID := os.Getenv("INSTANCE_ID")
|
||
if instanceID == "" {
|
||
hostname, err := os.Hostname()
|
||
if err == nil {
|
||
instanceID = hostname
|
||
}
|
||
}
|
||
|
||
// 如果有实例ID,先查找实例专用配置
|
||
if instanceID != "" {
|
||
instanceKey := fmt.Sprintf("instance.%s.%s", instanceID, key)
|
||
configMu.RLock()
|
||
val, ok := configCache[instanceKey]
|
||
configMu.RUnlock()
|
||
|
||
if ok {
|
||
if arr, ok := val.([]interface{}); ok {
|
||
result := make([]string, len(arr))
|
||
for i, v := range arr {
|
||
result[i] = gconv.String(v)
|
||
}
|
||
glog.Debugf(ctx, "🎯 使用实例专用配置: %s = %v", instanceKey, result)
|
||
return result
|
||
}
|
||
}
|
||
}
|
||
|
||
// 未找到实例配置,fallback到全局配置
|
||
return GetConfigStringSlice(ctx, key)
|
||
}
|
||
|
||
// logConfigChange 记录配置变更
|
||
func logConfigChange(ctx context.Context, key, oldVal, newVal string) {
|
||
glog.Infof(ctx, "📝 配置变更: %s = %s → %s", key, oldVal, newVal)
|
||
}
|