マスタデータは「上書くのではなく切り替える」


要点

ソーシャルゲームにはマスタデータの存在がつきものです。

しかしながら、マスタデータを更新する際、単純に各データを順繰りに上書きしていく方式では、ユーザーがアクセスしてきたタイミング次第で不整合が起こる可能性を否定しきれません。
この問題はマスタデータを更新する際には「上書くのではなく切り替える」方式で解決できます。

マスタデータのデプロイ時にありがちな問題

通常、マスタデータは、いくつかのデータの集合を関連させた状態で表現します。これらのデータは本質的に不可分であり、その更新は完全に同時に行う必要があります。

ところが、実際の更新作業においては、技術的問題から、各集合ごとに個別に更新を行わざるを得ないことがしばしばあります。例えばMySQLでLOAD DATA INFILEを使って更新している場合、トランザクションを効かせようにも暗黙的なコミットが走ってしまうため、各テーブルごとにデータを更新せざるを得ません。

このことが実際の運用ではよく問題となります。

同時に更新できないと何が起こるのか

どういうことか例を挙げて説明します。
仮に、現在、下記のガチャ関連のマスタデータを本番環境で利用しているとします。

ガチャマスタ

id name price
1 有料ガチャ1 3000

武器マスタ

id name attack
1 有料武器1 2000

ガチャ排出マスタ

gacha_id weapon_id weight
1 1 100

ガチャマスタにリストアップされているガチャを回すと、ガチャ排出マスタに登録されている武器のいずれかが重み付けを参考に排出される仕様です。

さてここで、有料ガチャ1を3000円から5000円に値上げする代わりに、最新の強力な武器を排出するよう、マスタデータを変更することになったとします。
もしもこの更新作業を行う際、ガチャマスタを更新した後で、続いてガチャ排出マスタを更新する前にユーザーがガチャを回したらどうなるでしょうか。
もちろんユーザーに値上げ後の額を払わせた上で弱い武器を排出してしまいます。大問題です。

マスタデータを順繰りに上書きする方式をとっていると、必ずと言って良いほどこの手のデータ不整合問題に頭を悩ませることになります。
その都度メンテを入れられれば良いのですが、マスタデータの更新程度で毎回メンテを入れるのは実際には難しいですし、運用ルールを整備して対応するにしても、見落としや対応しきれないパターンが出現するリスクを否定できません。

「上書くのではなく切り替える」

この問題は、データ一式をひとまとめに取り扱うようにすることで解決できます。
つまり、マスタデータ更新の際、旧データには手をつけず、新データを一式別に用意した上で

アプリが参照する先を旧データから新データに切り替えるようにするのです。

もちろんアプリ側で処理の最中に参照先を新データに切り替えてしまったのでは意味がありませんから、処理のはじめにどのデータ一式を使うのかの情報(例えばデータバージョン)を取得しておき、同一リクエスト内では同じデータ一式を用いるよう記述する必要があります。

良いフロー

まずデータバージョンを取得する
    ↓
古いガチャマスタのデータを取得
    ↓
データ更新発生
    ↓
データバージョンを元に古い武器マスタのデータを取得 ← OK!

悪いフロー

古いガチャマスタのデータを取得
    ↓
データ更新発生
    ↓
新しい武器マスタのデータを取得 ← NG!!

若干手間がかかりますが、こうすることにより、順繰りに上書きする方式につきもののデータ不整合問題を恐れる必要がなくなります。

具体的な実現方法

さて具体的な実現方法ですが、マスタデータが全てファイルで管理されている場合は簡単です。バージョンごとにディレクトリを作り、データファイル一式をそのディレクトリに格納するようにすれば、後はアプリ側で参照先のディレクトリ名を変更するだけです。

/ver1 ← 各ディレクトリにデータ一式を全て格納する
/ver2
/ver3
verion.txt ← ここに参照するバージョンを記載する

マスタデータをデータベースで管理している場合は若干面倒ですが、各テーブルにデータバージョンを表すカラムを追加することで、こちらも同じように切り替えられるようになります。

ガチャマスタ

version id name price
1 1 有料ガチャ1 3000
2 1 有料ガチャ1 5000

武器マスタ

version id name attack
1 1 有料武器1 2000
2 1 有料武器1 2000
2 2 有料武器2 9999

ガチャ排出マスタ

version gacha_id weapon_id weight
1 1 1 100
2 1 2 100

バージョン管理テーブル

version
2

2017年時点のFGOがこの方式だそうです。ディライトワークス様の素晴らしい情報共有に感謝。ちなみにFGOでは有効期間も合わせて設定し、時限式でマスタデータを切り替えられるようにもなっているそうです。

しめ

以上のようなわけで、マスタデータを更新する際には「上書くのではなく切り替える」方式が良いと考える次第です。

追記: データを洗い替えすることになるため、更新処理を適当にエイヤで行うと大変危険です。本番適用の前にしっかりチェックできるようにしておくことが望ましいです。