Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
在庫違算を防ごう!トランザクション&行ロック
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
Shohei Kondo
July 20, 2026
Programming
41
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
在庫違算を防ごう!トランザクション&行ロック
Shohei Kondo
July 20, 2026
More Decks by Shohei Kondo
See All by Shohei Kondo
生成AIで帳票OCRが「簡単に」作れる時代になった?
kon_shou
0
1.2k
Serverless 入門 ~環境構築からデプロイまで~
kon_shou
0
390
実運用におけるLaravelとNuxtでのRepositoryのレイヤ分割の話
kon_shou
3
3.8k
Other Decks in Programming
See All in Programming
なぜCTOを降りてFDEを選んだのか?〜なぜプロダクト企業がFDEで顧客の現場に踏み込むのか〜
gonta
1
130
そのリトライ、死んだコネクションを使い回していませんか ── GoのHTTPクライアントとHTTP/2を実プロダクト障害から学び直す
myus4a
0
380
Everything will be SERVERLESS — 信じて運用した10年の経験値 / Everything Will be Serverless — Lessons Learned from 10 Years of Operational Experience
seike460
PRO
1
690
速く作れる。その次は、速く確かめられる開発へ 〜AIネイティブ開発を支える、Shift Down〜 / Can build fast. Next, moving to development where we can verify fast.
rkaga
8
5.4k
WebRTC映像をAirPlayに対応させる挑戦.pdf
monolithic_adam
0
330
iOSDC Japan 2026 - Swiftで作って学ぼう!データベース自作入門
kaseken
2
440
Findy - エンジニア向け会社紹介/Findy Company Deck
findyinc
6
400k
APNsからLive Activityを開始する話
yumnumm
0
150
テストを司るデーモンに会いに行く 〜隔離した仮想マシンでテストを通すまで〜
h1d3mun3
1
600
wkhtmltopdfの次どうするか問題2026
willnet
2
1.8k
[ハンズオン]AIへの指示だけで「五目並べ」を作ってみよう
satoshi256kbyte
1
330
手動確認はもう限界 〜XCUITestでCustom URL Schemeの遷移を起動種別ごとに自動テストする〜 / Testing Custom URL Schemes with XCUITest
otouto
0
380
Featured
See All Featured
We Analyzed 250 Million AI Search Results: Here's What I Found
joshbly
1
2k
Build The Right Thing And Hit Your Dates
maggiecrowley
39
3.5k
Code Reviewing Like a Champion
maltzj
528
40k
Redefining SEO in the New Era of Traffic Generation
szymonslowik
1
460
Hiding What from Whom? A Critical Review of the History of Programming languages for Music
tomoyanonymous
3
1.3k
GraphQLの誤解/rethinking-graphql
sonatard
75
12k
So, you think you're a good person
axbom
PRO
2
2.2k
How STYLIGHT went responsive
nonsquared
100
6.3k
Have SEOs Ruined the Internet? - User Awareness of SEO in 2025
akashhashmi
0
510
Visual Storytelling: How to be a Superhuman Communicator
reverentgeek
2
700
Six Lessons from altMBA
skipperchong
29
4.5k
Fireside Chat
paigeccino
43
4k
Transcript
在庫違算を防ごう! トランザクション & 行ロック 並行実行で在庫がズレる理由と、その閉じ方 在庫違算を防ごう!トランザクション&行ロック 1
その在庫、二重に売れていませんか 在庫1個。同じ瞬間に2人が「買う」を押す。 在庫: 1 Aさん: 在庫を見る → 1個ある → 買う
→ 在庫 0 Bさん: 在庫を見る → 1個ある → 買う → 在庫 0 ← 同じ1個を売った 売れたのは 2個。減ったのは 1個。 これが 在庫違算。原因は「読んでから書くまでの隙間」。 在庫違算を防ごう!トランザクション&行ロック 2
今日の目次 行ロックをする目的 — 並行実行での意図せぬ減算を防ぐ 2. 行ロックの注意点 1. 同種の減算ロジックで、同じロックを取る 2. テレコによる、デッドロック
3. 空レコードによる、ギャップロック 3. 行ロックとは別の解決法 — アトミック UPDATE / UPSERT / 分散ロック ※MySQLでの経験をベースにしてます。 1. 在庫違算を防ごう!トランザクション&行ロック 3
1. 行ロックをする目的 「読んで・判定して・書き戻す」の隙間を閉じる 在庫違算を防ごう!トランザクション&行ロック 4
何が起きているか(lost update) プレーンな SELECT は スナップショット読み。 最新のコミットを待たず、自分が見た値のまま突き進む。 t0 t0 t1
t2 A: SELECT quantity → 10 B: SELECT quantity → 10 A: 9 を書く (UPDATE = 10-1) B: 9 を書く (UPDATE = 10-1) ← 同じ 10 を読む ← A の更新を上書き。1回ぶんが消える 回減らしたのに、在庫は 9(本来は 8)。 (更新の消失)。 2 = lost update 在庫違算を防ごう!トランザクション&行ロック 5
FOR UPDATE (悲観ロック)で直列化する SELECT ... FOR UPDATE t0 t0 t1
t2 は排他ロック+最新コミット値を読む。 A: SELECT ... FOR UPDATE → 10 (ロック取得) B: SELECT ... FOR UPDATE → 待たされる… A: 9 を書いて COMMIT(ロック解放) B: 待ち解除 → 9 を読み直す → 8 を書く 読み・判定・書きが1本ずつ直列に並ぶ。在庫は正しく 8。 在庫違算を防ごう!トランザクション&行ロック 6
コード: 違算する実装 vs 直した実装 // BuggyStockService:ロックもトランザクションも無い $stock = Stock::on($conn)->findOrFail($id); //
読む(スナップショット) if ($stock->quantity < $by) throw ...; // 判定 $stock->quantity -= $by; // ← 隙間で誰かが書くと上書き $stock->save(); // 書く // SafeStockService:トランザクション内で FOR UPDATE DB::connection($conn)->transaction(function () use ($id, $by, $conn) { $stock = Stock::on($conn)->lockForUpdate()->findOrFail($id); // ロックして読む if ($stock->quantity < $by) throw ...; $stock->quantity -= $by; $stock->save(); }, attempts: 3); // デッドロック時だけ自動リトライ lockForUpdate() → SQL の FOR UPDATE 。トランザクションの中でこそ効く。 在庫違算を防ごう!トランザクション&行ロック 7
2. ロックの注意点 正しく取っているつもりで、漏れる・詰まる 在庫違算を防ごう!トランザクション&行ロック 8
注意点(1) 同種の減算は「同じロック」を取る 在庫を減らす経路が 複数 あると、片方がロックを取っても もう片方がロック無し/別の行をロックしていれば、そこから漏れる。 経路X: 注文確定 経路Y: 手動調整
→ stocks(id=5) を FOR UPDATE → stocks(id=5) をロック無しで UPDATE → X が守っていても Y が突き抜ける 同じ資源には、同じ粒度・同じ対象のロックを、全経路で 「減算は必ずこのサービスを通す」と入口を1つにするのが堅い 在庫違算を防ごう!トランザクション&行ロック 9
注意点(2) テレコによるデッドロック 2 つのトランザクションが、同じ2行を 逆順 にロックすると詰まる。 T1: A をロック →
次に B が欲しい T2: B をロック → 次に A が欲しい ↓ T1 は B 待ち、T2 は A 待ち → 循環=デッドロック は循環を自動検知し、片方をロールバック ( 、PostgreSQL: 40P01 ) 回避策 = ロック順をそろえる(例: 常に id の小さい順) 保険として DB::transaction(fn () => ..., attempts: N) 的なリトライもありかも?(ロジック による) テレコを防ぐために、トランザクションのネスト(=save point)は、できるだけ避けたい、ての が個人的な肌感。 MySQL/Postgres MySQL: errno 1213 / SQLSTATE 40001 在庫違算を防ごう!トランザクション&行ロック 10
注意点(2)補足 ネストを避ける具体策 class StockService { // $manageTransaction:「Tx+ロック」のセットをこのメソッドが管理するか public function decrement(int
$id, bool $manageTransaction = true): void { if ($manageTransaction) { // 自分で「Tx+ロック」のセットを張る DB::transaction(function () use ($id) { $stock = Stock::lockForUpdate()->findOrFail($id); $stock->decrement('quantity'); }); return; } // 親が「Tx+ロック」のセットを持っている前提。自分ではどちらも張らない if (DB::transactionLevel() === 0) { throw new LogicException('親のトランザクション内で呼ぶこと'); } Stock::findOrFail($id)->decrement('quantity'); } } // 1. 親が「Tx+ロック」のセットを張る → 子は manageTransaction: false DB::transaction(function () use ($service) { Stock::lockForUpdate()->findOrFail(5); // 親がロック。セットは親スコープで成立 $service->decrement(5, manageTransaction: false); // 子はTxもロックも張らない }); // 2. 親トランザクションが無い単体呼び出し → デフォルト true で自分でセットを張る $service->decrement(5); 在庫違算を防ごう!トランザクション&行ロック 11
注意点(3) 空レコードによる、ギャップロック の REPEATABLE READ では、範囲/非ユニーク条件の locking read は 該当行が無くても、スキャンした範囲(ギャップ)をロックする。
MySQL -- id=42 はまだ存在しない SELECT * FROM stocks WHERE id = 42 FOR UPDATE; -- ギャップロックを取る T1: WHERE id=42 FOR UPDATE → ギャップをロック(行は無い) T2: WHERE id=42 FOR UPDATE → 同じギャップをロック(両立する) T1: INSERT id=42 → T2 のギャップロックと衝突で待つ T2: INSERT id=42 → T1 と衝突 → 循環=デッドロック 「まだ無い行を予約してから作る」実装は、ここでよく詰まる。 対策:行が無ければ例外(無い行を FOR UPDATE しに行かない) $stock = Stock::on($conn)->lockForUpdate()->find($id); // 一意キーで1行を指す if ($stock === null) { throw new StockNotFoundException($id); // 予約INSERTに進ませない } 在庫違算を防ごう!トランザクション&行ロック 12
3. 在庫違算を防ごう!トランザクション&行ロック 行ロックとは別の解決法 13
アトミック更新 = 在庫減算は UPDATE 一発に畳める 「読む→判定→書き戻す」の代わりに、1文で条件付きに引く。 UPDATE stocks SET quantity
= quantity - 1 WHERE sku = 'A-001' AND quantity >= 1; -- ← ガード(在庫がある時だけ) -- 在庫不足なら「0行マッチ」で終わる(例外は飛ばない) 文なので read→判定→write の隙間が無い → lost update しない FOR UPDATE もアプリ側ロックも不要( MySQL ・ PostgreSQL とも既定で安全) 1 在庫違算を防ごう!トランザクション&行ロック 14
アトミック更新の落とし穴1:ガードを外すとマイナス 在庫 アトミック更新の弱点は「マイナスを作れてしまう」こと。 -ガード無し:在庫0でも引けてしまう → 売りすぎ(quantity がマイナス) UPDATE stocks SET
quantity = quantity - 1 WHERE sku = 'A-001'; 守りは2段構え: 必ず WHERE quantity >= 1 を付ける … そもそもマイナスにしない+ affected=0 で検知 保険に UNSIGNED 列 か CHECK (quantity >= 0) (MySQL 8.0.16+/PG)でDBからも弾く そして——判定が複雑で1文に畳めない (複数行を読んで計算し、複数箇所を書く)ときは、素直に トランザクション + FOR UPDATE に戻す。アトミック更新は万能ではない。 在庫違算を防ごう!トランザクション&行ロック 15
アトミック更新の落とし穴2:ガード付けたら今度は例 外が飛ばない ガード( WHERE quantity >= 1 )を付けても、在庫不足は「エラー」にならない。 UPDATE stocks
SET quantity = quantity - 1 WHERE sku = 'A-001' AND quantity >= 1; -- 在庫不足 → 「0行にマッチした成功」。例外は飛ばない 的には正常終了。呼び出し側は失敗に気づけない 影響行数(affected rows)を自分で見て、0 なら在庫不足として自前で throw する チェックを忘れると “売り切れなのに成功” = サイレント失敗 “ FOR UPDATE + if 方式なら if で明示的に投げられるのが対照的な強み。 なお affected=0 は「在庫不足」と「SKU不在」を区別しない → 分けたいなら別途存在確認を。 DB “ 在庫違算を防ごう!トランザクション&行ロック 16
UPSERT (ON DUPLICATE / ON CONFLICT) 注意点(3)の「無い行を予約してから作る」は、upsert 1文に畳めば FOR UPDATE
もギャップロックも要らない。 -- MySQL: 在庫行が無ければ作り、あれば足す(1文・原子的) INSERT INTO stocks (sku, quantity) VALUES ('A-001', 10) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity); -- PostgreSQL: 同じことを ON CONFLICT で INSERT INTO stocks (sku, quantity) VALUES ('A-001', 10) ON CONFLICT (sku) DO UPDATE SET quantity = stocks.quantity + EXCLUDED.quantity; 前提は sku にユニーク制約。重複INSERTをDBが弾く=並行でも1行に収束 「存在しないキーを FOR UPDATE 」→ ギャップロック地獄、を構造的に回避 ただし… PKの意図しない増加(UPDATE落ちでも AUTO_INCREMENT を消費=欠番)や、 謎のデッドロック(複数ユニークキー絡み)を踏んだ経験より、いい思い出はない… 在庫違算を防ごう!トランザクション&行ロック 17
分散ロック:Redis と アドバイザリロック ロックが重い/ホットな1行に集中 → 名前に対して取る論理ロックという選択肢。 Redis ( DB の外)
— Laravel なら Cache::lock() DB $lock = Cache::lock('stock:A-001', 10); // キー, TTL 10秒 if ($lock->get()) { try { /* 在庫処理 */ } finally { $lock->release(); } } ただし TTLで区切るため、フリーズ中(GCポーズ等)に失効→二重書きの穴(Redlock論争)= 効率化どまり アドバイザリロック(DBの中) — MySQL GET_LOCK() / PG pg_advisory_xact_lock() 行が無くても「名前」でロックできる。DBの中なので Redis より整合が取りやすい TTL が無くトランザクションに紐づく → フリーズしても失効せず、 Redis の穴が無い(※ Laravel 標 準の文法は無し) 在庫違算を防ごう!トランザクション&行ロック 18
まとめ:設計原則 基本:読み・判定・書きの“隙間”をロックで閉じる トランザクション + lockForUpdate() 。単発の条件付きUPDATEなら不要 1. 行ロックの目的 … lost
update を防ぐ=並行実行を直列化する 2. 行ロックの注意点(正しく取っているつもりで漏れる・詰まる) 1. 減算経路を 1 つに … 全経路で同じ粒度・対象のロックを取る 2. ロック順をそろえる … テレコ=逆順でデッドロック 3. 無い行を FOR UPDATE しない … 空レコードのギャップロック回避(無ければ例外) 3. 行ロック以外の選択肢を用途で使い分ける(真実の源は常に DB ) アトミックUPDATE( WHERE quantity >= n のガード。外すとマイナス在庫/ affected=0 を 自前で throw) UPSERT (ユニーク制約でギャップロック回避。 AUTO INCREMENT に注意) 分散ロック(Redis/advisory) 在庫違算を防ごう!トランザクション&行ロック 19