Go言語とPostgreSQLで遊ぼう!~ デッドロック回避編~

こんにちは。KOUKIです。

Go言語でアプリケーション開発手法を紹介しています。

前回は、デッドロック処理について触れました。

今回は、デッドロックをどのように回避していくかについて学習していきましょう。

デッドロックを発生させる

復習がてらデッドロックを発生させてみましょう。

最初にdocker-compose upでコンテナを起動しましょう。

次にターミナルを2つ立ち上げ、以下のコマンドを入力してください。

復習になりますが、 Transactionを開始するにはBEGIN;を入力します。そして、トランザクションは、ROLLBACK;やCOMMIT;を入力することで完了させることができます。

UPDATE文でデータ更新をかけていくと、Terminal1でブロックが発生しました。これは、同じIDに対して更新をかけたからです。

ロックを示すレコードを確認したい場合、「http://localhost:9232/」にアクセスして、以下のSELECT文を実行してください。

「ShareLock」が発生している行がありました。これはTerminal1のブロックの原因になっています。その証拠としてqueryに「UPDATE accounts SET balance = balance + 10 WHERE id = 2 RETURNING *;」とあります。

その上の行を見てみると「ExclusiveLock」とあり、queryに「UPDATE accounts SET balance = balance + 10 WHERE id = 2 RETURNING *;」の記載があります。これは、Terminal2で実行したqueryです。

ExclusiveLock(排他的ロック)をかけているレコードに対して、UPDATEをかけるとブロックされるようですね。

Terminal1でブロックになっている状態で、Terminal2で以下のコマンドを実行するとデッドロックが発生します

詳細にも「DETAIL: Process 44 waits for ShareLock on transaction 1002; blocked by process 36.」とありますね。

デッドロックの発生が確認できたので、後片付けをしてコンテナから抜けましょう。

実装

デッドロック確認コード

前回の記事ではリレーション(id)に問題があることがわかった為、UPDATE文に「NO KEY」を追加して、事象を回避しました。

しかし、完全に回避できたわけではないようです。

以下のテストコードをstore_test.goに追加して、テストしてみましょう。

デッドロックが発生しましたね!

次の処理で、accountのIDを入れ替えてます。

これにより、複数のトランザクションが同時に走るとaccount1およびaccount2のIDが同時に参照されるようになります。

# 今まで
T1 -> account1.ID参照 -> account2.ID参照
T2 -> account1.ID参照(T1がaccount1.IDを解放するのを待ってから) -> account2.ID参照

# 現状
T1 -> account1.ID参照 -> account2.ID参照(T2が解放するまで待つ)
T2 -> account2.ID参照 -> account1.ID参照(T1が解放するまで待つ)
->デッドロック

デッドロック回避コード

store.goにトランザクションを引き起こしているコードがあります。

デッドロックを回避するには、ここに条件分岐を入れると良さそうです。

前述の通り、異なるトランザクションがリレーション関係にあるaccountIDをロックし合うため、デッドロックが発生しました。つまり、特定の条件下では処理の順番を逆にして、ID1 -> ID2のように一方向の流れを作れば回避できるはずです。

if arg.FromAccountID < arg.ToAccountID チェックを入れて、条件に反した場合は処理の流れを逆にしました。これで大丈夫なはずです。

OKですね。ただ、処理が冗長になっているので、リファクタリングしましょう。

リファクタリング

重複しているAddAccountBalanceの呼び出しをユーティリティコードにします。

これを先ほど実装した条件文内のコードと差し替えます。

だいぶ簡潔になりましたね。テストを実行しましょう。

OKですね。

次回

次回も引き続き、PostgreSQL中心に学習しましょう。

Go言語まとめ

オススメ書籍

コメントを残す