この記事は、 Mercari Bold Challenge Monthの5日目の記事です。
こんにちは。株式会社メルペイでNFCサービス開発を担当している @fujimon です。
メルペイでは2019年2月にNFC決済サービスがリリースされました。このNFC決済は、株式会社NTTドコモが提供している非接触型決済サービス「iD」に対応しており全国の「iD」加盟店で利用が可能となってる決済手段です。
このNFC決済を運用する上では、事業提携先とNFC決済を管理するマイクロサービス(nfc-service)、決済基盤マイクロサービス(payment-service)、3つの決済データの整合性が保たれていることが重要になります。
本記事ではNFC決済データをどのよう仕組みで整合性を保っているのかをまとめました。
NFC決済の流れ
まずNFC決済が開始されてからの一連の処理の流れを見ていきます。
Shop | NFC決済が行われ加盟店に設置された決済端末からリクエストが行われます |
決済NW | 事業提携先の決済ネットワークがリクエスト情報から売上、取消なのか判別しメルペイへ決済リクエストします |
GW | nfc-serviceへプロキシします |
nfc-service | 決済リクエスト情報から決済処理をします(エンドポイント) |
payment-service | nfc-serviceから決済依頼を受けます |
大まかにこのような流れになります。
決済NWからのリクエスト種別には売上、取消の他に障害取消という特別なリクエストがあります。決済NWのリクエストにはタイムアウト値が定められており、規定値を越えると無条件でShop側へ決済失敗の結果が通知されます。その際にタイムアウトした決済を無効にするための障害取消リクエストが届きます。障害取消は元決済が売上であれば取消、取消であれば売上に戻す必要があります。
不整合が起きるケースとしては、決済NW ⇔ nfc-service間の処理途中でタイムアウトするケース、nfc-servicel ⇔ payment-service のマイクロサービスを跨ぐトランザクションでタイムアウトするケースなどがあり、この辺りでどのように整合性を保つか考える必要がありました。
決済トランザクション
次にnfc-serviceでどのような決済トランザクションで整合性を保つようにしているか説明します。
決済トランザクションは次の仕様を考慮して実装しています。
- 不整合が起きた決済をあるべき状態にするため、どの時点まで処理できていたか把握できる
- DBトランザクション内で別マイクロサービスへのリクエストを行うとロック時間が長くなることやエラーハンドリングなど考慮すべきことが増えるため、DBトランザクション内はDB依存処理のみに集約させる
このような仕様の実現ため Write Ahead Logging (WAL) 手法を参考に決済トランザクションの仕組みを考えました。WALの考え方としては最初にUndo, Redo用の情報を含んだログデータを書き込み、必要なタイミングで適宜データ更新をしていきます。実際のロジックではログデータにあたる決済履歴を最初に書き込むことでエラーが起きた場合のRollback処理のハンドリングが可能になってます。
別案として、パフォーマンスを考慮して決済処理自体をCloud PubSubを経由し非同期処理させる案もありましたが次の理由で見送りました。
- NFC決済以外に決済手段があるため、決済リクエスト時は残高が足りていても決済実行時には残高不足の可能性があり実装が難しい
- キューのシステム自体に障害が発生した場合やキューイング状態で障害が起きた場合の調査、復旧の難易度が上がる
- 返金も非同期になるため、店頭レジで返品してその取消金額と残高を合わせて決済をしようとした時に未返金で残高不足になる可能性があり、お客様にとって良くない体験となってしまう
ユースケース
では、実際にNFC決済でどのようなユースケースがあるか見ていきます。
正常なケース
決済リクエストを受けて、正常に処理でき決済が成功するケースです。
決済履歴の作成と更新は別々のDBトランザクションで処理しています。
Rollbackが必要なケース
決済処理の途中で失敗するケースです。
「決済履歴を作成」までの失敗であれば特に整合性を気にする必要はありませんが、以降の処理で失敗した場合はデータ整合性を保つためのアクションが必要になります。
整合性を担保するためのアクション
前段で処理した分だけRollbackで同期的に整合性を保ちます。
payment-serviceへの売上リクエストが成功していれば取消リクエスト、DBに対しては決済履歴を削除します。
Rollbackのいずれかの処理が失敗するケースもありますがその場合は中断して終了します。この場合は後述するRepair処理によって非同期でRollbackされます。
タイムアウトするケース
決済処理の途中で決済NWがタイムアウトするケースです。
決済NWのリクエストにはタイムアウト値が定められており、規定の値を越えると無条件でShop側へ決済失敗の結果が通知されます。そして通知と同じ契機で障害取消リクエストが届きます。
整合性を担保するためのアクション
決済NWタイムアウトは検知できないためNFC決済処理には決済NWタイムアウト値より少なめなタイムアウト値を設定し、先にタイムアウトさせることで決済の整合性を保ってます。タイムアウトした決済履歴はRepair対象となり売上リクエストが成功していれば取消リクエストを実行し、決済履歴を削除することであるべき状態に修復します。
また、決済NWへレスポンス中にタイムアウトするケースもありますが、その場合は決済NW側が障害取消リクエストを投げることで整合性を保ってます。障害取消リクエスト処理中に失敗した場合は後述するRepair処理によって非同期でリトライされます。
障害取消リクエストが決済リクエストを追い抜くケース
何らかの要因(GCP障害など)でネットワークが不安定になり決済リクエストより障害取消リクエストが先に届くケースです。(決済リクエストは届かず障害取消リクエストのみ届くこともあります)
整合性を担保するためのアクション
障害取消リクエスト時点には取消対象の履歴が存在しないので障害取消のRepair対象としての履歴を残し、後述するRepair処理によってリトライされます。
Repair
同期的に修復できない問題が起きた場合に非同期であるべき状態へ修復を行うバッチです。Rollback処理中の失敗、タイムアウト、取消対象の履歴が見つからないケースなどが対象にあたり売上、取消、障害取消で修復内容が異なるためバッチ内では異なるWorkerが修復を実施しています。
バッチ内で実施している作業は次の2点です。
-
payment-serviceへ取消リクエストが必要かどうか
決済履歴からどの時点で失敗したのか把握できても、payment-service側の決済履歴がどのような状態かはnfc-service側では把握できません。そのため冪等キーを使いpayment-service側から決済履歴を取得し取消リクエストが必要なのかを判断し修復を実施します。
※ 冪等キー(Idempotency Key)は、冪等性を担保するためpayment-service APIの利用側が決済時に発行しなければならいユニークキーです。 -
DBのRollback
決済履歴からどの時点まで処理できていたか判断し、Rollbackを実施します。
Reconcile
関連サービス間で確認したいデータを突合させて整合性が保たれているか確認する作業です。
NFC決済の場合は決済NW事業者より前日分の決済履歴データをいただき、日次バッチで決済NW事業者とnfc-service、payment-service間で決済履歴を突合させデータの整合性を確認しています。不一致があればRepair対象としての履歴を残し、Rollbackを実施します。
おわりに
NFC決済のデータ整合性に対するアプローチの概要を紹介させていだきました。同期、非同期処理以外にも障害取消リクエストを組み合せた仕組みにすることに特徴があったかと思います。「データ整合性」について考えているエンジニアにとって、 わずかでも参考にしていただければと思います。
もっと詳細を聞きたい方やデータ整合性に興味があったりする方は
「Backend Engineer’s meetup ~SIer経験者が語る自社サービス開発の魅力~」
というイベントにパネラーで参加しますのでぜひ参加ください!!
次の記事は、@lainra による「Standardizing network architecture across microservices」です。
お楽しみに!