[インデックス 15815] ファイルの概要
このコミットは、Go言語の database/sql
パッケージにおけるトランザクション (Tx
) 内での同時クエリ実行を可能にするための変更です。具体的には、Tx
構造体から不要なミューテックス (cimu
) を削除し、それに伴う関連コードの修正を行っています。これにより、トランザクション内で複数のクエリを同時に発行してもデッドロックが発生しないようになります。
コミット
commit a7a803c7b7b5322c90d093cc603ebafd0c3c320a
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Mar 18 11:39:00 2013 -0700
database/sql: allow simultaneous queries, etc in a Tx
Now that revision 0c029965805f is in, it's easy
to guarantee that we never access a driver.Conn
concurrently, per the database/sql/driver contract,
so we can remove this overlarge mutex.
Fixes #3857
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/7707047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a7a803c7b7b5322c90d093cc603ebafd0c3c320a
元コミット内容
このコミットの目的は、database/sql
パッケージにおいて、トランザクション (Tx
) 内での同時クエリ実行を許可することです。以前は、Tx
オブジェクトが driver.Conn
(データベースドライバとの接続を表すインターフェース) へのアクセスを保護するために cimu
というミューテックスを保持していました。しかし、先行するリビジョン 0c029965805f
の変更により、driver.Conn
が並行してアクセスされることがないという database/sql/driver
の契約が保証されるようになったため、この「過剰な」ミューテックスを削除することが可能になりました。これにより、Issue #3857 で報告されていたデッドロックの問題が修正されます。
変更の背景
この変更の背景には、Go言語の database/sql
パッケージにおけるトランザクションの並行性に関する課題がありました。以前の database/sql
の実装では、トランザクション (Tx
) オブジェクトが内部的に driver.Conn
(データベースドライバとの物理的な接続) を排他的に所有し、その driver.Conn
へのアクセスを cimu
というミューテックスで保護していました。
しかし、この設計は、同じトランザクション内で複数のクエリを並行して実行しようとするとデッドロックを引き起こす可能性がありました。具体的には、あるクエリが cimu
をロックしている間に、別のクエリが同じ cimu
をロックしようとしてブロックされ、結果として処理が進まなくなるという問題です。これは、Issue #3857 (Go issue 3857: database/sql: Tx.Query deadlocks if called twice) で報告されていました。
この問題の解決を可能にしたのが、先行するコミット 0c029965805f
(commit 0c029965805f: database/sql: make driver.Conn usage non-concurrent) です。このコミットにより、database/sql
パッケージは、driver.Conn
が常に単一のゴルーチンによってのみアクセスされることを保証するようになりました。つまり、driver.Conn
は並行アクセスから保護される必要がなくなったため、Tx
オブジェクトが独自に driver.Conn
へのアクセスを保護するための cimu
ミューテックスが不要になったのです。
このコミットは、0c029965805f
の変更によって得られた安全性を活用し、cimu
ミューテックスを削除することで、トランザクション内での同時クエリ実行を可能にし、デッドロックの問題を解消することを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の database/sql
パッケージに関する前提知識が必要です。
database/sql
パッケージ: Go言語の標準ライブラリの一部で、SQLデータベースとのやり取りを行うための汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバを含まず、データベース固有の操作はdatabase/sql/driver
パッケージで定義されたインターフェースを実装するサードパーティのドライバに委譲します。driver.Conn
インターフェース:database/sql/driver
パッケージで定義されているインターフェースの一つで、データベースへの単一の物理的な接続を表します。このインターフェースを実装することで、データベースドライバはdatabase/sql
パッケージと連携できます。Tx
(Transaction):database/sql
パッケージにおけるトランザクションを表す構造体です。データベース操作をアトミックな単位で実行するために使用されます。トランザクション内のすべての操作は成功するか、すべて失敗するかのいずれかです(ACID特性のAtomicity)。- ミューテックス (
sync.Mutex
): Go言語のsync
パッケージで提供される同期プリミティブの一つです。共有リソースへの複数のゴルーチンからの同時アクセスを制御するために使用されます。Lock()
メソッドでロックを取得し、Unlock()
メソッドでロックを解放します。ロックが取得されている間は、他のゴルーチンはロックを取得しようとするとブロックされます。 - 並行性 (Concurrency) と並列性 (Parallelism):
- 並行性: 複数のタスクが同時に進行しているように見える状態を指します。Goのゴルーチンとチャネルは並行性を実現するための強力な機能です。
- 並列性: 複数のタスクが物理的に同時に実行されている状態を指します(マルチコアCPUなど)。
このコミットでは、
driver.Conn
へのアクセスが「並行」に行われないことが保証されるようになった点が重要です。
技術的詳細
このコミットの技術的な核心は、database/sql
パッケージの Tx
構造体から sync.Mutex
である cimu
フィールドを削除し、それに伴う grabConn()
および releaseConn()
メソッドの変更、そして Query
, Exec
, Prepare
, Stmt
メソッドからの releaseConn()
の呼び出し削除です。
以前の Tx
構造体は、以下のようなフィールドを持っていました。
type Tx struct {
// ...
dc *driverConn
txi driver.Tx
// cimu is held while somebody is using ci (between grabConn
// and releaseConn)
// TODO(bradfitz): golang.org/issue/3857
cimu sync.Mutex
// ...
}
ここで cimu
は、driver.Conn
(dc.ci
を通じてアクセスされる) へのアクセスを保護するためのミューテックスでした。grabConn()
メソッドは cimu.Lock()
を呼び出してロックを取得し、releaseConn()
メソッドは cimu.Unlock()
を呼び出してロックを解放していました。
しかし、先行するコミット 0c029965805f
によって、database/sql
パッケージは driver.Conn
が常に単一のゴルーチンによってのみアクセスされることを保証するようになりました。これは、database/sql
の内部で driver.Conn
のライフサイクルと使用が厳密に管理されるようになったためです。具体的には、driver.Conn
はプールから取得され、使用中は排他的にそのゴルーチンに割り当てられ、使用が完了するとプールに戻されるというメカニズムが確立されました。
この保証により、Tx
構造体内で driver.Conn
へのアクセスを保護するための cimu
ミューテックスは冗長となり、むしろ並行クエリのデッドロックの原因となっていました。cimu
が存在すると、同じトランザクション内で複数の Query
や Exec
が同時に発行された場合、最初の操作が cimu
をロックし、次の操作が cimu
をロックしようとしてブロックされ、デッドロックが発生していました。
このコミットでは、以下の変更が行われました。
Tx.cimu
フィールドの削除:Tx
構造体からcimu sync.Mutex
フィールドが完全に削除されました。これにより、Tx
オブジェクトはdriver.Conn
へのアクセスを保護するための独自のミューテックスを持たなくなりました。Tx.grabConn()
からのcimu.Lock()
呼び出し削除:grabConn()
メソッドは、Tx
オブジェクトが保持するdriverConn
を返す前にcimu.Lock()
を呼び出していましたが、この呼び出しが削除されました。Tx.releaseConn()
メソッドの削除:cimu.Unlock()
を呼び出すだけのreleaseConn()
メソッドが完全に削除されました。Query
,Exec
,Prepare
,Stmt
メソッドからのreleaseConn()
呼び出し削除: これらのメソッドは、トランザクション内でdriver.Conn
を使用した後、defer tx.releaseConn()
を使ってreleaseConn()
を呼び出していましたが、これらの呼び出しがすべて削除されました。Query
メソッド内のreleaseConn
クロージャの変更:Query
メソッド内でtx.db.queryConn
に渡されるreleaseConn
クロージャが、以前はfunc(err error) { tx.releaseConn() }
であったものが、func(error) {}
(何もしない関数) に変更されました。これは、Rows
オブジェクトがdriver.Conn
の所有権を引き継ぎ、Rows.Close()
が呼ばれたときにdriver.Conn
を解放する責任を持つため、トランザクション側で明示的にreleaseConn
を呼ぶ必要がなくなったためです。
これらの変更により、Tx
オブジェクトは driver.Conn
へのアクセスを同期する責任から解放され、database/sql
パッケージ全体の driver.Conn
の排他的な使用保証に依存するようになりました。結果として、同じトランザクション内で複数のクエリを並行して実行しても、Tx
レベルでのデッドロックが発生しなくなりました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に src/pkg/database/sql/sql.go
ファイルに集中しています。
src/pkg/database/sql/sql.go
-
Tx
構造体からのcimu
フィールドの削除:--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -689,15 +689,9 @@ type Tx struct { // dc is owned exclusively until Commit or Rollback, at which point // it's returned with putConn. - // TODO(bradfitz): golang.org/issue/3857 dc *driverConn txi driver.Tx - // cimu is held while somebody is using ci (between grabConn - // and releaseConn) - // TODO(bradfitz): golang.org/issue/3857 - cimu sync.Mutex - // done transitions from false to true exactly once, on Commit // or Rollback. once done, all operations fail with // ErrTxDone.
cimu sync.Mutex
フィールドが削除されています。 -
Tx.grabConn()
メソッドからのtx.cimu.Lock()
の削除:--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -720,14 +714,9 @@ func (tx *Tx) grabConn() (*driverConn, error) { if tx.done { return nil, ErrTxDone } - tx.cimu.Lock() return tx.dc, nil }
ロック取得の呼び出しが削除されています。
-
Tx.releaseConn()
メソッドの削除:--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -720,14 +714,9 @@ func (tx *Tx) grabConn() (*driverConn, error) { if tx.done { return nil, ErrTxDone } - tx.cimu.Lock() return tx.dc, nil } -func (tx *Tx) releaseConn() { - tx.cimu.Unlock() -} - // Commit commits the transaction. func (tx *Tx) Commit() error { if tx.done {
releaseConn()
メソッド全体が削除されています。 -
Tx.Prepare()
,Tx.Stmt()
,Tx.Exec()
メソッドからのdefer tx.releaseConn()
の削除:--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -774,7 +763,6 @@ func (tx *Tx) Prepare(query string) (*Stmt, error) { if err != nil { return nil, err } - defer tx.releaseConn() dc.Lock() si, err := dc.ci.Prepare(query) @@ -817,7 +805,6 @@ func (tx *Tx) Stmt(stmt *Stmt) *Stmt { if err != nil { return &Stmt{stickyErr: err} } - defer tx.releaseConn() dc.Lock() si, err := dc.ci.Prepare(stmt.query) dc.Unlock() @@ -840,7 +827,6 @@ func (tx *Tx) Exec(query string, args ...interface{}) (Result, error) { if err != nil { return nil, err } - defer tx.releaseConn() if execer, ok := dc.ci.(driver.Execer); ok { dargs, err := driverArgs(nil, args)
これらのメソッド内で、
driver.Conn
の使用後にミューテックスを解放するためのdefer tx.releaseConn()
が削除されています。 -
Tx.Query()
メソッド内のreleaseConn
クロージャの変更:--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -871,14 +857,12 @@ func (tx *Tx) Exec(query string, args ...interface{}) (Result, error) { // Query executes a query that returns rows, typically a SELECT. func (tx *Tx) Query(query string, args ...interface{}) (*Rows, error) { - ci, err := tx.grabConn() + dc, err := tx.grabConn() if err != nil { return nil, err } - - releaseConn := func(err error) { tx.releaseConn() } - - return tx.db.queryConn(ci, releaseConn, query, args) + releaseConn := func(error) {} + return tx.db.queryConn(dc, releaseConn, query, args) }
releaseConn
クロージャがfunc(error) {}
(何もしない関数) に変更されています。 -
Stmt.connStmt()
メソッド内のreleaseConn
クロージャの変更:--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -980,7 +964,7 @@ func (s *Stmt) connStmt() (ci *driverConn, releaseConn func(error), si driver.St if err != nil { return } - releaseConn = func(error) { s.tx.releaseConn() } + releaseConn = func(error) {} return ci, releaseConn, s.txsi.si, nil }
同様に、
releaseConn
クロージャがfunc(error) {}
に変更されています。
src/pkg/database/sql/sql_test.go
TestSimultaneousQueries
テストケースの追加:
このテストケースは、トランザクション内で2つの--- a/src/pkg/database/sql/sql_test.go +++ b/src/pkg/database/sql/sql_test.go @@ -736,3 +736,28 @@ func TestIssue4902(t *testing.T) { t.Logf("stmt = %#v", stmt) } }\n+\n+// Issue 3857 +// This used to deadlock. +func TestSimultaneousQueries(t *testing.T) { + db := newTestDB(t, "people") + defer closeDB(t, db) + + tx, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer tx.Rollback() + + r1, err := tx.Query("SELECT|people|name|") + if err != nil { + t.Fatal(err) + } + defer r1.Close() + + r2, err := tx.Query("SELECT|people|name|") + if err != nil { + t.Fatal(err) + } + defer r2.Close() +}\n
Query
を連続して呼び出すことで、以前デッドロックが発生していた状況を再現し、修正が正しく機能することを確認します。
コアとなるコードの解説
このコミットのコアとなるコードの変更は、database/sql
パッケージのトランザクション (Tx
) 処理における同期メカニズムの根本的な変更を反映しています。
以前の Tx
構造体には cimu sync.Mutex
というフィールドがあり、これは Tx
が内部的に保持する driver.Conn
(データベース接続) へのアクセスを排他的に制御するためのものでした。Tx.grabConn()
メソッドが driver.Conn
を取得する際にこのミューテックスをロックし、Tx.releaseConn()
メソッドが driver.Conn
の使用を終える際にロックを解放していました。
しかし、この設計は、同じトランザクション (Tx
インスタンス) に対して複数のゴルーチンが同時に Query
や Exec
などの操作を実行しようとすると問題を引き起こしました。例えば、ゴルーチンAが Tx.Query()
を呼び出し、cimu
をロックしたとします。その間にゴルーチンBが同じ Tx
インスタンスに対して Tx.Query()
を呼び出すと、ゴルーチンBは cimu
のロック取得を待ってブロックされます。もしゴルーチンAの処理が何らかの理由で遅延したり、ゴルーチンBがロックを待っている間にゴルーチンAがさらに別の操作を試みたりすると、デッドロックが発生する可能性がありました。これが Issue #3857 で報告されていた問題です。
このコミットは、先行するコミット 0c029965805f
によって database/sql
パッケージ全体で driver.Conn
が常に単一のゴルーチンによってのみアクセスされることが保証されるようになったという前提に基づいています。つまり、driver.Conn
自体は並行アクセスから保護されているため、Tx
レベルでさらに driver.Conn
へのアクセスを保護するためのミューテックス (cimu
) は不要になったのです。
具体的には、以下の変更が重要です。
cimu
ミューテックスの削除:Tx
構造体からcimu
フィールドが削除されたことで、Tx
オブジェクトはdriver.Conn
へのアクセスを同期する責任から解放されました。これにより、複数のゴルーチンが同じTx
インスタンスに対して同時に操作を行っても、cimu
によるブロックやデッドロックが発生しなくなります。releaseConn()
メソッドの削除と関連呼び出しの変更:releaseConn()
メソッド自体が削除され、Tx.Prepare()
,Tx.Stmt()
,Tx.Exec()
からのdefer tx.releaseConn()
の呼び出しも削除されました。これは、driver.Conn
の解放がTx
の責任ではなく、Rows
オブジェクトやStmt
オブジェクトがdriver.Conn
のライフサイクルを管理するようになったためです。特にQuery
メソッドでは、tx.db.queryConn
に渡されるreleaseConn
クロージャがfunc(error) {}
(何もしない関数) に変更されています。これは、Rows
オブジェクトがdriver.Conn
の所有権を引き継ぎ、Rows.Close()
が呼ばれたときにdriver.Conn
をプールに戻す責任を持つため、トランザクション側で明示的にreleaseConn
を呼ぶ必要がなくなったことを意味します。
これらの変更により、Tx
オブジェクトは driver.Conn
の排他的な使用を保証する下位レイヤーのメカニズムに依存するようになり、トランザクション内での並行クエリ実行が可能になりました。これにより、アプリケーション開発者はトランザクション内でより柔軟にデータベース操作を設計できるようになります。
関連リンク
- Go Issue #3857: database/sql: Tx.Query deadlocks if called twice
- Go Change List 7707047: database/sql: allow simultaneous queries, etc in a Tx
- 先行するコミット 0c029965805f: database/sql: make driver.Conn usage non-concurrent
参考にした情報源リンク
- Go言語の
database/sql
パッケージのドキュメント - Go言語の
database/sql/driver
パッケージのドキュメント - Go言語の
sync
パッケージのドキュメント - Go言語の並行性に関する一般的な情報源 (例: Go Concurrency Patterns)
- Go言語のIssueトラッカー (GitHub)
- Go言語のコードレビューシステム (Gerrit)
- https://go.googlesource.com/go/+/refs/heads/master (GerritのCLリンクは、GitHubのコミットページからも辿れます)
- Go言語の公式ブログ
- https://go.dev/blog/ (特定の記事を直接参照したわけではありませんが、Goの設計思想や変更の背景を理解する上で役立ちます)
- Stack Overflow や技術ブログ記事 (Go database/sql deadlock, Go driver.Conn concurrency などで検索) (具体的な記事は特定しませんが、一般的な理解を深めるために参照する可能性があります)