複数のサービスに使われるポイント増減管理システム

## はじめに
こんにちは。課金開発セクションの藤代です。

ドワンゴではポイントを利用するサービスが複数存在します。
今回はそれらサービスから利用されている(されていた)ポイントシステムの説明をします。

## ポイントシステムとは

昨今ソーシャルゲームやポイントカード等様々なポイントが存在しています。それらポイントを「誰が」「いつ」「どのポイントを」「どれだけ」「どうしたか」という情報を管理するために作られたシステムです。ポイントシステムはユーザの保有する複数サービスのポイントの管理を行っています。現状はニコニコ漫画で利用されていますが、過去にはテクテクテクテク/Artilfe 等のサービスからも利用されていました。

提供する機能としては、以下の機能をサービスに対して提供しています。

- ポイント残高取得
- ポイント増減履歴取得
- ポイント消費
- ポイント付与
- ポイント失効
- 更新系操作のキャンセル

複数のサービスから利用される事を想定してポイントの増減に関与する機能以外提供しません。


## 複数のサービスから使われるために

まず私達はサービス毎のポイントを識別するために、どのサービスのポイントかを表す識別子としてポイント種別を定義しました。ポイント種別ごとに境界を引く事で複数のポイント種別に対応する事を実現しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31971/a6636682-e1cc-ad03-b239-a0b823559123.png)


しかし、ポイントに関するプリミティブな操作を共通化し提供しても、サービスから使って頂けるわけではありません。サービスにはそれぞれの要件が存在します。そのため利用されるためには、サービス毎の要求を実現する必要があります。要求の一例として、`サービスAでは（キャンペーン等の）期限付きポイントが存在する場合には失効期限順、そうれでなければ無料ポイント優先`で消費したい。`サービスBでは、古いポイントから消費させたい`等様々な要求が存在します。



その実現の方法一つとしてポイントシステムでは、ポイント消費時のルール指定機能を提供しています。

- ポイントの消費順序ルールの指定機能
  - ポイント付与順
  - 失効期限順
  - 有料ポイント優先
  - 無料ポイント優先
  - etc...

このようにサービスに対して、柔軟性が高いポイント増減機能を提供することで複数のサービスから利用してもらう機会に繋がりました。

ここからは、このような仕様を持つポイントシステムの実現方法について説明していきます。

## ポイント残高の計算

残高の計算はユーザのポイント増減履歴から計算されます。ポイントの増減が発生するたびに増減のデータを作成され、増減データの履歴を全て足す事で現時点での残高を出力します。以下イメージ図です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31971/556a3288-2a0e-d46a-6db1-d96161bed884.png)

図の例ですと300ptが残高として取得出来ます。実際には有料ポイント・無料ポイント等の情報も含まれているためより複雑なデータ構造になりますがここでは割愛します。

そして、これら残高計算のための増減データをユーザ公開向け用に加工しAPIで提供することで、ユーザのポイント通帳を表現するとが出来ます。


#### 積立計算型の問題【サービスの成長につれて計算処理が重くなる問題】
増減データを足し合わせていく仕様の場合、ユーザがポイント増減の操作を繰り返すことで増減データが大量に発生します。そのためサービスの成長に伴い残高計算に時間がかかるようになる問題が存在しました。そこでポイントの残高計算時、使い切ったポイントは計算しない仕様を導入して対策を行うことにしました。

#### 対策方法
様々な対策方法があると思いますが、私達は`使い切ったポイントは再計算しない`方法を検討しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31971/dae89666-e1ee-a7b6-6e0c-06f9e00bf93a.png)

（下記図）私達はポイントを使い切ったかどうかを識別するためにポイント自体にIDを振りました。（※ ポイントグループと呼びます）
ポイントグループは購入時・付与時のポイントを表す抽象概念です。これは、ポイントが増加するタイミングで一意のものが発行されます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31971/f0d16c7f-5696-f557-99ba-3c5cc943aee3.png)

ユーザが操作を繰り返すことで特定のポイントグループのポイントが0になる場合があります。ポイントグループの残高が0になった場合には、今後計算を行わないようにDB上から計算を行わない領域に移動させます。
こうしてユーザの増減操作が肥大化しても残高計算時間の増加対策を入れています。とはいえ、毎回計算するのも余計な負荷をかけることになるので、Redisによるキャッシュの導入も行っています。

### この仕様にした事で実現できた事

ポイントグループ毎に、以下のようなポイントのメタデータを付与しています。

- 有料ポイントか無料ポイントか
- ポイントの期限
- 発行日

これらメタデータを活用する事で、冒頭で紹介したポイントの消費順序ルールの指定機能提供が実現できました。ポイントの消費時に消費するポイントをポイントグループのメタデータを使うことで柔軟にコントロールすることが出来るようになりました。


##  ポイントの失効
ポイントの期限をポイントグループに付与しておくことで、残高計算時に期限が切れているポイントグループに関しては残高の計算から除外しています。そのため残高取得時はリアルタイムでポイントの失効が反映されます。また、ポイント消費に関しても消費前の残高が消費ポイント数より上回っている時のみ実行可能な処理であるため、残高計算時にリアルタイムで失効が反映される事で失効されたポイントが使わるような事はありません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31971/cfb92526-8e33-d210-ca24-afe285cb1a68.png)




## ポイントの消費と冪等性について

ポイント消費はユーザの残高を減少させる破壊的操作です。例えば、サービス等で何らかの問題が発生しリトライ処理が行われた場合、多重にリクエストされてユーザのポイント残高が想定より多く引かれてしまう問題や多く引かれることで残高がマイナスになる問題が発生する可能性があります。そのため冪等性を担保する必要があります。

### 冪等性の担保
ポイントシステムはAPIのパラメータに一意なトークンを渡してもらう事で冪等性を実現しています。この一意なトークンは、消費リクエストの冪等性を担保するためのトークンであり、サービスから何度リクエストされてもこのトークンをキーとしてリクエストの一意性を担保します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31971/d0e94141-4a2d-3d23-8ca9-88e9634b37a5.png)



また同時にユーザIDで排他処理をかけているためポイントが重複して消費される事はありません。

## ポイント購入によるポイントの付与とサポート対応について
ポイントの付与にはいくつかのパターンが存在します。

 - キャンペーンによるポイント配布
 - サポート対応でのポイント付与
 - ポイント購入によるポイント付与

これらポイント付与は基本的にポイント消費と同様の仕組みで冪等性を担保しています。
異なる点としては、ポイントの付与にはポイント購入による付与が存在する事です。購入の場合には決済が伴うためユーザのお金が関与します。よって、何らかの問題が発生した場合には返金対応が発生する場合があります。しかし、`ポイントシステムの責務はポイントの管理であるため決済や返金に関しての情報を保持していません。`そのため、ポイントの購入時のみポイントシステムは決済システムとの連携を行う必要があります。

#### 連携方法について
ポイントの購入が行われると、決済システムで`TransactionId`が発行されます。TransactionIdは、ポイントの購入を決済の一トランザクションとみなし一意に識別します。このTransactionIdをキーとしてそれぞれのシステムから情報を取得できる設計になっています。


#### サポート対応について
以下の問い合わせを例に説明します。

```
2019-○○-○○
ユーザID: xxxxxx
GooglePlayIABで課金したがポイントが反映されなかった。
GooglePlay上の決済ID:hoge
```

上記問い合わせの場合には、

 1. GooglePlay決済IDから決済情報とTransactionIdを取得
 2. TransactionIdからポイント増減情報を取得
 3. 上記1,２の整合状態を確認
 4. 1~3の結果からサポート対応

このようにしてサポート対応を実現しています。



## ツラミとこれからの課題
ここまでポイントシステムについて説明しましたが、ツラミについても記載しておきます。

#### ポイントの購入ではシステムが横断しデータが分散。トレーシングが困難
マイクロサービスでの課題になると思います。システムを分散させた結果としてデータをトレーシングする時にまだまだ面倒な処理を行っていることがあります。分散トレーシングツール等を導入していきたいです。

#### 整合状態の担保
何らかの障害でサービスとポイントシステムの整合が取れなくなった場合に復旧が非常に面倒になりがちです。例えばポイントシステムだけであればキャンセル処理を入れれば完了ですが、サービス側でも処理をロールバックさせる必要がある等復旧が非常に複雑になります。このような問題に対してもオペレーションを最小に出来るように対応していきたいです。


#### データ量について
当初の性能要求では問題なかったものの、今後ポイントシステムを利用しているサービスがより成長していった際にデータ量が増加し続けDBに限界が来る時がくる想定です。今後の課題として対処していく予定です。


## 最後に
この記事では、ドワンゴのサービスで利用されているポイントシステムを紹介しました。辛みもいくつかありますが複数のサービスから使ってもらい運用実績を積み現在安定稼働できているため結果としてはうまく行ったのではないでしょうか。

この他にも課金開発セクションのシステムは存在しています。今回紹介できなかった関連システムの決済システムも紹介できる時が来たら書きたいと思います。ありがとうございました。