仕事で、Microsoft Fabric絡みのところを調べていました。
基本、Fabricの中ではApache Sparkなどが動いていることは判っていたので、Delta LakeでACID特性が実現されているというのもなんとなくわかっていました。では、どのようにしてACID特性が実現されているのかというところには関心がありました。この辺に関して触れられているのは以下のQiita上の記事です。
https://qiita.com/toshimitsuk/items/5dac76dddfe921cb8a47
そして、上記記事の次のところに着目しました。
Delta Lakeのソースコード (https://github.com/delta-io/delta) から、Delta Lakeの楽観的排他制御の仕組みを見ていきます。なお、ここではUpsertを対象に楽観的排他制御の仕組みを見ていきます。(再掲)
なるほど、楽観的排他制御かと。 では、楽観的排他制御とはなにかということです。
悲観的排他制御
これと対照的な概念が悲観的排他制御です。 悲観的排他制御とは所謂ロックによって排他制御を実現します。
こちらの方がよく知られているでしょう。いわゆるUpsertの場合において考えます。Upsertは排他制御がよくわかるところです、Upsertを仮に選択と条件分岐と更新と追加の組み合わせで考えます。
- 対象キーでレコードが存在するかどうかを確認する
- もし、レコードが存在すれば更新する
- レコードが存在しなければ追加する
これをそのまま実装してしまうと、1.と2.の間に割り込まれてしまうと、その間に重複するレコードが作成されてしまうリスクが発生します。結果的に、3.は正しく、Primary Keyなどの処理が実装されていればエラーになります。
Upsertであれば、悲観的排他制御であれば、1.のタイミングで対象となる表などにはロックが作られ、割り込み処理は一般的には待たされます。結果として、Upsertは完遂できます。仮に、待たされた方が自動連番などでキーを発生させていれば、その後により後のキーが発行されて正常に終了するでしょう。
楽観的排他制御
では、これが楽観的排他制御ではどうなるでしょうか? 楽観的排他制御ではロックではなく、更新の検出により処理します。つまり、処理する前に、割り込まれたことを検出して処理をします。 つまり、なぜ楽観的なのかというと、滅多に抵触する処理は行われないはずという仮定があります。 このあたりの詳しい話については以下の記事も参考にできます。
https://qiita.com/NagaokaKenichi/items/73040df85b7bd4e9ecfc
- 検証: 最初にレコードの現在の状態を取得し、その状態を記録します
- 更新の準備: データを更新する準備をします
- 更新の試行: 更新を試みますが、その際に他のトランザクションが同じレコードを変更していないかを確認します
- 競合の対処: 他のトランザクションが変更していた場合は、更新を再試行するか、エラーを返します
パフォーマンスと課題
さて、悲観的排他制御と楽観的排他制御の差はどこにあるのか? 一つのポイントはパフォーマンスです。悲観的排他制御はロックを行うため、そこがパフォーマンス上のボトルネックになります。では、楽観的排他制御は銀の弾丸か? そんなことはありません。銀の弾丸はどこにも存在しないのです。
楽観的排他制御の弱点は、更新の検出にあります。これが崩れればすべてがお釈迦です。そして、楽観的排他制御のポイントは、楽観的、競合することはそうそう起きないだろうにあります。競合がかなりの頻度で起きるとすればどうでしょうか? ほとんどの場合で、何らかの回避動作が必要になります。更に、その回避動作は正しいのかの検証も極めて重要です。
結論から言うと、楽観的排他制御は難易度が高いです。その意味ではロックして、競合処理を起こさないようにする悲観的排他制御の方がシステム的な難易度は低いです。パフォーマンスの劣化は大きな課題ですが。