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
相关产品推荐
相关产品推荐

