こう 聞かれます
- 「複数の
サービスや DB に またがる 処理の 整合性を、 どう 保ちますか」 - 「注文の
サービスで 注文を 作り、 決済の サービスで 支払いを 取ります。 決済だけ 失敗したら、 どうしますか」 - 「DB に
保存した あと、 キューに 知らせを 送る 前に サーバーが 落ちたら、 どうなりますか」
面接官が 見ている点
- 別々の
サービスや DB を、 1 つの トランザクションで 囲めないと 分かっているか - 失敗した
ときに、 済んだ 手順を 打ち消す処理 (補償) で 戻す 考えを 持っているか - DB の
更新と 知らせの 送信が ずれる 問題と、 その 手当て (アウトボックス) を 知っているか - 少しの
間ずれる ことを 受け入れ、 照らし合わせで 拾う、という 運用まで話せるか
短い答え
- 別々の
サービスや DB は 1 つの トランザクションで 囲めないので、 手順に 分け、 途中で 失敗したら、 済んだ 手順を 打ち消す処理 (補償) を 逆の 順で 流して 戻します。 - DB の
更新と、 ほかの サービスへの 知らせは、 知らせを 同じ DB の 表に 同じトランザクションで 書き、 別の 働き手が 後から 送ります (アウトボックス)。 受け取る 側は、 同じ 知らせが 2 回届いても 困らない 形にします。 - 少しの
間ずれる ことは 事業側と 決めて 受け入れ、 定期的な 照らし合わせで、 ずれたまま 残った 物を 見つけて 直します。
この
少し 詳しい答え
なぜ 1 つの トランザクションに できないか
1 つの DB の
手順に 分け、 失敗したら打ち消す
注文を、
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 に
保存してから、 そのまま キューに送る間で 落ちると 知らせが 消えます。 同じ トランザクションで 知らせを 記録します - 知らせは 1 回だけ届く
前提で作る 送り直しで 2 回届く ことがあります。 受け取る 側で 2 回目を 無視します - 打ち消しの
処理を 用意しない 決済が 失敗しても 在庫が 確保されたまま 残ります。 手順ごとに 逆の 操作を 決めておきます
深掘りの問い
- 「打ち消しが
失敗したら、 どうしますか」 何度か 試し、 それでも 駄目なら別の 置き場に 移して 人が 見る、 照らし合わせでも 拾う、と答えます - 「手順を
順に 呼ぶ係を 1 つ置くのと、 知らせを 受けて 各サービスが 動くのは、 どう 選びますか」 手順が 多く 流れを 追いたいなら係を 置き、 手順が 少なく 各サービスを 独立させたいなら 知らせで 動かす、と答えます - 「サービスを
分けない、という 選択肢は」 1 つの トランザクションで 済むなら その ほうが 確実で、 分けるのは チームや 規模の 理由が はっきりしてから、と答えます
