feat(db,docs): 优化数据库连接池与生产日志,新增 Web 访客端接入指南
This commit is contained in:
@@ -22,12 +22,16 @@ type ServerConfig struct {
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Password string
|
||||
Name string
|
||||
SSLMode string
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Password string
|
||||
Name string
|
||||
SSLMode string
|
||||
MaxOpenConns int
|
||||
MaxIdleConns int
|
||||
ConnMaxLifetime time.Duration
|
||||
ConnMaxIdleTime time.Duration
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
@@ -64,12 +68,16 @@ func Load() *Config {
|
||||
Mode: getEnv("GIN_MODE", "debug"),
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Host: getEnv("DB_HOST", "localhost"),
|
||||
Port: getEnv("DB_PORT", "5433"),
|
||||
User: getEnv("DB_USER", "postgres"),
|
||||
Password: getEnv("DB_PASSWORD", "postgres"),
|
||||
Name: getEnv("DB_NAME", "kefu_cloud"),
|
||||
SSLMode: getEnv("DB_SSLMODE", "disable"),
|
||||
Host: getEnv("DB_HOST", "localhost"),
|
||||
Port: getEnv("DB_PORT", "5433"),
|
||||
User: getEnv("DB_USER", "postgres"),
|
||||
Password: getEnv("DB_PASSWORD", "postgres"),
|
||||
Name: getEnv("DB_NAME", "kefu_cloud"),
|
||||
SSLMode: getEnv("DB_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvInt("DB_MAX_OPEN_CONNS", 50),
|
||||
MaxIdleConns: getEnvInt("DB_MAX_IDLE_CONNS", 10),
|
||||
ConnMaxLifetime: time.Duration(getEnvInt("DB_CONN_MAX_LIFETIME_MINUTES", 60)) * time.Minute,
|
||||
ConnMaxIdleTime: time.Duration(getEnvInt("DB_CONN_MAX_IDLE_MINUTES", 15)) * time.Minute,
|
||||
},
|
||||
JWT: func() JWTConfig {
|
||||
hours := getEnvInt("JWT_EXPIRE_HOURS", 24)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
@@ -10,21 +14,100 @@ import (
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
// DBPoolConfig 数据库连接池配置
|
||||
type DBPoolConfig struct {
|
||||
MaxOpenConns int
|
||||
MaxIdleConns int
|
||||
ConnMaxLifetime time.Duration
|
||||
ConnMaxIdleTime time.Duration
|
||||
}
|
||||
|
||||
// DefaultDBPoolConfig 返回连接池配置,优先读取环境变量,缺省提供生产合理的默认值
|
||||
func DefaultDBPoolConfig() DBPoolConfig {
|
||||
maxOpen := getEnvInt("DB_MAX_OPEN_CONNS", 50)
|
||||
maxIdle := getEnvInt("DB_MAX_IDLE_CONNS", 10)
|
||||
lifetimeMin := getEnvInt("DB_CONN_MAX_LIFETIME_MINUTES", 60)
|
||||
idleTimeMin := getEnvInt("DB_CONN_MAX_IDLE_MINUTES", 15)
|
||||
|
||||
if maxOpen <= 0 {
|
||||
maxOpen = 50
|
||||
}
|
||||
if maxIdle <= 0 {
|
||||
maxIdle = 10
|
||||
}
|
||||
if maxIdle > maxOpen {
|
||||
maxIdle = maxOpen
|
||||
}
|
||||
|
||||
return DBPoolConfig{
|
||||
MaxOpenConns: maxOpen,
|
||||
MaxIdleConns: maxIdle,
|
||||
ConnMaxLifetime: time.Duration(lifetimeMin) * time.Minute,
|
||||
ConnMaxIdleTime: time.Duration(idleTimeMin) * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyPoolConfig 将连接池参数应用至底层 *sql.DB
|
||||
func ApplyPoolConfig(sqlDB *sql.DB, cfg DBPoolConfig) {
|
||||
if sqlDB == nil {
|
||||
return
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||
sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
|
||||
sqlDB.SetConnMaxIdleTime(cfg.ConnMaxIdleTime)
|
||||
}
|
||||
|
||||
// BuildGormLogger 根据 GIN_MODE 区分日志策略:生产模式仅记录慢查询与错误,避免全量 SQL 撑满磁盘
|
||||
func BuildGormLogger() logger.Interface {
|
||||
mode := os.Getenv("GIN_MODE")
|
||||
if mode == "release" {
|
||||
return logger.New(
|
||||
log.New(os.Stdout, "\r\n", log.LstdFlags),
|
||||
logger.Config{
|
||||
SlowThreshold: 200 * time.Millisecond,
|
||||
LogLevel: logger.Warn,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
Colorful: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
return logger.Default.LogMode(logger.Info)
|
||||
}
|
||||
|
||||
func getEnvInt(key string, fallback int) int {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func InitDB(dsn string) {
|
||||
var err error
|
||||
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
Logger: BuildGormLogger(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("数据库连接失败: %v", err)
|
||||
}
|
||||
|
||||
sqlDB, err := DB.DB()
|
||||
if err != nil {
|
||||
log.Fatalf("获取底层数据库连接失败: %v", err)
|
||||
}
|
||||
|
||||
poolCfg := DefaultDBPoolConfig()
|
||||
ApplyPoolConfig(sqlDB, poolCfg)
|
||||
|
||||
err = Migrate(DB)
|
||||
if err != nil {
|
||||
log.Fatalf("数据库迁移失败: %v", err)
|
||||
}
|
||||
|
||||
log.Println("数据库迁移完成")
|
||||
log.Printf("数据库连接成功并完成迁移 [pool: max_open=%d, max_idle=%d, lifetime=%v, idletime=%v]",
|
||||
poolCfg.MaxOpenConns, poolCfg.MaxIdleConns, poolCfg.ConnMaxLifetime, poolCfg.ConnMaxIdleTime)
|
||||
}
|
||||
|
||||
func Migrate(db *gorm.DB) error {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func TestDefaultDBPoolConfig(t *testing.T) {
|
||||
// 默认兜底测试
|
||||
cfg := DefaultDBPoolConfig()
|
||||
if cfg.MaxOpenConns <= 0 || cfg.MaxIdleConns <= 0 {
|
||||
t.Fatalf("连接池默认值非法: %+v", cfg)
|
||||
}
|
||||
if cfg.MaxIdleConns > cfg.MaxOpenConns {
|
||||
t.Fatalf("MaxIdleConns 不能大于 MaxOpenConns: %+v", cfg)
|
||||
}
|
||||
|
||||
// 环境变量覆盖测试
|
||||
os.Setenv("DB_MAX_OPEN_CONNS", "120")
|
||||
os.Setenv("DB_MAX_IDLE_CONNS", "30")
|
||||
os.Setenv("DB_CONN_MAX_LIFETIME_MINUTES", "120")
|
||||
os.Setenv("DB_CONN_MAX_IDLE_MINUTES", "30")
|
||||
defer func() {
|
||||
os.Unsetenv("DB_MAX_OPEN_CONNS")
|
||||
os.Unsetenv("DB_MAX_IDLE_CONNS")
|
||||
os.Unsetenv("DB_CONN_MAX_LIFETIME_MINUTES")
|
||||
os.Unsetenv("DB_CONN_MAX_IDLE_MINUTES")
|
||||
}()
|
||||
|
||||
customCfg := DefaultDBPoolConfig()
|
||||
if customCfg.MaxOpenConns != 120 {
|
||||
t.Errorf("期望 MaxOpenConns=120, 实际=%d", customCfg.MaxOpenConns)
|
||||
}
|
||||
if customCfg.MaxIdleConns != 30 {
|
||||
t.Errorf("期望 MaxIdleConns=30, 实际=%d", customCfg.MaxIdleConns)
|
||||
}
|
||||
if customCfg.ConnMaxLifetime != 120*time.Minute {
|
||||
t.Errorf("期望 ConnMaxLifetime=120m, 实际=%v", customCfg.ConnMaxLifetime)
|
||||
}
|
||||
if customCfg.ConnMaxIdleTime != 30*time.Minute {
|
||||
t.Errorf("期望 ConnMaxIdleTime=30m, 实际=%v", customCfg.ConnMaxIdleTime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyPoolConfig(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:pool_test_%d?mode=memory&cache=shared", time.Now().UnixNano())), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("打开 sqlite 失败: %v", err)
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
t.Fatalf("获取 sql.DB 失败: %v", err)
|
||||
}
|
||||
|
||||
cfg := DBPoolConfig{
|
||||
MaxOpenConns: 25,
|
||||
MaxIdleConns: 5,
|
||||
ConnMaxLifetime: 30 * time.Minute,
|
||||
ConnMaxIdleTime: 10 * time.Minute,
|
||||
}
|
||||
ApplyPoolConfig(sqlDB, cfg)
|
||||
ApplyPoolConfig(nil, cfg) // 防空指针保护
|
||||
|
||||
stats := sqlDB.Stats()
|
||||
if stats.MaxOpenConnections != 25 {
|
||||
t.Errorf("期望 MaxOpenConnections=25, 实际=%d", stats.MaxOpenConnections)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildGormLogger(t *testing.T) {
|
||||
os.Setenv("GIN_MODE", "release")
|
||||
relLog := BuildGormLogger()
|
||||
if relLog == nil {
|
||||
t.Fatal("release logger 为空")
|
||||
}
|
||||
|
||||
os.Setenv("GIN_MODE", "debug")
|
||||
debLog := BuildGormLogger()
|
||||
if debLog == nil {
|
||||
t.Fatal("debug logger 为空")
|
||||
}
|
||||
os.Unsetenv("GIN_MODE")
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 避免测试时与真实 sql.DB 产生类型告警
|
||||
var _ *sql.DB
|
||||
var _ = logger.Warn
|
||||
}
|
||||
Reference in New Issue
Block a user