From 08eeeb2a40253cb37c0f77088fbc14abc5ee0383 Mon Sep 17 00:00:00 2001 From: hahwu <31872165+hahwu@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:36:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96goroutine,decorate=20part?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/conf/conf.go | 9 +-- src/server/conf/decorate/decorate_cfg.go | 8 +++ src/server/db/Mysql.go | 91 ++++++++++++++++++++---- src/server/game/external.go | 6 +- src/server/game/mod/decorate/Decorate.go | 30 +++++--- src/server/game/player_data.go | 9 ++- src/server/game/server_mod.go | 41 ++++++++--- src/server/main.go | 4 ++ src/server/test/fix_test.go | 79 +++++++++++++------- 9 files changed, 212 insertions(+), 65 deletions(-) diff --git a/src/server/conf/conf.go b/src/server/conf/conf.go index 0da14e78..df9225bd 100644 --- a/src/server/conf/conf.go +++ b/src/server/conf/conf.go @@ -17,8 +17,9 @@ var ( LittleEndian = false // skeleton conf - GoLen = 10000 - TimerDispatcherLen = 10000 - AsynCallLen = 10000 - ChanRPCLen = 10000 + // 增加 goroutine 相关配置,避免 "Too many goroutines" 错误 + GoLen = 50000 // 从 10000 增加到 50000,控制并发 goroutine 数量 + TimerDispatcherLen = 50000 // 从 10000 增加到 50000,定时器队列长度 + AsynCallLen = 50000 // 从 10000 增加到 50000,异步调用队列长度 + ChanRPCLen = 50000 // 从 10000 增加到 50000,RPC 通道长度 ) diff --git a/src/server/conf/decorate/decorate_cfg.go b/src/server/conf/decorate/decorate_cfg.go index 41ffefb3..e4d2bcab 100644 --- a/src/server/conf/decorate/decorate_cfg.go +++ b/src/server/conf/decorate/decorate_cfg.go @@ -213,3 +213,11 @@ func GetPartNumByAreaId(AreaId int) map[int]int { } return res } + +func GetAreaIdByIndoorId(IndoorId int) int { + data, err := gamedata.GetDataByIntKey(INDOOR_PROGRESS, IndoorId) + if err != nil { + return 0 + } + return gamedata.GetIntValue(data, "Scene") +} diff --git a/src/server/db/Mysql.go b/src/server/db/Mysql.go index fecbf2de..0b870d1d 100644 --- a/src/server/db/Mysql.go +++ b/src/server/db/Mysql.go @@ -1,6 +1,7 @@ package db import ( + "database/sql" "fmt" "reflect" "server/MergeConst" @@ -17,17 +18,50 @@ import ( ) var SqlDb *sqlx.DB -var sqlDbMu sync.Mutex +var sqlDbMu sync.RWMutex + +// GetDB 线程安全地获取数据库连接 +func GetDB() *sqlx.DB { + sqlDbMu.RLock() + defer sqlDbMu.RUnlock() + return SqlDb +} + +// GetDBOrPanic 获取数据库连接,如果为 nil 则记录错误 +func GetDBOrPanic() *sqlx.DB { + db := GetDB() + if db == nil { + log.Error("Database connection is nil, please check database initialization") + } + return db +} + +// EnsureDB 确保数据库连接可用,如果不可用则返回错误 +func EnsureDB() (*sqlx.DB, error) { + db := GetDB() + if db == nil { + return nil, fmt.Errorf("database connection is nil") + } + if err := db.Ping(); err != nil { + return nil, fmt.Errorf("database ping failed: %w", err) + } + return db, nil +} // 封装创建连接 func connectMySQL() (*sqlx.DB, error) { MysqlPwd, _ := GoUtil.Decrypt(conf.Server.MySqlPwd) - connect := fmt.Sprintf("%s:%s@(%s:%s)/%s", conf.Server.MySqlUsr, MysqlPwd, conf.Server.MySqlAddr, conf.Server.MySqlPort, conf.Server.DbName) + // 减少超时时间,避免长时间阻塞 + connect := fmt.Sprintf("%s:%s@(%s:%s)/%s?timeout=10s&readTimeout=15s&writeTimeout=15s&parseTime=true", conf.Server.MySqlUsr, MysqlPwd, conf.Server.MySqlAddr, conf.Server.MySqlPort, conf.Server.DbName) db, err := sqlx.Connect("mysql", connect) if err != nil { return nil, err } - db.SetMaxOpenConns(20) + // 增加最大连接数,减少连接等待时间 + db.SetMaxOpenConns(100) + db.SetMaxIdleConns(20) + db.SetConnMaxLifetime(30 * time.Minute) // 减少连接生命周期 + db.SetConnMaxIdleTime(5 * time.Minute) // 减少空闲时间 return db, nil } @@ -38,19 +72,29 @@ func InitDB() { log.Debug("connect mysql failed: %v", err) return } + sqlDbMu.Lock() SqlDb = db + sqlDbMu.Unlock() log.Debug("connect mysql success") // 定时检测与重连 go func() { - ticker := time.NewTicker(time.Second) + ticker := time.NewTicker(5 * time.Second) // 改为5秒检测一次,降低频率 defer ticker.Stop() for range ticker.C { - sqlDbMu.Lock() + sqlDbMu.RLock() cur := SqlDb - sqlDbMu.Unlock() - if cur == nil || cur.Ping() != nil { - log.Debug("mysql ping failed, start reconnect") + sqlDbMu.RUnlock() + + if cur == nil { + log.Debug("mysql connection is nil, start reconnect") + ReconnectDB() + continue + } + + // Ping 操作不持有锁,避免阻塞其他操作 + if err := cur.Ping(); err != nil { + log.Debug("mysql ping failed: %v, start reconnect", err) ReconnectDB() } } @@ -74,7 +118,12 @@ func ReconnectDB() { } func SeriesTransaction(sqlstrs []string, params [][]any) (err error) { - tx, err := SqlDb.Begin() + sqlDb := GetDB() + if sqlDb == nil { + return fmt.Errorf("database connection is nil") + } + + tx, err := sqlDb.Begin() if err != nil { log.Debug("Transaction failed, err:%v\n", err) return err @@ -379,20 +428,38 @@ func FormatAllMemLoadDb(u interface{}, tableName string, Exclude string) (err er } func GetServerData(d interface{}, Key string) (err error) { + sqlDb := GetDB() + if sqlDb == nil { + return fmt.Errorf("database connection is nil") + } sql := "select * from t_server_mod where `key` = ?" - err = SqlDb.Get(d, sql, Key) + err = sqlDb.Get(d, sql, Key) return } func SaveServerData(data *SqlServerModStruct) error { + sqlDb := GetDB() + if sqlDb == nil { + return fmt.Errorf("database connection is nil") + } sql := "update t_server_mod set `mData` = ? , `updateTime` = ? where `key` = ?" - _, err := SqlDb.Exec(sql, data.ModData, data.UpdataTime, data.Key) + _, err := sqlDb.Exec(sql, data.ModData, data.UpdataTime, data.Key) + return err +} + +func SaveServerDataWithTx(tx *sql.Tx, data *SqlServerModStruct) error { + sql := "update t_server_mod set `mData` = ? , `updateTime` = ? where `key` = ?" + _, err := tx.Exec(sql, data.ModData, data.UpdataTime, data.Key) return err } func InsertServerData(data *SqlServerModStruct) error { + sqlDb := GetDB() + if sqlDb == nil { + return fmt.Errorf("database connection is nil") + } sql := "insert into t_server_mod (`mData` , `updateTime` ,`key`) Values (?,?,?)" - _, err := SqlDb.Exec(sql, data.ModData, data.UpdataTime, data.Key) + _, err := sqlDb.Exec(sql, data.ModData, data.UpdataTime, data.Key) return err } diff --git a/src/server/game/external.go b/src/server/game/external.go index 6a9125e2..437eb1ef 100644 --- a/src/server/game/external.go +++ b/src/server/game/external.go @@ -219,8 +219,12 @@ func HandleClientReq(args []interface{}) { } p.(*Player).ProcessTrigger() execTime := time.Now().UnixMilli() - now - if execTime > int64(1) { + if execTime > int64(500) { log.Warn("uid : %d, func : %s, execTime : %d ms", p.(*Player).M_DwUin, m.GetFunc(), execTime) + p.(*Player).TeLog("Long_Method_Log", map[string]interface{}{ + "method_name": m.GetFunc(), + "exec_time": execTime, + }) } } } diff --git a/src/server/game/mod/decorate/Decorate.go b/src/server/game/mod/decorate/Decorate.go index 903d66dc..a62ff232 100644 --- a/src/server/game/mod/decorate/Decorate.go +++ b/src/server/game/mod/decorate/Decorate.go @@ -43,7 +43,7 @@ func (d *Decorate) InitData() { } for k := range d.PartCost { AreaId := decorateCfg.GetAreaId(k) - if AreaId != d.AreaId { + if AreaId < d.AreaId { delete(d.PartCost, k) } } @@ -70,6 +70,15 @@ func (d *Decorate) Decorate(areaId int, decorateId int) ([]*item.Item, error) { d.AreaId++ d.Progress = 0 d.FinishList = make(map[int]struct{}) + for k := range d.PartCost { + AreaId := decorateCfg.GetAreaId(k) + if AreaId < d.AreaId { + delete(d.PartCost, k) + } + } + if len(d.PartCost) == 0 { + d.initPartCost(d.AreaId) + } } d.DecorateNum++ @@ -101,7 +110,7 @@ func (d *Decorate) GetDecorateCostItem(AreaId, DecorateId int, DecorateOffIsExis } for k := range d.PartCost { AreaId := decorateCfg.GetAreaId(k) - if AreaId != d.AreaId { + if AreaId < d.AreaId { delete(d.PartCost, k) } } @@ -167,17 +176,16 @@ func (d *Decorate) DecorateAll(Star int, DecorateOffIsExist bool) ([]*item.Item, d.AreaId++ d.Progress = 0 d.FinishList = make(map[int]struct{}) - } - for k := range d.PartCost { - AreaId := decorateCfg.GetAreaId(k) - if AreaId != d.AreaId { - delete(d.PartCost, k) + for k := range d.PartCost { + AreaId := decorateCfg.GetAreaId(k) + if AreaId < d.AreaId { + delete(d.PartCost, k) + } + } + if len(d.PartCost) == 0 { + d.initPartCost(d.AreaId) } } - if len(d.PartCost) == 0 { - d.initPartCost(d.AreaId) - } - SubItems = append(SubItems, item.NewItem(item.ITEM_STAR_ID, SubItem)) return SubItems, AddItem, Num, DecorateList, Log, PetExp } diff --git a/src/server/game/player_data.go b/src/server/game/player_data.go index 8a25bec3..b8f4ee50 100644 --- a/src/server/game/player_data.go +++ b/src/server/game/player_data.go @@ -260,7 +260,7 @@ func (p *Player) InitPlayer(UserName string) error { ChessMod := p.PlayMod.getChessMod() ChargeMod.FixBug(ChessMod.GetEmitList()) p.FixOrderBug() - p.FixDecorate() + //p.FixDecorate() return nil } @@ -269,8 +269,10 @@ func (p *Player) OrderShip() { if err != nil { return } + // 避免为每个订单创建 goroutine,改为批量处理或同步处理 for _, OrderInfo := range OrderList { - go p.TriggerShippingOrderOrigin(&msg.ReqShippingOrder{ + // 直接同步处理,避免创建过多 goroutine + p.TriggerShippingOrderOrigin(&msg.ReqShippingOrder{ OrderSn: OrderInfo.OrderId, }) } @@ -1207,7 +1209,8 @@ func (p *Player) DispatcherHandle() { if msg != nil { p.wg.Done() log.Debug("player %d recive msg %v", p.M_DwUin, msg) - go p.HandleMsg(msg.Clone()) + // 直接在当前 goroutine 中处理,避免创建过多 goroutine + p.HandleMsg(msg.Clone()) } } } diff --git a/src/server/game/server_mod.go b/src/server/game/server_mod.go index eb1ce64a..cddcee79 100644 --- a/src/server/game/server_mod.go +++ b/src/server/game/server_mod.go @@ -43,8 +43,9 @@ func (s *ServerMod) init() { s.handler = make(map[int]interface{}) s.update = false s.LoadData() + // 直接调用 SaveData,不要创建新的 goroutine s.mDispatr.AfterFunc(time.Duration(PER_SAVE_TIME)*time.Second, func() { - go s.SaveData() + s.SaveData() }) go func() { defer func() { @@ -96,8 +97,9 @@ func (s *ServerMod) Send(msg *msg.Msg) { // 同步请求 func (s *ServerMod) Call(m *msg.Msg) (interface{}, error) { - responseChan := make(chan interface{}) - errorChan := make(chan error) + // 使用带缓冲的 channel 避免 goroutine 在超时时泄漏 + responseChan := make(chan interface{}, 1) + errorChan := make(chan error, 1) go func() { defer func() { @@ -129,8 +131,9 @@ func (s *ServerMod) Call(m *msg.Msg) (interface{}, error) { // mysql 保存消息 func (s *ServerMod) SaveData() { + // 直接在定时器回调中执行,不创建新的 goroutine 避免泄漏 s.mDispatr.AfterFunc(time.Duration(PER_SAVE_TIME+GoUtil.RandNum(5, 10))*time.Second, func() { - go s.SaveData() + s.SaveData() }) DbData := db.SqlServerModStruct{} DbData.Key = s.key @@ -139,23 +142,41 @@ func (s *ServerMod) SaveData() { DbData.ModData, err = GoUtil.GobMarshal(s.data) if err != nil { log.Error("SaveData Marshal failed,Mod Key: %s err:%v", s.key, err) + return } // log.Debug("SaveData Marshal success,Mod Key: %s", s.key) - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + + // 使用线程安全的方式获取数据库连接 + sqlDb := db.GetDB() + if sqlDb == nil { + log.Error("SaveData failed, database connection is nil, Mod Key: %s", s.key) + return + } + + // 先测试连接是否可用 + if err := sqlDb.Ping(); err != nil { + log.Error("SaveData failed, database ping error, Mod Key: %s err:%v", s.key, err) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() txOptions := &sql.TxOptions{} - tx, err := db.SqlDb.BeginTx(ctx, txOptions) + tx, err := sqlDb.BeginTx(ctx, txOptions) if err != nil { - log.Error("SaveData sql begin tx failed,Mod Key: %s err:%v", s.key, err) + log.Error("SaveData sql begin tx failed,Mod Key: %s err:%v, data size: %d bytes", s.key, err, len(DbData.ModData)) return } - err = db.SaveServerData(&DbData) + err = db.SaveServerDataWithTx(tx, &DbData) if err != nil { tx.Rollback() - log.Error("SaveData sql exec ,Mod Key: %s err:%v", s.key, err) + log.Error("SaveData sql exec failed,Mod Key: %s err:%v", s.key, err) return } - tx.Commit() + err = tx.Commit() + if err != nil { + log.Error("SaveData sql commit failed,Mod Key: %s err:%v", s.key, err) + } } func (s *ServerMod) LoadData() { diff --git a/src/server/main.go b/src/server/main.go index 92ba76e1..bb9e2371 100644 --- a/src/server/main.go +++ b/src/server/main.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" _ "net/http/pprof" + "runtime" "runtime/debug" "server/conf" "server/game" @@ -13,6 +14,9 @@ import ( ) func main() { + // 设置使用所有 CPU 核心 + runtime.GOMAXPROCS(runtime.NumCPU()) + lconf.LogLevel = conf.Server.LogLevel lconf.LogPath = conf.Server.LogPath lconf.LogFlag = conf.LogFlag diff --git a/src/server/test/fix_test.go b/src/server/test/fix_test.go index 0a4da88d..ea90b595 100644 --- a/src/server/test/fix_test.go +++ b/src/server/test/fix_test.go @@ -1,6 +1,9 @@ package test import ( + "fmt" + decorateCfg "server/conf/decorate" + "server/db" "server/game" "server/pkg/github.com/name5566/leaf/log" "testing" @@ -12,7 +15,7 @@ func TestFixDecorate(t *testing.T) { p.FixDecorate() // - p.InitPlayer("202601K111") + p.InitPlayer("dee8eeb83ea3c54e48427b7ff20066fb") p.FixDecorate() DecorateMod := p.GetDecorateMod() @@ -22,28 +25,56 @@ func TestFixDecorate(t *testing.T) { } func TestFixUserData(t *testing.T) { + // 确保数据库已初始化 + if db.SqlDb == nil { + db.InitDB() + // 等待初始化完成 + for i := 0; i < 10; i++ { + if db.SqlDb != nil { + break + } + log.Warn("Waiting for database initialization...") + // time.Sleep(100 * time.Millisecond) + } + } + + if db.SqlDb == nil { + t.Fatal("Database initialization failed") + } + log.Warn("hello world") - // type account struct { - // Account string `db:"user_name"` - // } - // var accounts []account - // err := db.SqlDb.Select(&accounts, "SELECT `user_name` FROM t_account ") - // if err != nil { - // t.Errorf("Failed to fetch accounts: %v", err) - // return - // } - // for _, acc := range accounts { - // p := new(game.Player) - // p.InitPlayer(acc.Account) - // DecorateMod := p.GetDecorateMod() - // if DecorateMod.PartCost == nil { - // continue - // } - // for k := range DecorateMod.PartCost { - // AreaId := decorateCfg.GetAreaId(k) - // if AreaId != DecorateMod.AreaId { - // fmt.Printf("Fixing account: %s, PartId: %d, OldAreaId: %d, NewAreaId: %d\n", acc.Account, k, AreaId, DecorateMod.AreaId) - // } - // } - // } + type account struct { + Account string `db:"user_name"` + } + var accounts []account + sqlDb := db.GetDB() // 使用线程安全的方式获取连接 + if sqlDb == nil { + t.Fatal("Database connection is nil") + } + err := sqlDb.Select(&accounts, "SELECT `user_name` FROM t_account order by auto_id") + if err != nil { + t.Errorf("Failed to fetch accounts: %v", err) + return + } + i := 0 + for _, acc := range accounts { + i++ + fmt.Printf("Fixing account %d/%d: %s\n", i, len(accounts), acc.Account) + account := acc.Account + p := new(game.Player) + p.InitPlayer(account) + DecorateMod := p.GetDecorateMod() + if DecorateMod.PartCost == nil { + return + } + for k := range DecorateMod.PartCost { + AreaId := decorateCfg.GetAreaIdByIndoorId(k) + if AreaId < DecorateMod.AreaId { + log.Debug("Fixing account: %s, PartId: %d, OldAreaId: %d, NewAreaId: %d\n", account, k, AreaId, DecorateMod.AreaId) + } + } + p.Stop() + p = nil + } + log.Debug("All accounts fixed") }