Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 が存在すると、同じトランザクション内で複数の QueryExec が同時に発行された場合、最初の操作が cimu をロックし、次の操作が cimu をロックしようとしてブロックされ、デッドロックが発生していました。

このコミットでは、以下の変更が行われました。

  1. Tx.cimu フィールドの削除: Tx 構造体から cimu sync.Mutex フィールドが完全に削除されました。これにより、Tx オブジェクトは driver.Conn へのアクセスを保護するための独自のミューテックスを持たなくなりました。
  2. Tx.grabConn() からの cimu.Lock() 呼び出し削除: grabConn() メソッドは、Tx オブジェクトが保持する driverConn を返す前に cimu.Lock() を呼び出していましたが、この呼び出しが削除されました。
  3. Tx.releaseConn() メソッドの削除: cimu.Unlock() を呼び出すだけの releaseConn() メソッドが完全に削除されました。
  4. Query, Exec, Prepare, Stmt メソッドからの releaseConn() 呼び出し削除: これらのメソッドは、トランザクション内で driver.Conn を使用した後、defer tx.releaseConn() を使って releaseConn() を呼び出していましたが、これらの呼び出しがすべて削除されました。
  5. 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

  1. 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 フィールドが削除されています。

  2. 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
     }
    

    ロック取得の呼び出しが削除されています。

  3. 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() メソッド全体が削除されています。

  4. 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() が削除されています。

  5. 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) {} (何もしない関数) に変更されています。

  6. 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

  1. TestSimultaneousQueries テストケースの追加:
    --- 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
    
    このテストケースは、トランザクション内で2つの Query を連続して呼び出すことで、以前デッドロックが発生していた状況を再現し、修正が正しく機能することを確認します。

コアとなるコードの解説

このコミットのコアとなるコードの変更は、database/sql パッケージのトランザクション (Tx) 処理における同期メカニズムの根本的な変更を反映しています。

以前の Tx 構造体には cimu sync.Mutex というフィールドがあり、これは Tx が内部的に保持する driver.Conn (データベース接続) へのアクセスを排他的に制御するためのものでした。Tx.grabConn() メソッドが driver.Conn を取得する際にこのミューテックスをロックし、Tx.releaseConn() メソッドが driver.Conn の使用を終える際にロックを解放していました。

しかし、この設計は、同じトランザクション (Tx インスタンス) に対して複数のゴルーチンが同時に QueryExec などの操作を実行しようとすると問題を引き起こしました。例えば、ゴルーチン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) は不要になったのです。

具体的には、以下の変更が重要です。

  1. cimu ミューテックスの削除: Tx 構造体から cimu フィールドが削除されたことで、Tx オブジェクトは driver.Conn へのアクセスを同期する責任から解放されました。これにより、複数のゴルーチンが同じ Tx インスタンスに対して同時に操作を行っても、cimu によるブロックやデッドロックが発生しなくなります。
  2. 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 の排他的な使用を保証する下位レイヤーのメカニズムに依存するようになり、トランザクション内での並行クエリ実行が可能になりました。これにより、アプリケーション開発者はトランザクション内でより柔軟にデータベース操作を設計できるようになります。

関連リンク

参考にした情報源リンク