下面的列表包含了一些有用的信息,在使用数据库驱动的Go应用程序时,这些信息是非常有益的,所以你要测试并应用于你自己的代码。下面的大部分信息来自Go数据库/sql教程和《用Go构建数据库驱动的应用程序终极指南》文件。
你在这里学到的最重要的东西,也是你必须注意的,就是db.Prepare 函数的使用。如果你不按照建议去做,不了解它的实际作用,不知道它是怎么做的,那么它真的会给你带来非常非常大的打击。除了下面写的信息外,我强烈建议你阅读上面的第二个链接,因为那里有更多关于为什么的深入解释。
1)准备好的与非准备好的报表
如果你100%确定你要用作SQL查询的数据(insert,update,delete )是安全的(无SQL注入),你应该使用db.Exec ,而不是依赖db.Prepare 语句。原因是,无论你在查询中是否有参数占位符,准备好的语句将浪费资源,发出三个查询(Prepare,Execute 和Close),只是为了完成一项工作,而直接执行的语句只发出一个查询(Query)。下面的例子是为了向一个表插入一条记录。准备选项将使网络往返的次数增加三倍。然而,直接执行选项将导致一次网络往返。**注意:**如果你希望明确地准备参数,可以使用%q 占位符的fmt.Sprintf() - 例如:fmt.Sprintf(`INSERT INTO table (name, age, active) VALUES (%q, %d, %t)`)
db.Prepare
func Insert() (int64, error) {
stmt, err := db.Prepare("INSERT INTO users (name) VALUES (?)")
if err != nil {
return 0, err
}
defer stmt.Close()
res, err := stmt.Exec("inanzzz")
if err != nil {
return 0, err
}
id, err := res.LastInsertId()
if err != nil {
return 0, err
}
return id, nil
}
2019-12-28T21:12:53.517599Z 3 Prepare INSERT INTO user (name) VALUES (?)
2019-12-28T21:12:53.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz')
2019-12-28T21:12:53.527352Z 3 Close stmt
db.Exec
func Insert() (int64, error) {
res, err := db.Exec("INSERT INTO user (name) VALUES ('inanzzz')")
if err != nil {
return 0, err
}
id, err := res.LastInsertId()
if err != nil {
return 0, err
}
return id, nil
}
2019-12-28T21:13:11.337554Z 3 Query INSERT INTO user (name) VALUES ('inanzzz')
基准测试
100个用户同时发送请求,持续10秒,没有休息。下面的发现只是粗略的计算。
// db.Prepare
Average total requests: 4500
Average request duration: 210ms
// db.Exec
Average total requests: 8500
Average request duration: 120ms
2) 循环内的准备与循环外的准备
如果你要在一个循环中对不同的数据集运行多个相同的查询,请将准备db.Prepare 在循环之外,并在其中执行stmt.Exec 。原因是重复准备和关闭同一个查询只会浪费所有执行的资源,这显然是没有用的。此外,在循环中调用defer stmt.Close() ,就像是在等待灾难的发生,因为它有可能导致资源泄漏。因此,在循环内准备将使查询量增加一倍。请看下面的例子。
内部
func Insert() error {
for i := 1; i < 4; i++ {
stmt, err := r.database.Prepare(`INSERT INTO users (name) VALUES (?)`)
if err != nil {
return err
}
defer stmt.Close()
_, err := stmt.Exec(fmt.Sprintf("inanzzz-%d", i))
if err != nil {
return err
}
}
return nil
}
如你所见,我们总共运行了9个查询。
2019-12-28T21:12:53.517599Z 3 Prepare INSERT INTO user (name) VALUES (?)
2019-12-28T21:12:53.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-1')
2019-12-28T21:12:54.517599Z 3 Prepare INSERT INTO user (name) VALUES (?)
2019-12-28T21:12:54.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-2')
2019-12-28T21:12:55.517599Z 3 Prepare INSERT INTO user (name) VALUES (?)
2019-12-28T21:12:55.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-3')
2019-12-28T21:12:56.527352Z 3 Close stmt
2019-12-28T21:12:56.527352Z 3 Close stmt
2019-12-28T21:12:56.527352Z 3 Close stmt
外部
func Insert() error {
stmt, err := db.Prepare(`INSERT INTO users (name) VALUES (?)`)
if err != nil {
return err
}
defer stmt.Close()
for i := 1; i < 4; i++ {
_, err := stmt.Exec(fmt.Sprintf("inanzzz-%d", i))
if err != nil {
return err
}
}
return nil
}
如你所见,我们总共运行了5个查询。
2019-12-28T21:12:53.517599Z 3 Prepare INSERT INTO user (name) VALUES (?)
2019-12-28T21:12:53.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-1')
2019-12-28T21:12:54.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-2')
2019-12-28T21:12:55.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-3')
2019-12-28T21:12:55.527352Z 3 Close stmt
3) LastInsertId和RowsAffected
只有当你明确需要知道最后插入的ID是什么或者有多少行被影响时,才使用result.LastInsertId() 和result.RowsAffected() 函数。否则不要默认使用它们,因为虽然这些函数不访问数据库,但如果其连接繁忙,它们可能成为 "阻塞 "操作。另外,也不是所有的数据库驱动都支持这些功能。
4)Query/QueryRow vs Exec
如果语句不返回行,你应该首选使用db.Exec ,而不是db.Query 和db.QueryRow 。主要原因是:db.Exec 直接将其连接释放到池中,而db.Query 将其连接保持在池外,直到rows.Close() 被调用。如果你避免这个建议,你可能会导致连接 "泄漏 "和服务器的可用连接耗尽。
如果你使用带或不带查询占位符的select 语句,你应该坚持使用db.Query 和db.QueryRow ,而不是db.Prepare & stmt.Exec 组合。原因是,如果查询中没有占位符,db.Query 和db.QueryRow 会像db.Exec 一样,只发出一个查询 (Query) ,这正是我们想要的。
5) db.SetMaxIdleConns
它定义了被释放后在池中应该保持空闲的连接数。你可以通过在数据库中运行SHOW PROCESSLIST; 查询来验证。默认值是2 。这个默认值会导致大量的连接被关闭和打开,这是你想要避免的。这将导致额外的工作和延迟,以及在等待新连接时的延迟。你可以安全地将其设置为5 (理想)或以上,如25 。
基准测试
100个用户同时发送请求,持续10秒,没有休息。下面的发现只是粗略的计算。
// db.SetMaxIdleConns = 2 (default)
Average total requests: 3800
Average request duration: 250ms
// db.SetMaxIdleConns = 5
Average total requests: 4700
Average request duration: 200ms
// db.SetMaxIdleConns = 25
Average total requests: 5500
Average request duration: 180ms
6) db.SetMaxOpenConns
它定义了数据库中开放连接的最大数量。默认值为无限。如果你想改变它的默认值,你必须要小心。如果任何给定的查询上下文实现了超时功能,如context.WithTimeout ,那么改变将有可能导致context deadline exceeded 错误。可能没有足够的开放连接来处理未决的查询,因此许多查询会超时。这也会降低性能。除非你有100%的把握,否则最好不要改变它。
7) db.SetConnMaxLifetime
它定义了一个连接应该生存的最大时间。默认值是无限的。理想的时间也取决于db.SetMaxIdleConns ,所以db.SetMaxIdleConns ,越高越低db.SetConnMaxLifetime 。粗略的例子。 db.SetMaxIdleConns: 5 -db.SetConnMaxLifetime: 30 minutes,db.SetMaxIdleConns: 10 -db.SetConnMaxLifetime: 15 minutes. 如果你有一个非常繁忙的数据库驱动的应用程序,你可以保持更高的数字。使用许多长期闲置的连接需要大量的系统资源。
补充说明
循环内的延缓
不要在循环中调用defer rows.Close() ,因为这有可能导致内存或连接的 "泄漏"。
打开许多数据库对象
在应用程序启动时只创建一次sql.DB 实例,让所有的请求都使用它。否则,你将会打开和关闭许多到数据库的TCP连接,并有可能使服务器崩溃。
忘记关闭行
尽快运行defer rows.Close() ,否则将有可能导致连接 "泄漏"。
避免使用准备好的语句
如果可以的话,尽量避免使用db.Prepare ,以便通过运行更少的查询和不必要的网络往返重复查询来提高性能。尽量利用fmt.Sprintf() 和%q 占位符来明确准备参数。
对非选择查询使用查询
如果你不发布Select 语句,就不要使用db.Query 和db.QueryRow 。使用db.Exec 来代替。
与空字段一起工作
如果表中的字段返回空值,你的结构或变量类型也必须设置为空值。例如:sql.NullString,mysql.NullTime 等。
与uint64参数一起工作
db.Query,db.QueryRow 和db.Exec 函数并不擅长处理uint64 类型的参数,所以用fmt.Sprint() 将它们转换为字符串以避免意外。