在庫や座席の取り合い (同時の更新) を、どう防ぎますか

アプリで読んでから調べて書く形をやめ、条件付きの UPDATE で DB に 1 回で判断させる、行をロックしてから読む、版の番号で後から来た書き込みをはじく、一意の制約で二重の確保を止める、のどれかで防ぐ、と答える問い。取り合いの激しさで手を選ぶ理由まで話します。

まず 4 択で考える

2 問

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

  1. 問 1在庫を 1 つ減らす処理で、売りすぎを防ぐ最も素直な書き方はどれか

  2. 問 2版の番号で後から来た書き込みをはじく楽観ロックが向くのは、どんな所か

2 問中 0 問に答えました

答えてみる

面接官に「在庫や座席の取り合い (同時の更新) を、どう防ぎますか」と聞かれたつもりで、答えを見る前に自分の言葉で、声に出して答えてみます。

解説を読む

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

こう聞かれます

  • 「在庫や座席の取り合い (同時の更新) を、どう防ぎますか」
  • 「残り 1 個の商品に、2 人が同時に購入のボタンを押しました。何が起きますか」
  • 「楽観ロックと悲観ロックは、どう使い分けますか」

面接官が見ている点

  • 読んでから書くまでの間に、別の人の書き込みが割り込むと分かっているか
  • トランザクションで囲むだけでは防げない場合があると知っているか
  • 条件付きの更新、行のロック、版の番号、一意の制約の、複数の手を持っているか
  • 取り合いの激しさや、失敗したときの利用者の体験で手を選べるか

短い答え

  • アプリで在庫を読み、足りるか調べ、減らして書く、の 3 手に分けると、間に別の人が割り込み、2 人とも買えてしまいます。
  • だから、stock >= 1 を条件にした UPDATE で DB に 1 回で判断させるか、行をロックしてから読むか、版の番号で後から来た書き込みをはじきます。座席のように 1 人だけが取れる物は、一意の制約でも止めます。
  • 取り合いが少なければ版の番号 (楽観ロック)、多ければ行のロック (悲観ロック) や条件付きの更新を選びます。

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

レッスンを開く

少し詳しい答え

何が起きるか

在庫が 1 個のとき、2 人のリクエストが同時に来ると、次の順で動くことがあります。

  1. A が在庫を読む (1 個)
  2. B が在庫を読む (1 個)
  3. A が「足りる」と判断し、0 個にして書く
  4. B も「足りる」と判断し、0 個にして書く

在庫は 0 個ですが、注文は 2 つできています。読んでから書くまでの間に、相手の書き込みが割り込んだためです。多くの DB の既定の分離レベルでは、トランザクションで囲むだけでは、この割り込みは防げません。

条件付きの UPDATE

いちばん素直な手は、「足りるなら減らす」を 1 つの SQL にして、DB に判断させることです。

SQL クエリ

UPDATE products SET stock = stock - 1
WHERE id = 10 AND stock >= 1;

DB は行を更新するときにその行をロックするので、2 人が同時に流しても、片方が終わってから、もう片方の条件が調べられます。更新できた行の数を見て、0 行なら「売り切れ」と返します。在庫を減らすだけのような単純な更新なら、これで足ります。

行をロックしてから読む (悲観ロック)

読んだ値を使って複雑な判断をするときは、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 失敗 ほかの人が先に更新した

実際は、版の比べと書き込みを 1 つの SQL (UPDATE ... WHERE id = ? AND version = ?) で行い、更新できた行の数で成否を見ます。待たせないぶん、取り合いが少ない所では速く、編集の画面で「ほかの人が先に保存しました」と出す場面にも向きます。

一意の制約で止める

映画の座席のように、1 つの物を 1 人だけが取れるなら、「公演と座席の組」に一意の制約を付けた表に予約を入れます。2 人目の書き込みは DB が制約の違反ではじくので、アプリの判断に漏れがあっても二重の予約はできません。

トレードオフ

楽観ロックは、取り合いが多いと失敗とやり直しが増え、利用者は何度も「失敗しました」を見ます。悲観ロックは確実ですが、人気の商品に注文が集中すると、1 つの行の前に行列ができて全体が遅くなります。限定品の発売のように 1 つの行に注文が殺到するときは、注文をキューに並べて順に処理する、在庫を前もって小分けにしておく、といった作りに変えることも考えます。

話す例は架空の例で構いません。「チケットの購入で二重の確保が起きたので、座席の表に一意の制約を付け、在庫は条件付きの UPDATE で減らすようにした」のように話せると伝わります。

よくある間違い

  • アプリで読んで、調べて、書く間に別の人の書き込みが割り込み、売りすぎます
  • トランザクションで囲めば大丈夫だと思う 既定の分離レベルでは、読んでから書く間の割り込みは防げないことがあります
  • 更新できた行の数を見ない条件付きの UPDATE が 0 行でも「成功」と返し、売り切れに気づきません

深掘りの問い

  • 「分離レベルを上げれば解決しますか」防げる場合もありますが、ロックや失敗が増え、DB によって動きも違うので、まず条件付きの更新やロックで明示的に防ぐ、と答えます
  • 「カートに入れた時点で在庫を確保しますか」確保すると買えない人が出るので、決済の手前で短い期限付きで確保し、期限が切れたら戻す、と答えます
  • 「在庫を Redis のようなキャッシュで数えるときは」 減らす操作が 1 回で済む命令を使い、最後は DB の記録と照らし合わせる、と答えます

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

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