📑

データベースのロックって必要なの?売上と振込の複雑な処理を試行錯誤して学んでみた

2024/12/09に公開

データベースの設計やシステム構築を進めていると、「これで本当にデータが壊れないかな?」という疑問に直面すること、ありますよね。私もそんな状況に陥り、「楽観的ロック」や「悲観的ロック」といったロックの仕組みについて調べてみました。

特に今回考えたのは、売上と振込の処理をどう設計するかという課題です。「売上を超える振込はできない」という条件を守りながら、複数のトランザクションが同時に動いても整合性を保つためにはどうすればいいのか?一緒に考えていきましょう!


売上処理と振込処理:設計の基本を考える

まず、今回のケースを整理してみます。

  • 売上の書き込み処理

    • 売上テーブルに売上情報を追加したり、更新したりする操作。
    • 単一テーブルを扱うため、操作としては比較的シンプル。
  • 振込依頼の処理

    • 振込テーブルに新たな振込情報を追加する操作。
    • 同時に売上テーブルを参照して「売上以上の金額を振り込めない」ことをチェックする。

この2つの処理を並行して動かすとき、売上以上の振込が行われないようにするにはどうするかがポイントになります。


ロックの基本を学ぶ:楽観的ロックと悲観的ロック

楽観的ロックの仕組みと特徴

楽観的ロックは「競合はあまり起きないだろう」という前提で動作します。データのバージョン番号や更新日時を利用して、更新時に変更がないことを確認する仕組みです。

  • :
    1. 売上テーブルから現在の売上金額とバージョン番号を取得。
    2. 振込処理時に再度バージョン番号を確認し、一致していれば振込情報を追加。

この方法はロックをかけないため、並行処理のパフォーマンスを向上させる利点があります。ただし、複数のテーブルやリソースにまたがる処理では不整合が生じる可能性があることが分かりました。


悲観的ロックの仕組みと特徴

一方、悲観的ロックは「競合が起きる可能性がある」と考え、データをロックして他の処理が干渉できないようにします。

  • :
    1. 振込処理を開始する際に売上テーブルの該当行をロック。
    2. ロック中は他のトランザクションが売上データにアクセスできない。
    3. 振込処理が完了するまで、データの整合性を完全に保証。

複数のテーブルにまたがる処理でも、一貫して整合性を保てるのが特徴です。ただし、ロック時間が長くなると他のトランザクションが待機するため、パフォーマンスが低下するリスクがあります


複数テーブルにまたがる処理で楽観的ロックが難しい理由

ここが一番の学びポイントでした。複数テーブルにまたがる処理で楽観的ロックを使うと、次のような問題が発生します。

1. 競合検知がリソース単位でしか行われない

楽観的ロックは基本的に1つのテーブルや行の状態を確認します。売上と振込のように複数テーブルにまたがる場合、片方のテーブルで競合が検知されても、もう片方の整合性は保証されません。


2. トランザクション中に状態が変わるリスク

トランザクションを使って振込処理を進めても、トランザクション内で売上データをチェックしてから振込情報を追加するまでの間に、他の処理が売上データを更新する可能性があります。この結果、振込処理が売上の状態を正しく反映できなくなることがあります。


3. リトライが多発する可能性

競合が発生するたびに処理をリトライする必要があるため、複雑なビジネスロジックを持つ処理ではリトライ回数が増え、パフォーマンスが低下する可能性があります。


振込処理の整合性を守る設計:悲観的ロックの活用

複数テーブル間での整合性を保証するには、悲観的ロックが適切だと学びました。具体的な設計のポイントを見ていきましょう。

1. 売上テーブルのロック

  • 振込処理のトランザクション内で、売上テーブルの該当行に排他的ロックを取得します。
  • これにより、振込処理が完了するまで他のトランザクションが売上データにアクセスできなくなります。

2. 振込処理の実行

  • ロック中に売上金額をチェックし、振込可能な金額であることを確認。
  • 振込テーブルに振込情報を追加します。

3. トランザクションのコミット

  • 処理が正常に終了したらコミットし、ロックを解放。
  • 他のトランザクションが売上データを使用できるようになります。

ロックの選択肢の比較

項目 楽観的ロック 悲観的ロック
競合検知の範囲 単一テーブルまたは行 トランザクション全体で保証
競合時の動作 リトライ処理が必要 処理待ちで競合を防止
パフォーマンス 高い(競合が少ない場合) 低い(ロック時間が長い場合)
複数リソース間の整合性 難しい 保証される

まとめ:学んだことと今後の設計に活かすこと

今回の学びを振り返ると、楽観的ロックと悲観的ロックは適材適所で使い分けることが重要だと分かりました。

  • 1つのテーブル・行の更新:

    • 楽観的ロックで十分。リトライ処理で競合を解消できる。
  • 複数テーブル・リソース間の整合性が重要:

    • 悲観的ロックを使用することで、データの一貫性を保つ。

また、ロックを過剰に使うとシステム全体のパフォーマンスに影響が出るため、競合頻度や処理内容に応じた設計を心がける必要があります。

今後もシステム設計の中でこうした問題に直面することがあると思いますが、そのたびに学びながらより良い解決策を探していきたいと思います。この記事が同じような課題に取り組んでいる方々の参考になれば幸いです!

Discussion