メルペイにおけるお客さま残高の管理手法

はじめに

この記事はMERPAY TECH OPENNESS MONTHの19日目の記事です。
こんにちは。株式会社メルペイのPayment Platformチームでエンジニアをしている knsh14 です。

メルペイの様々な決済手段でどのように決済の整合性を保とうとしているかは15日目の記事で紹介しました。
メルペイでは決済の手段をもつマイクロサービス(Payment Service)と決済のための原資を管理するマイクロサービス(Balance Service)を分けています。
今回はこのお客さまの残高を正しく管理するためのマイクロサービスがいかにして正確に増減をしているかを説明します。

Balance Service の提供する機能

Balance Service ではお客さまの残高を増減させるという機能を提供しています。
決済のフローや他のマイクロサービスとの整合性の管理などは上位レイヤーのサービスであるPayment Serviceで処理を行っています。
Balance Serviceでは「1つの取引において残高を増減させる」という機能に集中しています。
お客さまの残高にはメルペイ残高、売上金から購入されたり先日のゴールデンウィークに行われたポイント還元で付与されたポイントなどいくつかの種類があります。
これらを一つの取引毎に増減させる必要があります。

実装の方法にはいくつか考えられますが、以下のように残高の種類ごとに追加、消費などのメソッドを実装する方法が一番シンプルです。

  • 無償ポイント
    • 追加
    • 消費
    • 失効
  • 有償ポイント
    • 追加
    • 消費
    • 失効
  • メルペイ残高
    • 追加
    • 消費
    • 失効

この方法で実装すると以下のようなメリットがあります。

  • どの残高でも似たような処理を行うので、共通化がしやすくなる
  • それぞれの残高の処理が独立なので、残高の種類の追加や削除が他に影響を与えない

ですがこの方法にはメルペイでは採用できなかったデメリットもあります。

  • それぞれの残高の処理が別々なので一つの取引で複数の種類で決済する場合に複雑になる

メルペイでは決済時にポイントとメルペイ残高を合わせて支払うことができます。
そのようなケースでは「ポイントの支払は成功したが、残高の支払いは失敗した」という状況が起こりえます。
この場合に決済がなかった状態に戻すには正常に終了したポイントの消費を取り消す処理をしなければいけません。以下のような状況を考慮する必要があります。

  1. 消費したポイントをお客さまに返却するが、その時点で有効期限が切れていないか確認する
  2. ポイント消費は正しく終了していたはずなので、利用履歴などが残っているが、これらを正しく削除する
  3. 1、2 の処理がそれぞれ失敗する
    これらの状況を考慮した実装にするのは非常に難しいため、Balance Serviceでは別の方法を採用しています。

Balance Service が採用している方法は追加、消費、失効という大きな機能を提供します。
その中のパラメータとしてそれぞれの種類で金額がいくらという指定をします。

この方法では以下のようなメリットがあります。

  • 最初の方法で問題になった複数の残高で決済する場合に、全てを1つのDBトランザクションの中で処理することができる
    • そのためどこかで失敗した場合にはDBのロールバック処理で全てを安全に元の状態に戻すことができる

ただこの方法でもデメリットはあって以下のことで最近は苦しんでいます。

  • 残高の種類の組み合わせ爆発が起きるので、テストが大変

消費について

消費は残高を減らす前に実際に減らすことが可能か確認する必要があります。
買い物をする前に財布の中身を確認する様な感じです。
実際に支払える金額があれば、その金額を仮押さえという形で状態を変えます。

その後別のリクエストとして実際に消費するリクエストを受けてお客さまの残高を減らします。

冪等性の担保

15日目の記事でも説明されていたように、決済のステートマシンはフェーズ毎に複数回リトライをすることを前提に作られています。参考
そのため、Payment Serviceからのリクエストを受けるBalance Serviceも1度の取引に対して複数回リクエストを受けることを想定した作りにする必要があります。
全てのリクエストが別々の取引であると仮定して処理すると、Payment Serviceでは、リトライしているつもりでも、Balance Serviceとしては何回も同じ操作を繰り返し、多重付与や多重決済の原因になります。
それを避けるためにリクエストごとではなく取引毎に異なるIDを付与してもらう必要があります。
そのIDが既に処理済みの取引IDであれば、実際に処理しなくても、あたかも処理したかのように結果だけを返せばよい仕組みです。

その際に利用されるのが、Idempotency Key です。
冪等性キーはその名の通りリクエストにおける冪等性を担保するためのIDです。
同じ冪等性キー、同じリクエストパラメーターならば同じリクエストだと認識できます。
なので、処理は最初に成功したときだけ行われ、結果はDBに保存されます。
それ以降は実際に処理をせずに同じ結果だけを返すようにしています。
逆に同じパラメーターを持ったリクエストが来ても異なる冪等性キーを持つ場合、別々の取引として扱います。

こうすることでBalance Serviceを利用する他のマイクロサービスは安心してリトライすることができるようになります。

お客さまの残高の整合性を保つ

Idempotency Keyによってリクエストの冪等性は担保されるようになりました。

Balance Serviceではデータ整合性や、お問い合わせ調査、他のマイクロサービスとのデータ突合処理のために適切に処理の履歴を保存する必要があります。
ここからはBalance Serviceがお客さまの残高をどのような仕組みで変更し、記録しているかをお話します。

Balance Serviceでの残高の操作は全て3種類に抽象化されると説明しました。
これらの操作が行われる毎に、異なる2種類の履歴にレコードを追加することで、お客さまの残高の移動が正しく行われたかを確認することができるようにしています。
この履歴を利用して、経理グループと正しく決済できているか確認しています。

  • 操作の種類と金額の絶対値を保存する方法
  • 金額が変動したあと、お客さまがいくら持っているかを記録する方法

それぞれについて説明していきます。

操作の種類と金額の絶対値を保存する方法

それぞれの種類の残高が変動するたびに、お客さまのIDや取引の種類や取引額、取引のIDなどと保存しています。

お客さまの今の所持金額を知るためには、これの履歴を全て足し合わせれば正確に値を出すことができるはずです。

図にすると以下のようになります。

f:id:knsh14:20190613151746p:plain

この方法ですと、差分の積み上げを計算するだけなので、任意の期間にいくら決済されたかも簡単に知ることができます。

金額が変動したあと、お客さまがいくら持っているかを記録する方法

それぞれの種類の残高が変動するたびに、取引のIDなどと共に今お客さまがどれくらいの残高を持っているかのスナップショットを保存しています。
それと同時に直前のスナップショットのIDも保存しています。
こうすることで最新から順々にたどることで途中でおかしな履歴がないか、履歴間の差分が正しいかを確認することができます。

f:id:knsh14:20190613151823p:plain

最後に

お客さまの残高を正しく管理することはメルペイに限らず全てのサービスにおいて最重要の課題であると言えます。
メルペイでは冪等性によってリクエストの同一性やリトライへの対応を担保し、2種類の履歴を保存することで、なにか異常があった場合にどの取引が異常を引き起こしたのか、どの時点から影響を受けているのかをすばやく発見できるようにしていることを説明しました。
この世に完璧な仕組みはありませんが、これからも安心安全にメルペイを使っていただけるようにできる限りの方法でお客さまの残高を管理していければと思います。