見出し画像

No330 トランザクション処理で矛盾を回避する

前回は、2023年10月10日から11日にかけて発生した全銀ネットのトラブルについてお話しました。

本件には行政側からも指導があったようで、11月末までに事故報告書を作成して公開することになっているようです。

その時点で改めて解説をしようと考えています。

さて、前回は軽くお話した「トランザクション処理」をもう少し詳しくお話します。


トランザクション処理とは?

前回のおさらいとなりますが、トランザクション処理というのは、処理結果に矛盾がないように制御する手法を言います。

一つの取引が完了した時点では、以下のいずれかになるはずですよね。
 1. 依頼内容が全て処理が完了した
 2. (不具合が出たので)依頼をないことにして、元通りに戻した

ですが、普通にプログラムを組んでいるとこうはいかない事情がたくさんあります。

その難しさを示す典型例が銀行の振込などの送金処理です。

同じ銀行に口座を持っているAさんからBさんに3万円を振込むとします。
この実現には次の二つの処理が必要です。

 1. Aさんの口座から3万円を減算する
 2. Bさんの口座に3万円を加算する

この通りに処理を淡々と行うわっで、通常はどうということもないのですが、途中でエラーが発生するといきなり問題が難しくなります。

エラーといっても、プログラムが把握できるエラーならいいのです。
例えば、支払い元(Aさん)側の残高不足、口座凍結、受取り側(Bさん)の口座番号間違いなどは実際の送金処理の前に判断できますから、問題にはなりません。

問題になるのは、途中で予期しないエラーが発生した時、例えばバグでプログラムが異常終了した場合などです。

特にAさんの口座からの減算後にエラーが発生すると非常にやっかいなことになります。

上で書いたように、途中でプログラムが終わってしまうと、Aさんの口座は減ったままで、Bさんへの入金が行われません。
お金がどこかに消えてしまいます。

AさんとBさんへの処理順序を入れ換えても同様ですよね。
今度はどこからともなく、お金が生まれたことになります。

これが致命的なのはわかりきった話です。だからといって全ての異常事態に備えたプログラムとなると、めちゃくちゃ作るのが大変です。

このような矛盾を残さないために「トランザクション処理」があるのです。

でも、プログラムが落ちたら、どうにもならんでしょ?って思いますよね。

その話をご理解いただくため、少しトランザクション処理の内側に踏み込みましょう。

トランザクション処理の実際

トランザクション処理は、データベース内の矛盾を避けるためによく利用されます。
上述の送金処理などがその代表的なものです。

データベースは通常、専用のソフト(データベースソフト)で管理を行います。
データベース内のデータを更新する時は、依頼元のプログラムからデータベースソフトに対して「○○を更新してください」と依頼するわけです。

ですが、データベースソフト側は同時には1つのデータ更新しかできないという制約があります。(厳密にはいろいろあるんですが、ここでは触れません)
そのため、送金のように二人分のデータを更新したい時は、(1)Aさんの口座からの減算の依頼と、(2)Bさんの口座への加算の依頼の、二回の依頼が必要となります。

この時、データベース側には「今から二回に分けて依頼するけど、これはワンセットだよ。失敗したら両方とも元に戻してね」と伝えておくのです。

具体的には、依頼元(プログラム)は処理の依頼前には「トランザクション開始宣言」を、処理の依頼後には「トランザクション完了宣言」をデータベースに伝えます。

つまり、こんな形で依頼を送ることになります。

 1. トランザクション開始を宣言する
 2. Aさんの口座から3万円を減算する
 3. Bさんの口座に3万円を加算する
 4. トランザクション完了(コミット)を宣言する

依頼元のプログラム側でエラーが起きた場合は依頼元のプログラムから巻戻し(ロールバック)を宣言します。
ロールバックの依頼が来ると、データベースソフトは必ずトランザクション開始宣言直前の状態に戻してくれます。(データベースソフトが落ちようと、電源が落ちようと、です)

実にシンプルな仕組みですが、このトランザクション開始/完了/巻き戻しを加えることでデータの整合性を確実に保つことができます。
トランザクション処理はデータが矛盾しないことを保証してくれるとても重要な機構なのです。

さて、冒頭に書いたようにプログラムが落ちた場合は誰もロールバックなどしてくれません。
この場合は、データベース側は接続先からの反応が一定時間ないことから「落ちたな」と判断し、自動的にロールバックをすることになっています。
これもまた、トランザクション処理が実現してくれる大切な機構です。

さらにやっかいな同時処理

トランザクション処理は、さらにやっかいな同時処理の問題にも対応ができます。

上述のAさんからBさんへの送金に加えて、CさんからAさんへの送金がほぼ同時に行われたとします。
手続きとしては、以下の二つです。
 1. AさんからBさんに3万円の送金
 2. CさんからAさんに5万円の送金

この処理を行った結果、次の残高になるのが期待値です。
 Aさん:10万円 → -3万円+5万円=12万円
 Bさん:10万円 → +3万円    =13万円
 Cさん:10万円 →     -5万円=5万円

当然ながら、全員の残高を合計すると処理前も処理後も30万円で変化ありません。
(手数料は考えません)

この場合、以下の4つの処理が行われます。
 1. Aさんの口座から3万円を減算する(→7万円に更新)
 2. Bさんの口座に3万円を加算する(→13万円に更新)
 3. Cさんの口座から5万円を減算する(→5万円に更新)
 4. Aさんの口座に5万円を加算する(→12万円に更新)

さて、この処理には残高異常の可能性があるのですが、お気付きでしょうか?

AさんからBさんへの送金と、CさんからAさんの送金タイミングがほぼ同時だと計算結果が期待通りにならない場合があります。

ここで、上述の1. と 4. の処理をさらに詳細化しておきます。

 ○1. Aさんの口座から3万円を減算する(→7万円に更新)
  1-1)Aさんの残高(10万円)をデータベースから取得
  1-2)10万円-3万円=7万円を計算
  1-3)7万円でデータベースを更新

 ○4. Aさんの口座に5万円を加算する(→12万円に更新)
  4-1)Aさんの残高(7万円)をデータベースから取得
  4-2)7万円+5万円=12万円を計算
  4-3)12万円でデータベースを更新

問題は、4.の開始タイミングです。
例えば、これが上で書いた通りに行われた場合は問題にはなりません。

  1-1)Aさんの残高(10万円)をデータベースから取得
  1-2)10万円-3万円=7万円を計算
  1-3)7万円でデータベースを更新
  4-1)Aさんの現在の残高(7万円)をデータベースから取得
  4-2)7万円+5万円=12万円を計算
  4-3)12万円でデータベースを更新

また、先に4.の処理を行った場合も途中の計算は違いますが、問題ありません。

  4-1)Aさんの現在の残高(10万円)をデータベースから取得
  4-2)10万円+5万円=15万円を計算
  4-3)15万円でデータベースを更新
  1-1)Aさんの残高(15万円)をデータベースから取得
  1-2)15万円-3万円=12万円を計算
  1-3)12万円でデータベースを更新

問題となるのは、4-1~4-3の処理が1-1~1-4の途中にはさまった場合です。
  1-1)Aさんの残高(10万円)をデータベースから取得
  4-1)Aさんの現在の残高(10万円)をデータベースから取得
  4-2)10万円+5万円=15万円を計算
  4-3)15万円でデータベースを更新
  1-2)10万円-3万円=7万円を計算
  1-3)7万円でデータベースを更新

なんと、残高が7万円になってしまいます。

また、次のパターンでもおかしくなります

  1-1)Aさんの残高(10万円)をデータベースから取得
  4-1)Aさんの現在の残高(10万円)をデータベースから取得
  1-2)10万円-3万円=7万円を計算
  1-3)7万円でデータベースを更新
  4-2)10万円+5万円=15万円を計算
  4-3)15万円でデータベースを更新

今度は残高が15万円になってしまいました。

個々の処理は間違っていないのに全体としては間違った結果になってしまいます。
このような事象が生じるのは残高の取得と更新が同時に行えないためです。
要は、残高の取得から更新までにタイムラグがあることが問題なのです。

トランザクション処理はこのようなタイムラグが問題となるケースにも威力を発揮します。
トランザクションの開始後から完了宣言(コミット)までの間に来た依頼(他の処理)は待ってもらえば、このタイムラグは気にしなくてよくなります。

例えば次のようになります。(さらに手順が複雑になりますが...)

  1-1)Aさんの残高(10万円)をデータベースから取得
  4-1-1)Aさんの現在の残高を取得(待たされる)
  1-2)10万円-3万円=7万円を計算
  1-3)7万円でデータベースを更新
  4-1-2)ここでようやく残高(7万円)が取得できる
  4-2)7万円+5万円=12万円を計算
  4-3)12万円でデータベースを更新

こうすれば、4.の処理(Aさん口座の加算処理)は1.の処理(Aさん口座の減算処理)が完了してから動きますから、矛盾が生じないわけですね。

まとめ

トランザクション処理というのは、処理エラーの発生やタイミングによるデータベースの矛盾を予防する仕組みです。

これは銀行のような金銭を扱う業務だけではありません。
在庫量の計算、生産実績の更新、保有ポイントの計算など数値の更新が必要なケースでは頻繁に利用されています。

システム開発者がデータベースソフトを採用する理由として「トランザクション機能」はかなり上位に出てきますし、トランザクション処理のないデータベースソフトはなかなか一人前扱いされません。

もっとも、トランザクション処理はできなくても、やたら高速、めっちゃコンパクトといったデータベースソフトもありますので、どちらがエラいという話ではありません。

今回は、トランザクション処理機能についてのお話でした。

次回もお楽しみに。

このNoteは筆者が主宰するメルマガ「がんばりすぎないセキュリティ」からの転載です。
誰もが気になるセキュリティに関連するトピックを毎週月曜日の早朝に配信しています。
無料ですので、是非ご登録ください。
https://www.mag2.com/m/0001678731.html
また、公式サイトでも最新版を公開していますので、そちらもどうぞ。
https://www.egao-it.com/

この記事が気に入ったらサポートをしてみませんか?