😸

トランザクションの注意点:ファントムリード、システム日時で発生した例を踏まえて

2024/12/15に公開

はじめに

コミットした内容は、他のトランザクションにどう影響するのかって意識が希薄だった話です。
コミットは確定で他のトランザクションから見えるようになって、ロールバックすれば取り消しでしょ?ぐらいの感覚でいたので…

具体的には、

  • このレコードの更新時刻を条件にselectしてるんだけど、抽出できていなくね?
  • データ完了後にフラグ立ててるんだけど、まだ未処理のデータまで更新してね?

みたいなことが起こる可能性があります。

事前知識

トランザクションについて

↓がよくまとまっていると思うので、参考に
https://qiita.com/song_ss/items/38e514b05e9dabae3bdb

  • ACID特性の分離性
  • ファントムリード

が本記事に関連しているのでそこらへんを見てもらえれば。。。

システム日時について

postgreSQLなどでは、

select current_timestamp;

で、現在日時を取得できるが
取得する日時は、トランザクションの開始時点の時刻になります
同じトランザクションで何回システム日時が取得しても同じ値を返します。

長時間処理するようなバッチだと、トランザクション開始~コミットまでの時間が大きくシステム日時がズレているのかと勘違いすることも。。。

更新処理以外のトランザクションから見ると、コミットされて初めて参照できるようになるわけで、この時点でトランザクション開始日時は過去となります。なので、システム日時設定しているのに、なんで過去の日時が入るの?って勘違いしてしまう場合もあります。

システム概要

以下のようなシステムを考える。
(この設計が適切かどうかは置いといて、仕組みとして単純なので実装しやすいんですよね。)

簡単に説明すると・・・
連携トリガーテーブルを使って、どのレコードに変更があったかを検知してBバッチが外部システムにデータを渡すようなシステムです。
Bバッチで処理が完了したデータは、送信フラグを立てて終了という流れです。

今回説明するのは連携トリガーテーブルの部分だけなので、他は参考程度に考えてもらえれば…

補足として…

  • AとBは一定間隔で実行するが、実行間隔は異なる。
    ⇒実行サイクルが異なるので、開始/終了タイミングがABで入れ子になることがある
  • データの更新は短時間で複数回行われることがある。

連携トリガーの作成イメージです。

# データ連携日 キー情報 送信フラグ
1 2024-12-11 18:06:10 A0000 1
2 2024-12-11 18:15:12 A1234 0
3 2024-12-11 18:15:12 A1235 0
4 2024-12-11 18:15:12 A1236 0
5 2024-12-11 18:28:20 A1234 0

Aジョブのサイクルが短いため、Bジョブが動く間に
2行目と5行目のように、同一キーが作成される場合があります。

事象解説

上記システムを想定して、時系列で処理を確認していくことで、どういう事象が発生しているのか確認していきます。下図の太い矢印は、それぞれのバッチのトランザクションを表しています。

ファントムリード発生

① データ作成(18:15開始分)

この時点でのデータ連携トリガーは、下記の通り

# データ連携日 キー情報 送信フラグ
1 2024-12-11 18:15:12 A1234 0
2 2024-12-11 18:15:12 A1235 0
3 2024-12-11 18:15:12 A1236 0

② 処理対象データ抽出

連携トリガーテーブルから、処理対象となるデータを抽出しています。
①の段階でコミットが完了しているので、Bバッチからはすべて参照できています。

③ 外部システムへデータ送信

連携トリガーテーブルのキー情報に一致するデータを、外部システムへ送信します。

④ データ作成(18:28開始分)

Bバッチが終了する前に、Aバッチが開始され終了してしまいました。
この時点でのデータ連携トリガーはで、①で更新したレコードと同じキーが登録されています。
コミットされているので、Bバッチからもこれ以降は参照できます。

# データ連携日 キー情報 送信フラグ
1 2024-12-11 18:15:12 A1234 0
2 2024-12-11 18:15:12 A1235 0
3 2024-12-11 18:15:12 A1236 0
4 2024-12-11 18:28:20 A1234 0

⑤ 連携トリガーテーブルのフラグ更新

処理が完了したデータのフラグを更新します。

②で処理対象として抽出し、③のメイン処理を実施したのはどのデータでしょうか?
①で作成した3行分になります。④で追加された1行は、コミット前のため③時点ではBバッチからは確認できません。

ここで、「A1234」は処理済みだからフラグを立てて…としてしまうと
最新のデータであるはずの4行目が連携されずに、送信フラグを立てられてしまう。

# データ連携日 キー情報 送信フラグ 備考
1 2024-12-11 18:15:12 A1234 0
2 2024-12-11 18:15:12 A1235 0
3 2024-12-11 18:15:12 A1236 0
4 2024-12-11 18:28:20 A1234 0 ※この対応データは未処理なのでフラグ更新NG

※ Aのトランザクションでコミットした追加レコードがBのトランザクション途中で見えてしまう。(ファントムリード)

システム日時より過去のはずなのに、抽出されなかった

ファントムリードが発生してしまうなら、日時項目で処理開始時点で抽出対象としたデータのみ反映すればいいんだなということで、システム日時を取得してデータ連携日で抽出するようにしました。

① データ作成コミット

ここは前述のパターンと同じです。
データ連携日に入る値は、システム日時を設定しているため、トランザクション開始時点の日時となります。

# データ連携日 キー情報 送信フラグ
1 2024-12-11 18:15:12 A1234 0
2 2024-12-11 18:15:12 A1235 0
3 2024-12-11 18:15:12 A1236 0

② システム日時取得

Bバッチは、処理開始した時点でシステム日時を取得し、どこまでのデータを抽出対象としたかを明確にするようにしました。
ここでは、18:30の時刻を取得したとしています。

③ 処理対象データ抽出

②で取得したシステム日時も条件に含めて、連携トリガーテーブルから処理対象となるデータを抽出しています。
18:30より過去のデータなので①で作成した3レコードは抽出対象となります。

④ 外部システムへデータ送信

連携トリガーテーブルのキー情報に一致するデータを、外部システムへ送信します。

⑤ データ作成コミット

Bバッチが終了する前に、Aバッチがレコードを追加作成してしまいました。
データ連携日は18:28ですが、あくまでトランザクション開始日時です。

コミットの時刻が18:35だったと仮定するとわかりやすいかもしれません。
18:28~18:34ごろの間に、レコードは作成されていますが
コミットされていないので他からは参照できません。

18:35のコミットで、他から参照できるようになりますが…
この直後に参照したトランザクションからすれば、過去の時刻が登録されているように見えます。

# データ連携日 キー情報 送信フラグ
1 2024-12-11 18:15:12 A1234 0
2 2024-12-11 18:15:12 A1235 0
3 2024-12-11 18:15:12 A1236 0
4 2024-12-11 18:28:20 A1234 0

⑥ 連携トリガーテーブルのフラグ更新

処理も完了したので、フラグを立てようとします。
事前に取得していたシステム日付より以前は処理済みという意識で、フラグを立ててしまうと
4行目のデータまで、巻き添えで処理してしまいます。

「② システム日時取得」で取得したシステム日時は、18:30です。
しかし4行目のデータ連携日は18:28です。
抽出対象となってしまいます。

どうするか?

このシステムの例としては…

処理したレコードをちゃんとキー指定で更新する
のが基本とは思うが、同一キーが大量に登録されてきたり、件数が大きかったりすると
あるていどまとめて、更新したかったりする。

その場合は、
処理対象日時を管理するような、制御テーブルなど作るとかも考えられるが
データ連携処理日のmaxをとるだけでも、この場合は十分かも。

こういう事象が発生する場合がある、という紹介をメインにしたかったので
解決策については、ざっくり済ませてしまっていますm(_ _"m)

おわりに

久々に記事書いたので筆が進まないというか
どう表現すればよいいのか、なかなか悩んだ。
文才ないので、これで伝わるのかどうかも不安ですが…

また、検証しながら、推測しながらですので
そもそも間違ってるよ!!とか分かりづらい!!とかあったら、指摘ヨロシクです。

Discussion