你在做什么?
在至少有两个打开的数据库连接池上执行一个预准备的SQL查询,总是会在两个连接上发送Prepare
命令。
为了重现错误,首先在你的MySQL数据库中启用一般日志记录:
SET GLOBAL general_log = 'ON';
SET GLOBAL log_output = 'table';
现在,连接到你的mysql数据库:
package main
import (
"sync"
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
db, err := sql.Open("mysql", "***:***@/***")
if err != nil {
panic(err.Error())
}
defer db.Close()
db.SetMaxIdleConns(10)
db.SetMaxOpenConns(10)
然后运行一些并发的SQL命令,以确保连接池中有多个打开的连接:
var wg = &sync.WaitGroup{}
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
var stmt, err = db.Prepare("SELECT * FROM mytable WHERE id = ?")
if err != nil {
panic(err.Error())
}
defer stmt.Close()
_, err = stmt.Exec(1)
if err != nil {
panic(err.Error())
}
wg.Done()
}(i)
}
wg.Wait()
现在我们至少有两个打开的连接,在数据库上运行一个预准备和执行命令:
var stmt, err = db.Prepare("SELECT * FROM mytable WHERE id = ?")
if err != nil {
panic(err.Error())
}
defer stmt.Close()
_, err = stmt.Exec(2)
if err != nil {
panic(err.Error())
}
你期望看到什么?
mysql> SELECT thread_id,command_type,argument FROM mysql.general_log;
+-----------+--------------+------------------------------------------+
| thread_id | command_type | argument |
+-----------+--------------+------------------------------------------+
| 78 | Prepare | SELECT * FROM mytable WHERE id = ? |
| 77 | Prepare | SELECT * FROM mytable WHERE id = ? |
| 78 | Execute | SELECT * FROM mytable WHERE id = 1 |
| 77 | Execute | SELECT * FROM mytable WHERE id = 1 |
| 78 | Close stmt |
| 77 | Close stmt |
| 78 | Prepare | SELECT * FROM mytable WHERE id = ? |
| 78 | Execute | SELECT * FROM mytable WHERE id = 2 |
| 78 | Close stmt |
| 78 | Quit |
| 77 | Quit |
+-----------+--------------+------------------------------------------+
你看到的是什么?
mysql> SELECT thread_id,command_type,argument FROM mysql.general_log;
+-----------+--------------+------------------------------------------+
| thread_id | command_type | argument |
+-----------+--------------+------------------------------------------+
| 38 | Prepare | SELECT * FROM mytable WHERE id = ? |
| 38 | Execute | SELECT * FROM mytable WHERE id = 1 |
| 38 | Close stmt |
| 37 | Prepare | SELECT * FROM mytable WHERE id = ? |
| 38 | Prepare | SELECT * FROM mytable WHERE id = ? |
| 38 | Execute | SELECT * FROM mytable WHERE id = 1 |
| 38 | Close stmt |
| 37 | Close stmt |
| 37 | Prepare | SELECT * FROM mytable WHERE id = ? |
| 38 | Prepare | SELECT * FROM mytable WHERE id = ? |
| 38 | Execute | SELECT * FROM mytable WHERE id = 2 |
| 38 | Close stmt |
| 38 | Quit |
| 37 | Close stmt |
| 37 | Quit |
+-----------+--------------+------------------------------------------+
系统详细信息
mysql Ver 8.0.16 for Linux on x86_64 (MySQL Community Server - GPL)
go version go1.12.5 darwin/amd64
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/najani/Library/Caches/go-build"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/najani/go"
GOPROXY=""
GORACE=""
GOROOT="/usr/local/go"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD=""
GOROOT/bin/go version: go version go1.12.5 darwin/amd64
GOROOT/bin/go tool compile -V: compile version go1.12.5
uname -v: Darwin Kernel Version 18.2.0: Fri Oct 5 19:41:49 PDT 2018; root:xnu-4903.221.2~2/RELEASE_X86_64
ProductName: Mac OS X
ProductVersion: 10.14.1
BuildVersion: 18B75
lldb --version: lldb-1000.11.38.2
Swift-4.2
8条答案
按热度按时间aamkag611#
经过调查,我们发现
connStmt
函数从未找到prepare
命令发送的连接,因为db.conn
函数调用实现为后进先出(LIFO)模式,这意味着在prepare
和execute
之间没有发生其他事情时,将返回用于prepare
的最后一个连接。参考:https://github.com/golang/go/blob/master/src/database/sql/sql.go#L2461-L2478
我们通过实现一个遍历
DB.freeConn
和Stmt.css
以查找匹配项的函数来测试这个假设。这解决了这个问题,我们将提交一个 PR。qvk1mo1f2#
https://golang.org/cl/179298提到了这个问题:
database/sql: run prepare & execute commands on the same conn from DB pool
oxiaedzo3#
@nishaad78 我可以为您的Go1.14版本的CL进行审查(我们仍在冻结1.13版本)。
对于您的用例,您是否考虑使用专用的Conn()并在单个Conn上调用prepare和execute?
31moq8wy4#
是的,我们打算为我们的用例切换到专用连接。然而,我们仍然认为这种行为应该修复,而不需要使用一个,因为Prepare()可以在数据库池上运行...。
发件人:Daniel Theophanes notifications@github.com
发送时间:星期四,2019年5月30日,上午2:31:57
收件人:golang/go
抄送:Nishaad Ajani;提及主题:关于在具有多个打开连接的数据库池上执行语句时,数据库/sql的prepare命令总是再次发送的问题(#32298) @nishaad78< https://github.com/nishaad78 > 我可以查看Go1.14的CL(我们仍在1.13冻结中)。对于您的用例,您是否考虑使用专用Conn()并在一个Conn上调用prepare和execute? — 您收到此邮件是因为有人提到了您。直接回复此电子邮件,在GitHub上查看<#32298?email_source=notifications&email_token=AB7ZYNLVOXQTWMVKIXYSYTTPX3DZ3A5CNFSM4HQI6NUKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODWQHQ2Q#issuecomment-497055850>,或者静音线程< https://github.com/notifications/unsubscribe-auth/AB7ZYNIYC6ADB3BHBAOSUWTPX3DZ3ANCNFSM4HQI6NUA >。
vi4fp9gy5#
@kardianos 1.12版本的这个是否可以作为小版本?
gmxoilav6#
@nishaad78 假设它已经合并,不,它仍然不会符合小版本发布的要求。
hgtggwj07#
请在大型(64+核心)机器上测试可扩展性。
参见$x_{1e0f1}x$和$x_{1e1f1}x$
js4nwp548#
@nishaad78 我很高兴你选择转向专用连接。在整个SQL包代码库中,我认为语句子池是最具有挑战性的。你的CL可能没问题,但我高度怀疑我能否合并或维护它。
编辑:澄清一下,除非提出一个更简单、非常直接的解决方案,否则这很可能会被拒绝。