複数のサービスや DB にまたがる処理の整合性を、どう保ちますか

1 つのトランザクションでは囲めないと認め、手順に分けて、失敗したら済んだ手順を打ち消す処理 (補償) で戻す、DB の更新と送る知らせを同じトランザクションで記録する (アウトボックス)、受け取る側を何度届いても困らない形にする、と答える問い。少しの間ずれることを事業側と決め、定期的な照らし合わせで拾う所まで話します。

まず 4 択で考える

2 問

答えに入れるべき考えを選びます。選ぶと、すぐに正誤と解説が出ます。そのあと下の欄で、自分の言葉で答えてみます。

  1. 問 1在庫の確保は済んだが、別のサービスの決済が失敗した。整合性を保つ動きとして正しいものはどれか

  2. 問 2注文を DB に保存したあと、キューに知らせを送る前にサーバーが落ちても知らせが消えない作りはどれか

2 問中 0 問に答えました

答えてみる

面接官に「複数のサービスや DB にまたがる処理の整合性を、どう保ちますか」と聞かれたつもりで、答えを見る前に自分の言葉で、声に出して答えてみます。

解説を読む

聞かれ方から関連するコースまで、この問いの解説を全部ここに置いています。

こう聞かれます

  • 「複数のサービスや DB にまたがる処理の整合性を、どう保ちますか」
  • 「注文のサービスで注文を作り、決済のサービスで支払いを取ります。決済だけ失敗したら、どうしますか」
  • 「DB に保存したあと、キューに知らせを送る前にサーバーが落ちたら、どうなりますか」

面接官が見ている点

  • 別々のサービスや DB を、1 つのトランザクションで囲めないと分かっているか
  • 失敗したときに、済んだ手順を打ち消す処理 (補償) で戻す考えを持っているか
  • DB の更新と知らせの送信がずれる問題と、その手当て (アウトボックス) を知っているか
  • 少しの間ずれることを受け入れ、照らし合わせで拾う、という運用まで話せるか

短い答え

  • 別々のサービスや DB は 1 つのトランザクションで囲めないので、手順に分け、途中で失敗したら、済んだ手順を打ち消す処理 (補償) を逆の順で流して戻します。
  • DB の更新と、ほかのサービスへの知らせは、知らせを同じ DB の表に同じトランザクションで書き、別の働き手が後から送ります (アウトボックス)。受け取る側は、同じ知らせが 2 回届いても困らない形にします。
  • 少しの間ずれることは事業側と決めて受け入れ、定期的な照らし合わせで、ずれたまま残った物を見つけて直します。

この問いを含む分野をレッスンで練習する

レッスンを開く

少し詳しい答え

なぜ 1 つのトランザクションにできないか

1 つの DB の中なら、トランザクションで「全部成功か、全部無かったことか」にできます。しかし注文のサービスと決済のサービスが別の DB を持つと、片方だけ成功して片方が失敗することが起きます。複数の DB をまとめて確定させる仕組み (2 相コミット) もありますが、すべての参加者が対応している必要があり、1 つが止まると全体が待たされるので、Web のサービス間ではあまり使いません。

手順に分け、失敗したら打ち消す

注文を、在庫の確保、決済、配送の手配、の手順に分けます。それぞれに「打ち消す処理」を用意し、途中で失敗したら、済んだ手順を逆の順で打ち消します。この進め方はサーガと呼ばれます。動きを小さく書くと次の通りです。

JavaScript

const steps = [
  {
    name: "在庫の確保",
    run: () => console.log("在庫を確保"),
    undo: () => console.log("在庫を戻す"),
  },
  {
    name: "決済",
    run: () => {
      throw new Error("カードが通らない");
    },
    undo: () => console.log("決済を取り消す"),
  },
  {
    name: "配送の手配",
    run: () => console.log("配送を手配"),
    undo: () => console.log("配送を取り消す"),
  },
];

function runSaga(steps) {
  const done = [];
  for (const step of steps) {
    try {
      step.run();
      done.push(step);
    } catch (err) {
      console.log(step.name + "で失敗 " + err.message);
      for (const d of done.reverse()) {
        d.undo();
      }
      return "取り消して終了";
    }
  }
  return "すべて成功";
}

console.log(runSaga(steps));
在庫を確保
決済で失敗 カードが通らない
在庫を戻す
取り消して終了

打ち消しは「無かったこと」ではなく「逆の操作」です。支払いなら返金、メールなら「取り消しました」のお知らせになります。送ったメールは消せないので、取り消しにくい手順ほど後ろに置きます。注文には「確定待ち」のような途中の状態を持たせ、利用者にもそう見せます。

DB の更新と知らせをずらさない

「注文を DB に保存し、それからキューに『注文ができた』と送る」と書くと、保存の直後にサーバーが落ちたとき、知らせが永遠に送られません。逆の順だと、知らせだけ送られて注文が無い、が起きます。

そこで、知らせを送る代わりに、同じ DB の「送る予定の知らせ」の表に、注文と同じトランザクションで書きます。別の働き手がその表を読んでキューに送り、送れたら済みの印を付けます。この形はアウトボックスと呼ばれます。働き手は送ったあと印を付ける前に落ちることがあるので、同じ知らせが 2 回送られることはあります。だから受け取る側は、知らせの id を処理済みとして記録し、2 回目は何もしないようにします。決済の API の二重の送信と同じ考えです。

照らし合わせで拾う

打ち消しそのものが失敗する、知らせが何度送っても届かない、ということも起きます。何度か試して届かない知らせは別の置き場 (デッドレターキュー) に移して人が見ます。さらに、注文と決済の記録を定期的に照らし合わせ、片方にしか無い物を見つけて直す処理を用意します。

トレードオフ

この作りは、手順ごとの打ち消し、途中の状態、照らし合わせと、1 つの DB で済む場合よりずっと手間がかかります。「少しの間、在庫と注文の数がずれて見える」ことも受け入れる必要があり、どこまで許すかを事業側と決めます。分ける理由が弱いなら、同じ DB に置いて 1 つのトランザクションで済ませるほうが、確実で安く済みます。

話す例は架空の例で構いません。「ポイントの付与を別のサービスに分けたら、付与の漏れが出たので、注文と同じトランザクションで送る予定の知らせを書き、毎晩照らし合わせるようにした」のように話せると伝わります。

よくある間違い

  • DB に保存してから、そのままキューに送る間で落ちると知らせが消えます。同じトランザクションで知らせを記録します
  • 知らせは 1 回だけ届く前提で作る送り直しで 2 回届くことがあります。受け取る側で 2 回目を無視します
  • 打ち消しの処理を用意しない決済が失敗しても在庫が確保されたまま残ります。手順ごとに逆の操作を決めておきます

深掘りの問い

  • 「打ち消しが失敗したら、どうしますか」何度か試し、それでも駄目なら別の置き場に移して人が見る、照らし合わせでも拾う、と答えます
  • 「手順を順に呼ぶ係を 1 つ置くのと、知らせを受けて各サービスが動くのは、どう選びますか」手順が多く流れを追いたいなら係を置き、手順が少なく各サービスを独立させたいなら知らせで動かす、と答えます
  • 「サービスを分けない、という選択肢は」 1 つのトランザクションで済むならそのほうが確実で、分けるのはチームや規模の理由がはっきりしてから、と答えます

確認日 2026年10月30日書き手 小尾 知希 (講師)

技術面接問題の一覧へ戻る