理系学生日記

おまえはいつまで学生気分なのか

バッチで大量更新を行う際の設計パターン

次のようなよくある前提があるとして、じゃぁどのように設計すればいいのかという話です。 +24 時間サービスであり、エンドユーザがどの時間帯でもサービスを利用している +エンドユーザ用サービスが参照するテーブルに対し大量の(例えば数十万〜数億レコード)をバッチで更新する +不整合があるデータをエンドユーザから可視状態にしてはならない

一括コミットが楽なのだけれど、大量のデータの一括コミットは禁じ手です。だいたいの場合は UNDO 領域が枯渇して、おなじみの ORA-1555 なんてのがアラートに上がって夜中に起こされる羽目になり、世の中を恨みながら暮らすようになります。 一括コミットが禁じ手であれば、分割コミットしかありません。UNDO 領域が枯渇せず、かつ、commit の負荷が大きくならない程度に、更新対象レコード数を小分けにしてコミットしていくことになります。

ここで問題となるのが、前提の 3 番です。 コミットするということは、部分的に更新された分がエンドユーザに可視になります。つまり、このときにユーザから見えるテーブルはバッチによって「更新されたレコード」と、「後から更新されるけど今はまだ更新されていないレコード」が入り交じった不整合のある状態となります。これがサービス上問題になるケースも、問題にならないケースもあるでしょうが、経験的には問題になることが多いです。 つまり、部分コミットしないといけないにも関わらず、不整合のある状態は許容できないというジレンマがここで生じることになります。

このときのパターンとしてぼくが良く使うのが、カラムの 2 面持ち、あるいは、テーブルの 2 枚持ちです。このパターンでは、同じ役割を持つカラム、あるいはテーブルを 2 つ作成しておき、エンドユーザが参照する側(Active 面)とバッチが更新する側 (InActive 面) を分けるようにします。 バッチは InActive 面に対して、少しずつ部分コミットで更新をかけていきますが、このときエンドユーザ用のサービスは Active 面を見ているので、不整合な状態は見えません。バッチの処理が完了し、InActive 面に不整合がなくなった段階で、Active 面と InActive 面を入れ変えます。こうすることで、エンドユーザ用のサービスはバッチによって更新された最新かつ整合性の取れたデータを参照できるようになります。 InActive 面と Active 面を入れ替える制御は、「今どちらの面が Active なのか」を示すような制御テーブルがあれば事足ります。バッチの処理完了のタイミングで、この制御テーブルに対する更新 + コミットをかければ実現できます。

カラムの 2 面持ちをするのか、テーブルの 2 枚持ちをするのかというのは好みの問題も含まれますが、個人的にはカラムの 2 面持ちをするのが好きです。