Files
NetworkAuth/utils/database.go

328 lines
11 KiB
Go
Raw Normal View History

2025-10-24 00:09:45 +08:00
package utils
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"sync"
"time"
"github.com/redis/go-redis/v9"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"gorm.io/gorm"
)
// DatabaseConfig 数据库连接池配置结构体
// 用于配置数据库连接池的各项参数,包括连接池大小、生命周期管理和健康检查等
type DatabaseConfig struct {
// 连接池配置
MaxIdleConns int `mapstructure:"max_idle_conns"` // 最大空闲连接数
MaxOpenConns int `mapstructure:"max_open_conns"` // 最大打开连接数
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` // 连接最大生存时间
ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"` // 连接最大空闲时间
// 健康检查配置
PingTimeout time.Duration `mapstructure:"ping_timeout"` // Ping超时时间
HealthCheckInterval time.Duration `mapstructure:"health_check_interval"` // 健康检查间隔
}
// GetDefaultDatabaseConfig 获取默认数据库配置
// 返回一个包含合理默认值的数据库配置实例
func GetDefaultDatabaseConfig() *DatabaseConfig {
return &DatabaseConfig{
MaxIdleConns: 10, // 默认最大空闲连接数
MaxOpenConns: 100, // 默认最大打开连接数
ConnMaxLifetime: 30 * time.Minute, // 连接最大生存时间30分钟
ConnMaxIdleTime: 10 * time.Minute, // 连接最大空闲时间10分钟
PingTimeout: 5 * time.Second, // Ping超时5秒
HealthCheckInterval: 30 * time.Second, // 健康检查间隔30秒
}
}
// LoadDatabaseConfig 从配置文件加载数据库配置
// 使用指定的前缀从viper配置中读取数据库配置如果配置项不存在则使用默认值
func LoadDatabaseConfig(prefix string) *DatabaseConfig {
config := GetDefaultDatabaseConfig()
// 从viper读取配置如果不存在则使用默认值
if viper.IsSet(prefix + ".max_idle_conns") {
config.MaxIdleConns = viper.GetInt(prefix + ".max_idle_conns")
}
if viper.IsSet(prefix + ".max_open_conns") {
config.MaxOpenConns = viper.GetInt(prefix + ".max_open_conns")
}
if viper.IsSet(prefix + ".conn_max_lifetime") {
config.ConnMaxLifetime = viper.GetDuration(prefix + ".conn_max_lifetime")
}
if viper.IsSet(prefix + ".conn_max_idle_time") {
config.ConnMaxIdleTime = viper.GetDuration(prefix + ".conn_max_idle_time")
}
if viper.IsSet(prefix + ".ping_timeout") {
config.PingTimeout = viper.GetDuration(prefix + ".ping_timeout")
}
if viper.IsSet(prefix + ".health_check_interval") {
config.HealthCheckInterval = viper.GetDuration(prefix + ".health_check_interval")
}
return config
}
// ConfigureConnectionPool 配置数据库连接池
// 根据提供的配置参数设置GORM数据库的连接池属性
func ConfigureConnectionPool(db *gorm.DB, config *DatabaseConfig) error {
sqlDB, err := db.DB()
if err != nil {
return fmt.Errorf("获取底层数据库连接失败: %w", err)
}
// 设置连接池参数
sqlDB.SetMaxIdleConns(config.MaxIdleConns)
sqlDB.SetMaxOpenConns(config.MaxOpenConns)
sqlDB.SetConnMaxLifetime(config.ConnMaxLifetime)
sqlDB.SetConnMaxIdleTime(config.ConnMaxIdleTime)
// LogInfo("数据库连接池配置完成", map[string]interface{}{
// "max_idle_conns": config.MaxIdleConns,
// "max_open_conns": config.MaxOpenConns,
// "conn_max_lifetime": config.ConnMaxLifetime,
// "conn_max_idle_time": config.ConnMaxIdleTime,
// })
return nil
}
// PingDatabase 检查数据库连接健康状态
// 使用指定的超时时间ping数据库以验证连接是否正常
func PingDatabase(db *gorm.DB, timeout time.Duration) error {
sqlDB, err := db.DB()
if err != nil {
return fmt.Errorf("获取底层数据库连接失败: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return sqlDB.PingContext(ctx)
}
// GetConnectionStats 获取数据库连接池统计信息
// 返回当前数据库连接池的详细统计数据,包括连接数、等待时间等
func GetConnectionStats(db *gorm.DB) (*sql.DBStats, error) {
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("获取底层数据库连接失败: %w", err)
}
stats := sqlDB.Stats()
return &stats, nil
}
// LogConnectionStats 记录数据库连接池统计信息
// 获取并记录数据库连接池的统计信息到日志中,用于监控和调试
func LogConnectionStats(db *gorm.DB) {
stats, err := GetConnectionStats(db)
if err != nil {
LogError("获取数据库连接池统计信息失败", err, nil)
return
}
LogInfo("数据库连接池统计", map[string]interface{}{
"open_connections": stats.OpenConnections,
"in_use": stats.InUse,
"idle": stats.Idle,
"wait_count": stats.WaitCount,
"wait_duration": stats.WaitDuration,
"max_idle_closed": stats.MaxIdleClosed,
"max_idle_time_closed": stats.MaxIdleTimeClosed,
"max_lifetime_closed": stats.MaxLifetimeClosed,
})
}
// StartHealthCheck 启动数据库健康检查
// 启动一个后台goroutine定期检查数据库连接健康状态
// 只在健康检查失败时输出错误日志,正常情况下不输出日志
func StartHealthCheck(db *gorm.DB, config *DatabaseConfig) {
go func() {
ticker := time.NewTicker(config.HealthCheckInterval)
defer ticker.Stop()
for range ticker.C {
if err := PingDatabase(db, config.PingTimeout); err != nil {
// 只在健康检查失败时输出错误日志
LogError("数据库健康检查失败", err, map[string]interface{}{
"ping_timeout": config.PingTimeout,
})
}
// 记录连接池统计信息(仅在调试模式下)
if logrus.GetLevel() == logrus.DebugLevel {
LogConnectionStats(db)
}
}
}()
// LogInfo("数据库健康检查已启动", map[string]interface{}{
// "check_interval": config.HealthCheckInterval,
// "ping_timeout": config.PingTimeout,
// })
}
// ValidateDatabaseConfig 验证数据库配置参数
// 检查数据库配置参数的有效性,确保所有参数都在合理范围内
func ValidateDatabaseConfig(config *DatabaseConfig) error {
if config.MaxIdleConns < 0 {
return fmt.Errorf("最大空闲连接数不能为负数: %d", config.MaxIdleConns)
}
if config.MaxOpenConns < 0 {
return fmt.Errorf("最大打开连接数不能为负数: %d", config.MaxOpenConns)
}
if config.MaxIdleConns > config.MaxOpenConns && config.MaxOpenConns > 0 {
return fmt.Errorf("最大空闲连接数(%d)不能大于最大打开连接数(%d)", config.MaxIdleConns, config.MaxOpenConns)
}
if config.ConnMaxLifetime < 0 {
return fmt.Errorf("连接最大生存时间不能为负数: %v", config.ConnMaxLifetime)
}
if config.ConnMaxIdleTime < 0 {
return fmt.Errorf("连接最大空闲时间不能为负数: %v", config.ConnMaxIdleTime)
}
if config.PingTimeout <= 0 {
return fmt.Errorf("Ping超时时间必须大于0: %v", config.PingTimeout)
}
if config.HealthCheckInterval <= 0 {
return fmt.Errorf("健康检查间隔必须大于0: %v", config.HealthCheckInterval)
}
return nil
}
var (
// redisClient 全局Redis客户端
redisClient *redis.Client
// redisOnce 确保只初始化一次
redisOnce sync.Once
// redisAvailable 标记Redis是否可用
redisAvailable bool
)
// InitRedis 初始化Redis客户端仅在配置存在时尝试连接
// - 从 viper 读取 security.redis.* 配置
// - 如果连接失败,则标记为不可用,不影响主流程
func InitRedis() {
redisOnce.Do(func() {
host := viper.GetString("redis.host")
port := viper.GetInt("redis.port")
if host == "" || port == 0 {
logrus.Info("未配置Redis或配置不完整跳过初始化")
redisAvailable = false
return
}
addr := fmt.Sprintf("%s:%d", host, port)
redisClient = redis.NewClient(&redis.Options{
Addr: addr,
Password: viper.GetString("redis.password"),
DB: viper.GetInt("redis.db"),
})
// 健康检查
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := redisClient.Ping(ctx).Err(); err != nil {
logrus.WithError(err).Warn("Redis初始化失败标记为不可用")
redisAvailable = false
return
}
redisAvailable = true
logrus.WithField("addr", addr).Info("Redis 连接已建立")
})
}
// GetRedis 获取全局Redis客户端可能返回nil当不可用时
func GetRedis() *redis.Client {
if redisClient == nil {
InitRedis()
}
if !redisAvailable {
return nil
}
return redisClient
}
// IsRedisAvailable 判断Redis是否可用
func IsRedisAvailable() bool {
if redisClient == nil {
InitRedis()
}
return redisAvailable
}
// RedisGetOrSet 通用Redis缓存获取或设置函数基于JSON序列化
// - ctx: 上下文
// - key: 缓存键
// - ttl: 过期时间
// - loader: 当缓存不存在时的加载函数(一般执行数据库查询)
// 返回:目标对象指针和错误
func RedisGetOrSet[T any](ctx context.Context, key string, ttl time.Duration, loader func() (*T, error)) (*T, error) {
// 如果Redis不可用则直接调用加载函数
if !IsRedisAvailable() {
return loader()
}
client := GetRedis()
if client == nil {
return loader()
}
// 先尝试从缓存读取
data, err := client.Get(ctx, key).Bytes()
if err == nil {
var out T
if uerr := json.Unmarshal(data, &out); uerr == nil {
return &out, nil
}
// 反序列化失败时视为未命中,继续加载
logrus.WithError(err).WithField("key", key).Warn("Redis缓存反序列化失败回退到loader")
} else if err != redis.Nil {
// 非空且非不存在的错误,记录告警但不中断
logrus.WithError(err).WithField("key", key).Warn("读取Redis缓存失败")
}
// 加载数据
val, lerr := loader()
if lerr != nil {
return nil, lerr
}
if val == nil {
return nil, nil
}
// 写回缓存(错误不影响主流程)
if b, merr := json.Marshal(val); merr == nil {
if serr := client.Set(ctx, key, b, ttl).Err(); serr != nil {
logrus.WithError(serr).WithField("key", key).Warn("写入Redis缓存失败")
}
}
return val, nil
}
// RedisDel 删除一个或多个Redis键当Redis不可用时静默返回
// - ctx: 上下文
// - keys: 需要删除的键名
func RedisDel(ctx context.Context, keys ...string) error {
// 如果Redis不可用则直接返回
if !IsRedisAvailable() {
return nil
}
client := GetRedis()
if client == nil {
return nil
}
if len(keys) == 0 {
return nil
}
if _, err := client.Del(ctx, keys...).Result(); err != nil {
logrus.WithError(err).WithField("keys", keys).Warn("删除Redis键失败")
return err
}
return nil
}