Go读取MySQL计费表缓存仅返回单行问题排查与方案评估
问题排查与优化建议:Go协程缓存lwratecarddefinition表数据异常
一、为什么只返回单行数据?
核心问题出在数据存储结构和查询逻辑上:
- 你定义的
lwratecardefinition是单个结构体,而非切片([]Lwratecardefinition)。在getRatecard()的循环中,每次results.Next()都会覆盖这个结构体的内容,最终返回的只是数据库表的最后一行数据。 - 还有个隐藏坑:你的结构体字段类型和数据库表字段不匹配(比如表中
SERVICEID是varchar(10),但结构体里定义成了int),这会导致扫描数据时出错,甚至可能丢失部分数据。
二、修复代码:加载全表数据到缓存
首先修改结构体和全局变量的定义,改用切片存储所有数据:
// dbfunction.go import "sync" type Lwratecardefinition struct { Ratecardid int `json:"ratecardid"` Ratecardgroupid int `json:"ratecardgroupid"` Serviceid string `json:"serviceid"` // 改成string匹配表的varchar类型 Usagetypeid string `json:"usagetypeid"` // 同理匹配表字段类型 Currencycode string `json:"currencycode"` Ledgerid string `json:"ledgerid"` Pulse string `json:"pulse"` // 表中是varchar,先匹配类型避免扫描错误 Specialpulse string `json:"specialpulse"` Normalrate string `json:"normalrate"` Offpeakset string `json:"offpeakset"` Offpeakrate string `json:"offpeakrate"` Peakrate string `json:"peakrate"` Ondemandset string `json:"ondemandset"` Ondemandrate string `json:"ondemandrate"` } var lwratecardefinition []Lwratecardefinition // 改成切片存储全表数据 var ratecardMutex sync.RWMutex // 新增读写锁,解决并发读写冲突
然后修改getRatecard()函数,正确收集所有行数据:
func getRatecard() []Lwratecardefinition { // 建议初始化全局连接池,不要每次打开关闭连接(后面会说优化) db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/bcsdb") if err != nil { panic(err.Error()) } defer db.Close() rows, err := db.Query("SELECT ratecardid, ratecardgroupid, serviceid, usagetypeid, currencycode, ledgerid, pulse, specialpulse, normalrate, offpeakset, offpeakrate, peakrate, ondemandset, ondemandrate FROM lwratecarddefinition") if err != nil { panic(err.Error()) } defer rows.Close() // 必须关闭rows,避免连接泄漏 var ratecards []Lwratecardefinition for rows.Next() { var rc Lwratecardefinition // 扫描字段要和结构体类型严格匹配 err := rows.Scan( &rc.Ratecardid, &rc.Ratecardgroupid, &rc.Serviceid, &rc.Usagetypeid, &rc.Currencycode, &rc.Ledgerid, &rc.Pulse, &rc.Specialpulse, &rc.Normalrate, &rc.Offpeakset, &rc.Offpeakrate, &rc.Peakrate, &rc.Ondemandset, &rc.Ondemandrate, ) if err != nil { // 不要直接panic,建议用日志记录错误,避免程序崩溃 fmt.Printf("扫描行数据失败: %v\n", err) continue } ratecards = append(ratecards, rc) } // 检查遍历过程中是否出现错误 if err := rows.Err(); err != nil { fmt.Printf("遍历行数据出错: %v\n", err) } return ratecards }
最后修改定时更新协程,加上锁保证并发安全:
// main.go // 初始加载缓存 ratecardMutex.Lock() lwratecardefinition = getRatecard() ratecardMutex.Unlock() // 改成1分钟更新一次(符合你的目标) go func() { ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() for range ticker.C { newRatecards := getRatecard() ratecardMutex.Lock() lwratecardefinition = newRatecards ratecardMutex.Unlock() fmt.Printf("缓存更新完成,共加载%d行数据\n", len(lwratecardefinition)) } }()
三、实现按Ratecardgroupid和Serviceid匹配获取Pulse的逻辑
新增一个查询函数,用读锁保证并发安全:
func GetPulseByGroupAndService(groupID int, serviceID string) (string, bool) { ratecardMutex.RLock() defer ratecardMutex.RUnlock() for _, rc := range lwratecardefinition { if rc.Ratecardgroupid == groupID && rc.Serviceid == serviceID { return rc.Pulse, true } } return "", false // 未找到匹配项 }
四、方案合理性评估
你的核心思路(定时拉取全表做内存缓存减少DB调用)是合理的,尤其适合计费这类数据变更不频繁的场景,但当前实现有几个关键优化点:
- 连接池复用:原来每次
getRatecard()都新建连接,开销很大,应该初始化全局DB连接池。 - 并发安全:协程更新缓存时,其他goroutine读取可能遇到数据不一致,必须用读写锁保护。
- 错误容错:直接panic会导致程序崩溃,建议用日志库记录错误,拉取失败时保留旧缓存,避免服务不可用。
- 缓存策略:如果表数据量很大,全量拉取可能有性能问题,可以考虑增量更新(比如记录最后更新时间,只拉取变更数据),但中小规模计费表用全量拉取是简单有效的方案。
五、额外优化建议
- 把数据库连接信息放到配置文件(如
config.yaml),不要硬编码在代码里。 - 初始化全局DB连接池,避免重复建立连接:
之后var db *sql.DB func init() { var err error db, err = sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/bcsdb") if err != nil { panic(err.Error()) } // 设置连接池参数 db.SetMaxOpenConns(10) db.SetMaxIdleConns(5) }getRatecard()直接用全局的db即可,不用每次打开关闭。 - 用专业日志库(如zap、logrus)代替
fmt.Println,方便线上排查问题。
内容的提问来源于stack exchange,提问作者Rakesh Yadav
相关产品推荐
相关产品推荐

