1
This commit is contained in:
211
service/config_service.go
Normal file
211
service/config_service.go
Normal file
@@ -0,0 +1,211 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user