fix(stability): 修复慢查询雪崩隐患、系统配置死锁与提号翻页错误
- 消除 system_configs 写锁争抢:使用读写锁与启动预热将默认配置初始化收敛为单例,杜绝读请求开事务导致的 Lock wait timeout exceeded - 强化财务看板缓存:将 Redis 缓存有效期由 1 分钟延长为 5 分钟,接入 singleflight.Group 防并发击穿 - 修复提号列表 GORM 翻页 Bug:分离 Count 查询与列表查询构建,避免重复 Join 导致的 Error 1066 - 生产环境资源加固:docker-compose.prod.yml 中 MySQL 资源限制提升至 2.0 核 CPU、2G 内存 - 补充 systemconfig 并发测试与 pickup 分页 Count 回归测试
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
"hfb_sys/backend/internal/model"
|
||||
@@ -15,7 +16,9 @@ import (
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
initMu sync.RWMutex
|
||||
initialized bool
|
||||
}
|
||||
|
||||
type AuditMeta = auditlog.Meta
|
||||
@@ -88,8 +91,30 @@ func NewRepository(db *gorm.DB) *Repository {
|
||||
return &Repository{db: db}
|
||||
}
|
||||
|
||||
func (r *Repository) List(ctx context.Context) ([]ConfigDTO, error) {
|
||||
// EnsureDefaults 仅在服务启动或首次访问时初始化默认系统配置。
|
||||
// 使用读写锁保证进程生命周期内只真正落库初始化一次,避免每个读请求重复开事务写锁争抢。
|
||||
func (r *Repository) EnsureDefaults(ctx context.Context) error {
|
||||
r.initMu.RLock()
|
||||
if r.initialized {
|
||||
r.initMu.RUnlock()
|
||||
return nil
|
||||
}
|
||||
r.initMu.RUnlock()
|
||||
|
||||
r.initMu.Lock()
|
||||
defer r.initMu.Unlock()
|
||||
if r.initialized {
|
||||
return nil
|
||||
}
|
||||
if err := r.ensureDefaults(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
r.initialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) List(ctx context.Context) ([]ConfigDTO, error) {
|
||||
if err := r.EnsureDefaults(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rows []model.SystemConfig
|
||||
@@ -104,7 +129,7 @@ func (r *Repository) List(ctx context.Context) ([]ConfigDTO, error) {
|
||||
}
|
||||
|
||||
func (r *Repository) FindValue(ctx context.Context, key string) (string, error) {
|
||||
if err := r.ensureDefaults(ctx); err != nil {
|
||||
if err := r.EnsureDefaults(ctx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var row model.SystemConfig
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package systemconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func newTestSystemConfigRepo(t *testing.T) (*Repository, *gorm.DB) {
|
||||
t.Helper()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("连接测试数据库失败: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.SystemConfig{}); err != nil {
|
||||
t.Fatalf("自动迁移 SystemConfig 失败: %v", err)
|
||||
}
|
||||
return NewRepository(db), db
|
||||
}
|
||||
|
||||
func TestRepositoryEnsureDefaultsConcurrency(t *testing.T) {
|
||||
repo, _ := newTestSystemConfigRepo(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// 并发 20 个 goroutine 调用 EnsureDefaults
|
||||
var wg sync.WaitGroup
|
||||
errCh := make(chan error, 20)
|
||||
for i := 0; i < 20; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := repo.EnsureDefaults(ctx); err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
|
||||
for err := range errCh {
|
||||
if err != nil {
|
||||
t.Fatalf("并发 EnsureDefaults 失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 再次并发调用 FindValue,验证 initialized 为 true 时直接走只读路径
|
||||
for i := 0; i < 20; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
val, err := repo.FindValue(ctx, "handoff.owner_submit_timeout_minutes")
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if val != "30" {
|
||||
errCh <- fmt.Errorf("got %s, want 30", val)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
Reference in New Issue
Block a user