アイデンティティサービス
マルチテナントプラットフォーム向けのアカウント・セッション・多階層パートナー構造
プロジェクト背景
プラットフォーム上の他のすべてのサービスは ID 情報に依存している。ユーザーが誰か、どのテナントに属するか、最大10階層になり得るパートナー構造のどこに位置するか。「X の配下すべて」に素早く答え、セッションを失効可能に保ち、アカウント作成が複数サービスにまたがっても整合性を保つ必要がある。
アーキテクチャ
ゲートウェイ → ID サービス(go-zero gRPC)→ 合成可能な GORM スコープで構成した db/ 層経由で MySQL へ。Redis は cache/ 層の背後にあり、cacheSync/ 層がリードスルーと非同期ライトバックを制御する。アカウントの変更は変更前後のスナップショットとトレースヘッダー付きで Kafka に発行され、CRM のリードモデルやセグメンテーションサービスが追従できる。
主な技術ポイント
10階層のパートナーネットワークをクロージャテーブル(祖先・子孫・深さ)で表現し、アカウント作成と同じトランザクション内で書き込み
3層のデータアクセス:MySQL → Redis → ファンアウトワーカー、singleflight、TTL ジッターによる非同期キャッシュ同期
Redis の Bloom filter によるアカウントの重複排除。スライディング更新とサーバー側失効に対応した JWT セッション
独立した gRPC マイクロサービスとして切り出し(分離時点で 92 RPC)、その後クロージャテーブルによる階層クエリを含む約20の RPC を追加
複数サービスにまたがるアカウント作成の補償ロールバック。登録イベントは全ステップの成功後にのみ発行
キャッシュ層の堅牢化:キュー満杯メトリクス付きの有界非同期ワーカープール、セッション失効の同期フォールバック、キャッシュミス時のバッチ読み込み
コア実装
クロージャテーブルによるパートナー階層
祖先–子孫のすべての組とその深さを、アカウント作成と同じトランザクションで保存し、その上にサブツリー、配下メンバー、テナント別ディレクトリのクエリを構築した。
Why: 最も多い読み取りはサブツリークエリ(「このパートナーの配下全員」)。クロージャテーブルなら再帰クエリではなく1回のインデックス検索で答えられ、深さを持つことで10階層の上限も容易に強制できる。
非同期同期付きの3層キャッシュ
キャッシュミス時の読み取りは singleflight を経由して DB へ。キャッシュのライトバックと無効化は TTL ジッター付きの有界非同期ワーカープールで実行する。2025年に堅牢化を行い、ミス処理を修正し、singleflight を DB 読み取りのみに限定し、呼び出しごとのタイムアウトとキュー満杯カウンターを追加し、プールが飽和した場合はセッション失効を同期処理にフォールバックするようにした。
Why: ID サービスはすべてのリクエスト経路上にある。同時ミスの集約、有効期限の分散、失効を決して取りこぼさないことが、ここでは他のどこよりも重要になる。
Result: ユーザーごとのキャッシュミス検索を、1回のバッチクエリとパイプライン書き込みに置き換えた。
複数サービスにまたがるアカウント作成の整合性
アカウントを作成すると、他のサービスでもウォレットと設定が作成される。各フローを検証・準備・セットアップのステップに分割し、後続ステップが失敗した場合は、タイムアウトに対して安全な独立したコンテキスト上で補償削除を行う。
Why: これがないと、下流の失敗で作成途中のユーザーが残り、下流のコンシューマーがすでに存在しないアカウントの登録イベントを受け取る可能性があった。
技術的意思決定
| 技術的意思決定 | 選択 | 代替案 | 理由 |
|---|---|---|---|
| 階層の保存方式 | クロージャテーブル | 隣接リスト + 再帰クエリ | 読み取りが階層変更を圧倒的に上回り、ほぼすべての読み取りがサブツリークエリである。挿入時に余分な行を書くコストと引き換えに、サブツリーを1回の検索で読める。 |
| セッション失効 | JWT に加えて、Redis にユーザーごとに1つの有効トークンを保持 | 純粋なステートレス JWT | 管理者はアカウントを即座に無効化し、強制ログアウトできなければならない。ステートレスなトークンは有効期限前に失効させられない。 |