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

Go大量协程查询MySQL触发Panic的问题解决咨询

问题:高并发goroutine操作MySQL触发panic

当goroutine数量设置为1000时,Go程序触发panic,数量少于1000时正常运行。使用SQLite适配器时1000个goroutine可正常工作,但MySQL不行。环境:Go 1.18.3 windows/amd64,github.com/go-sql-driver/mysql v1.6.0。

原代码

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "time"
)

func routine(db *sql.DB, id int, ch chan<- string) {
    update, _ := db.Prepare("SELECT salary FROM users WHERE id = (?)")
    defer update.Close()
    var salary string
    update.QueryRow(id).Scan(&salary)
    ch <- salary
}

func main() {
    n := 1000

    ch := make(chan string)
    list := make([]string, n)

    db, _ := sql.Open("mysql", "root:root@/database")
    db.SetConnMaxLifetime(time.Minute * 3)
    defer db.Close()

    for i := 0; i < n; i++ {
        go routine(db, 123, ch)
    }
    for i := 0; i < n; i++ {
        list[i] = <-ch
    }
}

错误信息

panic: runtime error: invalid memory address or nil pointer dereference
        panic: runtime error: invalid memory address or nil pointer dereference

goroutine 948 [running]:
database/sql.(*Stmt).Close(0x0)
        C:/Program Files/Go/src/database/sql/sql.go:2872 +0x37
panic({0x4e8b20, 0x689240})
        C:/Program Files/Go/src/runtime/panic.go:838 +0x207
database/sql.(*Stmt).QueryContext(0x0, {0x5788e8, 0xc000018050}, {0xc000d5bf60, 0x1, 0x1})
        C:/Program Files/Go/src/database/sql/sql.go:2767 +0x82
database/sql.(*Stmt).QueryRowContext(0x0?, {0x5788e8?, 0xc000018050?}, {0xc000d5bf60?, 0x27?, 0x0?})
        C:/Program Files/Go/src/database/sql/sql.go:2845 +0x2c
database/sql.(*Stmt).QueryRow(...)
        C:/Program Files/Go/src/database/sql/sql.go:2867
main.routine(0x0?, 0x0?, 0x0?)
        C:/Users/me/Desktop/go/5. MySQL/main.go:13 +0xfb
created by main.main
        C:/Users/me/Desktop/go/5. MySQL/main.go:28 +0xba

解决方案

1. 根本原因

panic直接原因是db.Prepare返回nil(伴随错误),但代码忽略了错误,后续调用update.QueryRow和defer update.Close时操作nil指针。当goroutine数量超过MySQL连接池处理能力时,Prepare会因无法获取连接而失败,返回nil和错误。

2. 具体修复措施

  • 必须检查所有数据库操作的错误:忽略错误会导致nil指针操作,这是panic的核心原因。
  • 调整数据库连接池参数:根据并发量设置合适的最大打开连接数、最大空闲连接数,避免连接耗尽。
  • 复用Prepared Statement:*sql.Stmt是并发安全的,无需每个goroutine都调用Prepare,提前在main中准备好可大幅降低资源消耗。

3. 修改后的代码

package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
    "time"
)

func routine(stmt *sql.Stmt, id int, ch chan<- string) {
    var salary string
    // 检查QueryRow和Scan的错误
    err := stmt.QueryRow(id).Scan(&salary)
    if err != nil {
        ch <- fmt.Sprintf("error: %v", err)
        return
    }
    ch <- salary
}

func main() {
    n := 10000 // 支持10000个goroutine

    ch := make(chan string, n) // 带缓冲的通道避免阻塞
    list := make([]string, n)

    // 打开数据库时检查错误
    db, err := sql.Open("mysql", "root:root@/database")
    if err != nil {
        panic(fmt.Sprintf("failed to open db: %v", err))
    }
    // 设置连接池参数
    db.SetMaxOpenConns(200)    // 最大打开连接数,根据MySQL服务器配置调整
    db.SetMaxIdleConns(100)    // 最大空闲连接数
    db.SetConnMaxLifetime(time.Minute * 3)
    db.SetConnMaxIdleTime(time.Minute * 1)
    defer db.Close()

    // 提前准备Statement,复用给所有goroutine
    stmt, err := db.Prepare("SELECT salary FROM users WHERE id = ?")
    if err != nil {
        panic(fmt.Sprintf("failed to prepare stmt: %v", err))
    }
    defer stmt.Close()

    for i := 0; i < n; i++ {
        go routine(stmt, 123, ch)
    }
    for i := 0; i < n; i++ {
        list[i] = <-ch
    }
}

补充说明

  • 带缓冲的通道ch := make(chan string, n)可以避免goroutine因通道阻塞而堆积,提升并发性能。
  • SetMaxOpenConns的值不要超过MySQL服务器的max_connections配置(默认151),可根据实际需求调整。
  • 所有数据库操作的错误都必须处理,避免因隐藏错误导致程序异常。

内容的提问来源于stack exchange,提问作者Yoskutik

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 20:48:19