10/5 に ISUCON 3 の予選に Go 言語で参戦していました。
とりあえずレポートは会社のブログに書いたので、 Go 言語で go-sql-driver/mysql を使って MySQL を使う時に知っておくと良い点をまとめておきます。
ちなみに MySQL ドライバにはもうひとつ MyMySQL というものがあり、 まだ試していませんが、 MyMySQL の方が落とし穴が少なそうな気がします。
sql.Open() が返す DB オブジェクトはコネクションプールをしてくれる
なので、自前で DB オブジェクトを使いまわしてコネクションプールを実装しても意味は無いです。
DB.SetMaxIdleConn()
で、使い終わってもクローズしないコネクションの数を設定できます。
デフォルトだと使い終わったコネクションを閉じてしまうので、 DB オブジェクト自体をプールしても
コネクションプールは実現できていません。
最大接続数を制限したい場合は、最大接続数分のバッファを持ったチャンネルを作っておいて、
DBを使う前に dbMutex <- true
して終わったら <- dbMutex
するとかで良いと思います。
接続開始から一定時間経ったコネクションを自動で閉じるなど、もっと細かい制御がしたい場合は、
自前で実装する必要があります。
その場合は database/sql
をやめて自前で Driver 叩いたほうが楽だと思います。
rows.Close() の前に Commit するとエラーになる
rows.Next() は1 Row を読み込み、最後に EOF を読み込むと false を返します。
例えば1行だけのレスポンスを返すクエリを実行し、 rows.Next() を一回実行しただけで Tx.Commit() を 呼び出すと、コミットパケットを投げて、次のOKパケットを受け取るつもりで、残ってたEOFパケットを 読み込んでしまいます。
これ実装難しいところなんですよね。コネクションと相互作用のあるオブジェクトを複数 (この場合は Rows と Tx) 作るとき、片方のオブジェクトからした操作によって、もう片方のオブジェクトのオブジェクトの中途半端な状態を 適切に処理しないといけない。
プレースホルダを使って Query/Exec できない
プレースホルダありの Conn.Query()
は ErrSkip
を返し、 database.sql
は代わりに
Conn.Prepare()
と Stmt.Query()
を実行します。 Exec も同様です。
昔会社のブログにも書いたのですが、 1回のクエリのためだけに Prepare & Execute すると、通信回数が多くなってパフォーマンス的に よくありません。
なので、文字列を適切にエスケープしつつ、 fmt.Sprintf()
でクエリ文字列を作ってしまいましょう。
Go を使うということは文字コードは当然 utf8 を選ぶでしょうから、エスケープするのはそれほど難しくありません。
ちなみに、 'sql_mode="NO_BACKSLASH_ESCAPES"' すると、シングルクォートを2重にするだけで
エスケープできるのでオススメです。
https://github.com/methane/isucon3-qual-go/blob/master/app.go
func sql_escape(s string) string { return strings.Replace(s, "'", "''", -1) } ... result, err := DB.Exec( fmt.Sprintf("INSERT INTO memos (user, content, is_private, created_at) VALUES (%d, '%s', %d, '%s')", user.Id, sql_escape(r.FormValue("content")), isPrivate, now))