deepseek修改版
This commit is contained in:
+18
-9
@@ -49,7 +49,10 @@ func (p *RoundRobinPicker) Pick(replicas []*gorm.DB) *gorm.DB {
|
||||
return replicas[int(n-1)%len(replicas)]
|
||||
}
|
||||
|
||||
// RandomPicker 随机选择从库
|
||||
// RandomPicker 随机选择从库。
|
||||
//
|
||||
// D2 注释:rand.Intn 自 Go 1.20 起(go.dev/issue/54899)内部使用 per-goroutine
|
||||
// 随机源,并发安全无需额外同步。本模块 go.mod 声明 go 1.25.0,满足此要求。
|
||||
type RandomPicker struct{}
|
||||
|
||||
// Pick 随机选择一个从库
|
||||
@@ -69,11 +72,11 @@ type Manager struct {
|
||||
mu sync.Mutex
|
||||
|
||||
// #21 健康自愈
|
||||
healthy atomic.Bool // 主库是否健康
|
||||
replicaHealthy []atomic.Bool // 每个从库的健康标记,索引与 replicas 对齐
|
||||
probeFailures int // 主库连续探活失败次数
|
||||
probeMu sync.Mutex // 保护 probeFailures
|
||||
replicaHealthSet bool // replicaHealthy 是否已按 replicas 长度初始化
|
||||
healthy atomic.Bool // 主库是否健康
|
||||
replicaHealthy []atomic.Bool // 每个从库的健康标记,索引与 replicas 对齐
|
||||
probeFailures int // 主库连续探活失败次数
|
||||
probeMu sync.Mutex // 保护 probeFailures
|
||||
replicaHealthSet bool // replicaHealthy 是否已按 replicas 长度初始化
|
||||
}
|
||||
|
||||
// NewManager 创建数据库管理器
|
||||
@@ -434,18 +437,24 @@ func (m *Manager) InitDB(cfg *config.Config) error {
|
||||
|
||||
// isTransientDBError 判断数据库连接错误是否值得重试。
|
||||
// 认证失败、未知数据库、非法 DSN/驱动等属于配置类错误,重试无意义,直接返回更友好。
|
||||
// D5 修复:覆盖 MySQL 和 PostgreSQL 常见非瞬态错误。
|
||||
func isTransientDBError(err error) bool {
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
msg := err.Error()
|
||||
nonTransient := []string{
|
||||
"Access denied", // MySQL 认证失败(用户名/密码错误)
|
||||
"authentication plugin", // MySQL 认证插件不支持
|
||||
"Unknown database", // MySQL 目标库不存在
|
||||
// MySQL
|
||||
"Access denied", // 认证失败(用户名/密码错误)
|
||||
"authentication plugin", // 认证插件不支持
|
||||
"Unknown database", // 目标库不存在
|
||||
"invalid DSN", // DSN 语法错误
|
||||
"unknown driver", // 驱动未注册
|
||||
"unsupported driver", // 驱动不支持
|
||||
// PostgreSQL(D5 修复)
|
||||
"password authentication failed", // pg 密码错误
|
||||
"database", "does not exist", // pg 目标库不存在(消息含"database ... does not exist")
|
||||
"no pg_hba.conf entry", // pg_hba.conf 拒绝
|
||||
}
|
||||
for _, sub := range nonTransient {
|
||||
if strings.Contains(msg, sub) {
|
||||
|
||||
+34
-12
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/EthanCodeCraft/xlgo-core/config"
|
||||
"github.com/EthanCodeCraft/xlgo-core/logger"
|
||||
@@ -12,9 +13,22 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// RedisClient 全局 Redis 客户端(兼容 facade,由 RedisManager.Init 同步维护)。
|
||||
// 保留供存量代码直接访问;新代码建议用 GetRedis() 或持有 *RedisManager 实例。
|
||||
var RedisClient *redis.Client
|
||||
// redisClient 内部 Redis 客户端引用(由 RedisManager.Init/Close 同步维护)。
|
||||
//
|
||||
// 已废弃对外暴露:所有外部代码请使用 GetRedis() 或持有 *RedisManager 实例。
|
||||
// 测试如需注入 mock Redis,请用 SetDefaultRedisManager 替换 DefaultRedis。
|
||||
//
|
||||
// 仅包内 Init/Close 在持有 m.mu 时写入;外部直接读取存在竞态风险。
|
||||
var redisClient *redis.Client
|
||||
|
||||
// SetTestRedisClient 供测试注入 miniredis 等 mock 客户端。
|
||||
// 返回旧客户端引用以便测试清理时恢复。生产代码严禁调用。
|
||||
// 注意:仅在测试环境单 goroutine 使用,不持有锁保护。
|
||||
func SetTestRedisClient(c *redis.Client) *redis.Client {
|
||||
old := redisClient
|
||||
redisClient = c
|
||||
return old
|
||||
}
|
||||
|
||||
// RedisManager Redis 连接管理器(#10)。照 database.Manager 模式:
|
||||
// 实例化 + DefaultRedis 全局默认 + 包级 facade 代理,支持多实例与测试注入。
|
||||
@@ -44,20 +58,24 @@ func (m *RedisManager) Init(cfg *config.Config) error {
|
||||
defer m.mu.Unlock()
|
||||
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: cfg.Redis.Addr(),
|
||||
Password: cfg.Redis.Password,
|
||||
DB: cfg.Redis.DB,
|
||||
Addr: cfg.Redis.Addr(),
|
||||
Password: cfg.Redis.Password,
|
||||
DB: cfg.Redis.DB,
|
||||
DialTimeout: 5 * time.Second, // D7 修复:连接超时
|
||||
ReadTimeout: 3 * time.Second, // D7 修复:读超时
|
||||
WriteTimeout: 3 * time.Second, // D7 修复:写超时
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
if err := client.Ping(ctx).Err(); err != nil {
|
||||
pingCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := client.Ping(pingCtx).Err(); err != nil {
|
||||
client.Close()
|
||||
return fmt.Errorf("Redis 连接失败: %w", err)
|
||||
}
|
||||
|
||||
m.cfg = cfg
|
||||
m.client = client
|
||||
RedisClient = client // 同步兼容 facade
|
||||
redisClient = client // 内部同步
|
||||
logger.Info("Redis 连接成功", zap.String("addr", cfg.Redis.Addr()))
|
||||
return nil
|
||||
}
|
||||
@@ -72,7 +90,7 @@ func (m *RedisManager) Close() error {
|
||||
}
|
||||
err := m.client.Close()
|
||||
m.client = nil
|
||||
RedisClient = nil
|
||||
redisClient = nil
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -111,7 +129,11 @@ func HealthCheckRedis(ctx context.Context) error {
|
||||
return DefaultRedis.HealthCheck(ctx)
|
||||
}
|
||||
|
||||
// GetRedis 获取 Redis 客户端
|
||||
// GetRedis 获取 Redis 客户端。
|
||||
// 优先返回 DefaultRedis 实例化的客户端;若未初始化则回退到内部客户端(测试注入路径)。
|
||||
func GetRedis() *redis.Client {
|
||||
return DefaultRedis.Client()
|
||||
if c := DefaultRedis.Client(); c != nil {
|
||||
return c
|
||||
}
|
||||
return redisClient
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user