こう 聞かれます
- 「在庫や
座席の 取り合い (同時の 更新) を、 どう 防ぎますか」 - 「残り 1 個の
商品に、 2 人が 同時に 購入の ボタンを 押しました。 何が 起きますか」 - 「楽観ロックと
悲観ロックは、 どう 使い 分けますか」
面接官が 見ている点
- 読んでから
書くまでの 間に、 別の 人の 書き込みが 割り込むと 分かっているか - トランザクションで
囲むだけでは 防げない 場合が あると 知っているか - 条件付きの
更新、 行の ロック、 版の 番号、 一意の 制約の、 複数の 手を 持っているか - 取り合いの
激しさや、 失敗した ときの 利用者の 体験で 手を選べるか
短い答え
- アプリで
在庫を 読み、 足りるか 調べ、 減らして 書く、の 3 手に 分けると、 間に 別の 人が 割り込み、 2 人とも 買えてしまいます。 - だから、
stock >= 1を条件に した UPDATE で DB に 1 回で 判断させるか、 行を ロックしてから 読むか、 版の 番号で 後から 来た 書き込みを はじきます。 座席のように 1 人だけが 取れる 物は、 一意の 制約でも 止めます。 - 取り合いが
少なければ 版の 番号 (楽観ロック)、 多ければ 行の ロック (悲観ロック ) や 条件付きの 更新を 選びます。
この
少し 詳しい答え
何が起きるか
在庫が 1 個の
- A が
在庫を 読む (1 個) - B が
在庫を 読む (1 個) - A が
「足りる」と 判断し、 0 個にして書く - B も
「足りる」と 判断し、 0 個にして書く
在庫は 0 個ですが、
条件付きの UPDATE
いちばん素直な
SQL クエリ
UPDATE products SET stock = stock - 1
WHERE id = 10 AND stock >= 1;DB は
行を ロックしてから 読む (悲観ロック)
読んだ値をSELECT ... FOR UPDATE で
版の 番号で はじく (楽観ロック)
行に
JavaScript
const db = { stock: 1, version: 1 };
function read() {
return { stock: db.stock, version: db.version };
}
function buy(seen) {
if (db.version !== seen.version) {
return "失敗 ほかの人が先に更新した";
}
if (seen.stock < 1) {
return "失敗 在庫なし";
}
db.stock = seen.stock - 1;
db.version += 1;
return "成功 残り " + db.stock;
}
const a = read();
const b = read();
console.log("A " + buy(a));
console.log("B " + buy(b));A 成功 残り 0
B 失敗 ほかの人が先に更新した実際は、UPDATE ... WHERE id = ? AND version = ?) で
一意の 制約で止める
映画の
トレードオフ
楽観ロックは、
話す例は
よく ある間違い
- アプリで
読んで、 調べて、書く 間に 別の 人の 書き込みが 割り込み、 売りすぎます - トランザクションで
囲めば 大丈夫だと思う 既定の 分離レベルでは、 読んでから 書く 間の 割り込みは 防げない ことがあります - 更新できた
行の 数を見ない 条件付きの UPDATE が 0 行でも 「成功」と 返し、 売り 切れに 気づきません
深掘りの問い
- 「分離レベルを
上げれば 解決しますか」 防げる 場合も ありますが、 ロックや 失敗が 増え、 DB に よって 動きも 違うので、 まず条件付きの 更新や ロックで 明示的に 防ぐ、と答えます - 「カートに
入れた 時点で 在庫を 確保しますか」 確保すると 買えない 人が 出るので、 決済の 手前で 短い 期限付きで 確保し、 期限が 切れたら 戻す、と答えます - 「在庫を Redis のような
キャッシュで 数えるときは」 減らす操作が 1 回で 済む命令を 使い、 最後は DB の 記録と 照らし合わせる、と答えます
