【書き起こし】共通QRコード決済システムの裏側 – 青木 太郎 【Merpay Tech Fest 2021】

Merpay Tech Fest 2021は、事業との関わりから技術への興味を深め、プロダクトやサービスを支えるエンジニアリングを知れるお祭りで、2021年7月26日(月)からの5日間、開催しました。セッションでは、事業を支える組織・技術・課題などへの試行錯誤やアプローチを紹介していきました。

この記事は、「共通QRコード決済システムの裏側」の書き起こしです。

それでは「共通QRコード決済システムの裏側」というタイトルで発表を始めたいと思います。よろしくお願いします。

去年の2月、メルペイは、株式会社NTTドコモさまとの業務提携を結びました。この活動の一環として、メルペイ、d払い、どちらのアプリからでも決済ができる共通QRコードが利用できるようになりました。今回のセッションでは、この共通QRコード決済システムの構成や仕組み、システムの性能を向上させる取り組みについてご紹介します。

自己紹介

私の名前は、青木太郎と言います。IDは、ktrです。コード決済チームでバックエンドエンジニアとして働いています。このチームでは、名前のとおりQRコード決済や、バーコード決済を扱うシステムを開発しています。

QRコード決済におけるお金の流れ

ではまず、共通QRコード決済の仕組みについて見ていく前に、従来のメルペイのQRコード決済におけるお金の流れを見ていきましょう。

従来のQRコード決済では、登場人物が大きく3人います。まず、決済サービスを提供しているメルペイです。次に、メルペイを利用しているお客さまがいます。お客さまは、メルペイが使えるお店で決済を行います。このお店のことを加盟店と呼びます。そしてメルペイは、イシュアーと、アクワイアラーという2つの役割を持っています。

イシュアーは、お客さまの残高をもとに決済処理を行う事業者です。お客さまからの加盟店さまへの支払いを受け付け、処理します。イシュアーは、お客さまの残高の管理に責任を持っています。また、決済のほかにも残高のチャージや、お客さま向けの決済履歴の管理も行います。例えばメルペイの場合、メルカリアプリから決済や決済履歴の確認、チャージなどを行うことができます。

アクワイアラーは、決済サービスを使えるお店、つまり、加盟店さまを増やす事業者です。加盟店さまの売上金の管理について責任を持ち、加盟店さまへの入金などもアクワイアラーが行います。また、加盟店さま向けの管理画面の提供も行っています。管理画面では、決済履歴の確認や返金ができます。

共通QRコードコード決済におけるお金の流れ

ここまでざっと、従来のQRコード決済の流れを見てきました。では次に、d払いと共通QRコード決済での流れについて見ていきましょう。

従来のQRコード決済では、メルペイが、イシュアーと、アクワイアラーの、どちらの役割も果たしていました。それに対し共通QRコード決済では、d払いがイシュアー、メルペイがアクワイアラーとしての役割を果たしています。

イシュアーであるd払いは、d払いを利用しているお客さまの残高を管理しています。決済処理は、この時点で行われます。決済が成功した場合、アプリに決済完了画面を表示します。アクワイアラーであるメルペイは、加盟店さまの売上金を管理しています。アクワイアラーは、イシュアーから確定した決済データを受け付けます。決済データを受け取ったあと、加盟店さまの残高と決済履歴へ反映を行います。最終的にアクワイアラーは、加盟店さまの売上を入金します。

決済処理

実際の決済、返金処理についてもう少し掘り下げて説明していきます。

決済処理では、大きく2つの機能で構成されています。1つ目は、加盟店さまのステータスを公開し、決済ができるかどうかを判断するための機能です。2つ目は、イシュアーで確定した決済の情報を受け付ける機能です。ではまず、1つ目の機能の、加盟店での決済可否について紹介します。

加盟店での決済可否

特定のお店で決済ができない場合や、決済をできないようにしたい場合があります。例えばその加盟店さまが、既にメルペイを解約されていた場合や、何らかの理由で決済制限がかけられている場合などです。こういった加盟店さまに関するステータスは、アクワイアラーであるメルペイが持っています。ステータスは、加盟店ステータスと、決済ステータスの2種類があります。加盟店ステータスでは、その加盟店さま自体が正常に稼働しているかどうかを表しています。また、決済ステータスでは、その加盟店さまで実際に決済を行えるかどうかを表しています。そのためイシュアーはまず、その加盟店さまで決済ができるかどうかをアクワイアラーであるメルペイに確認しています。ステータスが正常でない場合、決済を行うことはできません。

決済通知の受け付け

次に、決済通知の受け付けを行う機能についてです。

決済可能な加盟店さまだった場合、イシュアーは決済処理を行います。この処理にはメルペイは一切関与せず、決済が確定したという情報のみを受け取っています。

決済確定処理は、ステートマシンを中心とした設計で実装されています。このステートマシンは非常にシンプルで、決済確定中と、決済確定済みという状態のみが存在しています。初期状態は、決済確定中という状態です。決済通知を受け付けたあと決済確定処理が行われ、決済確定済みという状態へ遷移します。

返金処理

次に、返金処理について見ていきましょう。

返金処理は、大きく2つの機能で構成されています。1つ目は、加盟店さま向けの管理画面から返金を行う機能です。2つ目は、イシュアーへの返金リクエストです。ではまず、加盟店さま向けの管理画面から返金を行う機能について紹介します。

加盟店向け管理画面からの返金

加盟店さま向けのサービスを提供するのは、アクワイアラーであるメルペイの役割です。そのため、メルペイの加盟店さま向け管理画面からd払いでの決済を確認することができます。この画面から返金を行うこともできます。

イシュアーへの返金リクエスト

次に、イシュアーへの返金リクエストについてです。

管理画面で返金を行うと、このようなフローで処理が行われます。まず、メルペイのサーバへリクエストが到達します。次に、返金対象の決済に対応するイシュアーへ返金リクエストを行っています。最終的に、お客さまへの返金処理はイシュアーによって行われます。

返金は、イシュアーとのネットワークをまたいだ通信が発生するため、決済より複雑なフローになっています。そのため返金では、TCCパターンを取り入れたインターフェースを利用しています。TCCパターンは、複数のサーバ間でデータの一貫性を保つパターンの1つで、TCCは、それぞれTry-Confirm/Cancelの略です。TCCパターンでは、トライというフェーズと、コンファーム、もしくはキャンセルというフェーズの2つで構成されています。

トライフェーズでは、実現したい処理ができるように準備をします。返金処理であれば、事前条件をチェックし、返金ができる状態にします。トライフェーズがすべて正常に完了した場合、コンファームフェーズに移ります。このフェーズでは、トライフェーズで準備したものを確定させ、処理を完了させます。返金処理であれば、ここで返金が確定することになります。もし、トライフェーズ中に何らかの問題が発生した場合は、コンファームフェーズに移ることはできません。その場合、コンファームフェーズではなく、キャンセルフェーズに移り、トライフェーズで準備したものすべて開放します。返金処理であれば、返金ができる状態をキャンセルします。

TCCパターンを使うと、問題が起こったときの対処がシンプルになります。TCCパターンを使わないで、返金処理を実装することを考えてみます。メルペイでの返金処理は成功したものの、何らかの理由でイシュアーでの返金処理が失敗したものとします。そうすると、メルペイでは返金が完了しているにもかかわらず、イシュアーではまだ返金されていないといった現象が発生します。

次に、TCCパターンをベースに実装されている場合です。この場合、メルペイは返金を確定させるのではなく、返金準備のみを行います。イシュアーの返金処理が失敗した場合、キャンセルフェーズに移ります。すなわち、返金のために準備したものをすべて開放し、返金処理をキャンセルします。

TCCパターンのそれぞれのフェーズも、ステートマシンの状態として表現されています。初期状態は返金中という状態です。返金が成功した場合は、返金済みという状態に遷移します。しかし中には、既に返金期限切れになっているような返金できないケースが存在します。このような事前条件を満たしていない、返金リクエストの場合、返金は恒久的に成功しません。そのため、返金を諦め、キャンセル中という状態に遷移することで返金をキャンセルします。返金のキャンセルが確定すると、最終的にキャンセル済みという状態へ遷移します。

可用性を高めるアーキテクチャ

次に、このシステムが特に重要視している、可用性を高めるアーキテクチャについて紹介します。

このシステムでは、非同期処理を中心としたリトライセーフなAPIを提供しています。ではまず、非同期処理の部分について紹介します。

非同期処理を中心としたAPI

同期処理が存在すればするほど、可用性は低下していきます。そのため、基本的に同期処理にしなければいけない箇所以外は、すべて非同期処理で行っています。

例えばお金を減らす処理は、一貫性を担保するために同期的に行わなければいけません。具体的には、決済時にお客さまの残高を減らすような処理や、返金時に加盟店さまの残高を減らすような処理です。これらの処理が非同期で行われた場合、決済が成功したのにお客さまの残高が減っていなかったり、返金が成功したのに加盟店さまの残高が減っていないというような一貫性のない状態となってしまう可能性があります。

逆に、お金を増やす処理は非同期的に行っています。具体的には、決済時に加盟店さまの残高に売上を反映する処理や、返金時にお客さまの残高へ返金額を反映する処理です。非同期にすることで一時的に反映が遅延してしまうことがありますが、最終的に整合性のとれた状態となります。

また、決済や返金に直接関係しない、メール送信などの処理もすべて非同期で行っています。そのため、これらの処理が決済や返金のパフォーマンスに影響したりすることもありません。

リトライセーフなAPI

次に、このAPIが持つリトライセーフという性質について紹介します。

リトライセーフなAPIとは、どんなタイミングであっても何度でも安全にリトライできるAPIを指します。例えば、新しくリソースを作成するリトライセーフなAPIがあったとします。このリトライセーフなAPIは、何度リトライされたとしても、作成されるリソースは常に1つだけになるような性質を持っています。こういった性質は、冪等(べきとう)性とも呼ばれます。

APIに冪等性があると、多くの恩恵を受けることができます。クライアントは、リソースが多重に作成されるといった副作用を気にせずにAPI呼び出しができます。そのため、クライアントのロジックは非常にシンプルなものとなります。リトライを行いたい場合も、常にリソースが作成されているかどうかを考えずに行うことができます。また、クライアントの呼び出しがexactly-onceな実行とは限りません。例えば、GCPのCloud Pub/Subや、Cloud Functionsは重複して実行される可能性があるため、APIを冪等に作成していない場合、意図しない挙動となる可能性があります。

冪等性のないAPIとリトライについての例

冪等性のないAPIとリトライについて例を見てみましょう。

クライアントがサーバに対して何らかのリソースを作成するAPIを呼び出します。サーバはそれを受け付け、リソースを正常に作成します。しかし、レスポンスを返却する途中で何らかのエラーが発生したとします。

クライアントは、リトライするために再度API呼び出しを行います。しかし、このサーバのAPIには冪等性がないため、これによって二重でリソースを作成してしまうことになります。

冪等性のあるAPIの場合、リソースは常にただ1つしか作成されません。そのため、既にリソースが作成されていた場合、そのリソースをそのまま返却し、新たにリソースを作成することはありません。

どのようにして冪等性を担保しているのか

では次に、どのようにして冪等性を担保しているのかを紹介します。この仕組みは、先ほど紹介した決済返金のステートマシンを使って実現されています。それぞれのリソースを作成するAPIは、冪等キーというクライアントが生成したランダムな値を受け取ります。ここではdeadbeefという値が使われています。この冪等キーを用いて、冪等性を担保する仕組みを実現します。

決済通知リクエストを受け付けられなかった場合を見てみましょう。この場合、リソースはまだ作成されていないので、安全にリトライすることができます。リソースの作成を行うときは、渡された冪等キーと、初期状態である決済確定中という状態を一緒に記録します。リソースの作成後、決済確定処理で失敗した場合のリトライを考えてみましょう。

同じ冪等キーでリトライすれば、それに紐づくリソースを取得できます。また、記録した状態から、どこまで処理が進んでいたのかを判別することができます。これにより、重複してリソースを作成することなく安全に処理を再開することができます。リトライされたときに、既に終了状態である決済確定済みになっていた場合、何も処理を行わずにこのリソースをそのまま返却することができます。

整合性を担保する取り組み

次に、整合性を担保する取り組みについて紹介します。

整合性を担保するために、おもに2つのことを実施しています。1つ目は、バッチによるデータ不整合の修復。2つ目は、決済・返金のリコンサイルです。ではまず、バッチによるデータ不整合の修復についてです。

バッチによるデータ不整合の修復について

パッチによるデータ不整合の修復は、数分程度の非常に短いインターバルで実行されています。これは、なるべく加盟店さまとお客さまの体験を損ねないようにするためです。例えば決済通知時にデータ不整合が発生した場合、修復されるまで加盟店管理画面から決済を確認することはできません。また、返金時にデータ不整合が発生した場合、修復されるまでお客さまの残高がもとに戻りません。このような状況は非常に混乱するため、できる限り最速でデータ不整合を修復できるようにしています。バッチ処理では、中間状態にある決済や返金を対象に、それらの処理をリトライし、正常に処理を完了させます。このリトライ処理は、APIのリトライモデルと全く同じように実現することができます。

例えば決済であれば、決済確定中のものを決済確定済みになるようにリトライします。また、返金であれば、返金中のものと、キャンセル中のものをリトライします。

決済・返金のリコンサイル

次に、リコンサイルについてです。

リコンサイルとは、2つのシステム間でデータの整合性がとれているかを確認することです。

リコンサイルは、アクワイアラーとイシュアーの間で行われます。リコンサイルの対象となるのは、前日発生したすべての取引です。決済返金を行うイシュアーの持つデータを正として、アクワイアラーがイシュアーから取引データを取得します。取得したデータがアクワイアラーの持つデータと一致するかどうかをチェックします。例えば取引の成功可否、決済金額や返金金額、決済店舗といった項目をそれぞれチェックします。

リコンサイルによって、アクワイアラーに存在しない決済が見つかることがあります。これは、システムが非同期処理中心のアーキテクチャを取り入れているためです。この場合、メルペイは新たに決済データを作成します。これにより、加盟店管理画面に正常に決済データが表示されるようになります。

つまり、非同期処理ベースのアーキテクチャとバッチ、リコンサイルによるデータ整合の修復により、一時的にメルペイが利用できなくなったとしても、可用性と整合性を損ねずに決済を行うことができています。

まとめ

まとめです。

従来のメルペイのQRコード決済では、メルペイがイシュアーとアクワイアラーの役割を担っています。イシュアーは、お客さまの残高を管理しており、決済処理を行う事業者です。アクワイアラーは、加盟店さまの売上金を管理しています。また、加盟店さま向けの管理画面を提供し、決済履歴や返金の提供も行っています。

これに対し共通QRコード決済システムでは、イシュアーとアクワイアラーがそれぞれ異なる事業者となっています。今回の場合、イシュアーはd払い、アクワイアラーはメルペイとなっています。お客さまは、イシュアーのアプリから決済を行います。イシュアーで決済が確定したあと、決済確定通知がアクワイアラーに非同期で連携されます。最終的にアクワイアラーは、売上金を加盟店さまに入金します。そして、このシステムは同期処理が必須なところ以外は、すべて非同期処理で行われており、できる限り可用性を損ねないようなアーキテクチャとなっています。

決済や返金は、ステートマシンベースでモデリングされており、ステートマシンを利用してリトライセーフなAPIが提供されています。

バッチによる未完了処理のリトライは、整合性を担保する仕組みです。このリトライ処理もステートマシンを利用して行われます。バッチ処理は、加盟店さまとお客さまの体験をできる限り損ねないために、非常に短いインターバルで実行されています。

また、リコンサイルも整合性を担保する仕組みの1つです。イシュアーのデータを正として、イシュアー、アクワイアラー間のデータ不整合を修復します。これらの仕組みによって、シンプルなアーキテクチャながらも非常に高い可用性と整合性を持つシステムが構築されています。

発表は以上となります。ご清聴いただき、ありがとうございました。