You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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调用)是合理的,尤其适合计费这类数据变更不频繁的场景,但当前实现有几个关键优化点:

  1. 连接池复用:原来每次getRatecard()都新建连接,开销很大,应该初始化全局DB连接池。
  2. 并发安全:协程更新缓存时,其他goroutine读取可能遇到数据不一致,必须用读写锁保护。
  3. 错误容错:直接panic会导致程序崩溃,建议用日志库记录错误,拉取失败时保留旧缓存,避免服务不可用。
  4. 缓存策略:如果表数据量很大,全量拉取可能有性能问题,可以考虑增量更新(比如记录最后更新时间,只拉取变更数据),但中小规模计费表用全量拉取是简单有效的方案。

五、额外优化建议

  • 把数据库连接信息放到配置文件(如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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 19:28:12