外部キー制約でデッドロックに引っかかった話
今回は仕事で直面したデッドロックのケースについて話したいと思います。
今回ハマったケース
今回ハマったケースは、複数プロダクトが共有するデータベースにあるデータを単体プロダクト専用のデータベースに持ち帰る開発を行っていたときでした。かなり簡略化しておりますが現状と分離後の理想型は下記の通りです。
現状
- 担当プロダクトと別プロダクトは同じ従業員データを参照している
- 従業員データの中には、担当プロダクト専用のカラムもあれば、別プロダクト専用の項目もある
- マイクロサービスがもつマスタ従業員データと旧従業員データがそれぞれ持つ名前カラムは同期している
理想系
- 担当プロダクトは新しい専用の従業員データをもち、専用のカラムはそのテーブルにもつ
- マイクロサービスがもつマスタ従業員データと各プロダクトの従業員データの名前カラムは同期させる
この対応を段階的に進めている途中で、
- 担当プロダクトによる旧従業員データの更新がまだ一部残っている
- マスタ従業員データの更新をマイクロサービスのAPI経由で行う
という状態が発生した時にデッドロックが発生しました。
デッドロックについて
ご存知の方も多いとは思いますが、改めてデッドロックとは何かについて確認します。
デッドロックとはそれぞれが他方が必要とするロックを保持していることによって、トランザクションが進行出来ない状況のことです。
例えば部署と従業員を管理するデータがあったとします。
これらのテーブルに対し異なる処理が同じデータを更新しようとした結果、互いの処理のlockの開放待ち状態になり、処理が進まなくなってしまうような状況です
(実際にMySQLで試してみたケース)
上記のような場合にROLLBACKが発生してしまうので、再度タイミングをずらして再実行する必要があります。実装としては、同じような更新を行う処理に関しては常に同じ順番での更新手続きに統一することでデッドロックの発生を回避出来ます。
今回デッドロックが発生した理由
今回は次のような手続きの更新を行った時にデッドロックが発生しました。
この場合だと一見異なるテーブルに対する更新処理を行っているのでデッドロックは発生しないように思えます。が、盲点だったのはlockは更新しようとしているテーブルだけではなく、外部キーを持っていた場合はその参照先のレコードもロックをとるという点でした。
今回だと、マスタ従業員データはデータの移行の背景もあって旧従業員データのidを外部キーとして持っているため新従業員データの更新をする際に 旧従業員データの行ロックを取得していました。
そのため、今回はマイクロサービス側の更新処理のタイミングを完全に分けることでデッドロックを解消させました。
が、これはアプリケーションの仕様が複雑だった故の苦肉の策であり、よりシンプルなケースであればマイクロサービス側のTransactionを先に完了させ、その後にアプリケーション側のTransactionを開始することで解消出来る思われます。
まとめ
なるべくリリース単位での変更内容を小さくすることで、不具合の可能性やユーザーへの影響を抑えようと取り組んでいたのですが、それによってこのような問題に直面するとは予想外でした。
また、デッドロックについては基礎的な理解はしているつもりでしたが、外部キー制約が存在する場合に外部キーで指定されているテーブルのロックも取得してしまうことは知らなかったので勉強になりました。
データベースレイヤーでの実装ミスは、アプリケーションレイヤーでのミスよりも影響度が大きくなりがちなので今後も気をつけていきたいと思います。
Discussion