在Go语言中向MySQL插入行非常缓慢?

3

我一直在将一个旧的PHP系统重写为Go,希望获得一些性能提升,但是我并没有得到。问题似乎出现在我向Mysql插入数据时。

在PHP中,处理CSV文件、进行哈希处理并将约10k行数据插入MySQL需要40秒(未优化的代码)。

另一方面,仅进行相同的插入10k(空)行数据,去掉任何处理的Go需要110秒。

两个测试都在同一台机器上运行,我使用了go-mysql-driver。

现在看一下Go代码:

这是非常简化的代码,但它仍然需要将近2分钟的时间,而PHP则可以在不到一半的时间内完成。

db := GetDbCon()
defer db.Close()

stmt, _ := db.Prepare("INSERT INTO ticket ( event_id, entry_id, column_headers, column_data, hash, salt ) VALUES ( ?, ?, ?, ?, ?, ? )")

for i := 0; i < 10000; i++{
    //CreateTicket(columns, line, storedEvent)
    StoreTicket(models.Ticket{int64(0), storedEvent.Id, int64(i),
                    "", "", "", "", int64(0), int64(0)}, *stmt)
}

//Extra functions
func StoreTicket(ticket models.Ticket, stmt sql.Stmt){
    stmt.Exec(ticket.EventId, ticket.EntryId, ticket.ColumnHeaders, ticket.ColumnData, ticket.Hash, ticket.Salt)
}

func GetDbCon() (sql.DB) {
    db, _ := sql.Open("mysql", "bla:bla@/bla")

    return *db
}

性能分析结果

这是我的代码问题,还是go-mysql-driver的问题?或者这很正常,只是PHP在插入记录方面非常快吗?

==编辑==

根据要求,我使用tcpdump记录了PHP和Go的运行情况:

比较这两个日志,我很难得出任何结论,因为它们似乎都在发送相同大小的数据包。但是,对于Go(~110),mysql似乎需要花费近两倍的时间来处理请求,而对于PHP(~44),mysql似乎等待更长时间才会再次发送新请求(不过差异很小)。


同一台机器没问题,但是数据库和表是否相同?你定义了额外的索引吗?只是猜测。 - Litmus
它们在不同的数据库上运行,命名上有轻微差异(结构相同),但索引完全相同,并且在每次测试之前都会截断数据库。 - Rohan
从Github获取go1.1.2 linux/amd和最新的go-mysql-driver文件。 - Rohan
我建议在你的程序上运行strace来收集SQL时间,这与go无关(或者在Mac上运行dtruss,在Windows上不确定)。此外,这可能取决于您运行代码的位置。是否同时从同一台机器上运行go和php到同一个数据库? - Daniel Williams
1
最后,虽然这不是你问题的答案,但你可能想尝试Mysql的多行插入语句(insert into ... values (row1), (row2)...),并通过基准测试找到每个语句最佳的行数。如果插入顺序对你的应用程序没有影响,你也可以使用goroutines并行发出多个语句。 - Tobia
显示剩余7条评论
2个回答

4

这是一个古老的问题,但仍然适用 - 迟到总比不到好;以下是您想要了解的内容:

将所有数据作为制表符分隔、以换行结尾和未引用的行放入bytes.Buffer中(如果文本存在问题,则必须先进行转义)。NULL 必须编码为\N

使用http://godoc.org/github.com/go-sql-driver/mysql#RegisterReaderHandler并在"instream"下注册返回该缓冲区的函数。接下来,调用LOAD DATA LOCAL INFILE "Reader::instream" INTO TABLE ...- 这是将数据快速导入MySQL的方法(当从标准输入上传输数据时,我使用 Go 测量了约19 MB / s,与使用MySQL命令行客户端从stdin上传输数据的18 MB/s相比 )。

据我所知,这个驱动程序是LOAD DATA LOCAL INFILE而无需文件的唯一方法。


3

我注意到你没有使用事务,如果你使用的是纯粹的mysql 5.x与InnoDB,那么这会严重拖累性能,因为它会在每次插入时自动提交。

func GetDbCon() (sql.DB) {
    db, _ := sql.Open("mysql", "bla:bla@/bla")
    return *db
}

func PrepareTx(db *db.DB,qry string) (tx *db.Tx, s *db.Stmt, e error) {
 if tx,e=db.Begin(); e!=nil {
  return
 }

 if s, e = tx.Prepare(qry);e!=nil {
  tx.Close()
 }
 return
}


db := GetDbCon()
defer db.Close()

qry := "INSERT INTO ticket ( event_id, entry_id, column_headers, column_data, hash, salt ) VALUES ( ?, ?, ?, ?, ?, ? )"

tx,stmt,e:=PrepareTx(db,qry)
if e!=nil {
 panic(e)
}

defer tx.Rollback()
for i := 0; i < 10000; i++{
 ticket:=models.Ticket{int64(0), storedEvent.Id, int64(i),"", "", "", "", int64(0), int64(0)}
 stmt.Exec(ticket.EventId, ticket.EntryId, ticket.ColumnHeaders, ticket.ColumnData, ticket.Hash, ticket.Salt)

 // To avoid huge transactions
 if i % 1000 == 0 {
  if e:=tx.Commit();e!=nil {
   panic(e)
  } else {
   // can only commit once per transaction
   tx,stmt,e=PrepareTx(db,qry)
   if e!=nil {
    panic(e)
   }
  }
 }
}

// Handle left overs - should also check it isn't already committed
if e:=tx.Commit();e!=nil {
 panic(e)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接