# Shopify 將庫存預留移至 MySQL

Published: 2026-10-10

Shopify 將其庫存預留系統從 Redis 移至已存放庫存分類帳的 MySQL 資料庫。一個有界限的、可單獨鎖定的單位行池讓併發結帳可以使用 SKIP LOCKED 來選擇不同的合格單位。該設計還取決於交易邊界、主鍵佈局、補貨規則以及觀察結帳過程中的連接保持時間。

Canonical: https://thedailydiff.dev/zh-TW/video/shopify-reservation-locks/

## 本影片重點

- 舊的 Redis 數量計數器模型處理了併發，但預留清理和 MySQL 分類帳更新無法共用單一的本地原子交易。
- 替換方案為每個商品/地點組合使用一個單位一行，池的大小上限為 1,000。空池可以觸發在線補貨，併發請求則在補貨鎖後等待。
- 複合主鍵是 (shop\_id, inventory\_item\_id, inventory\_group\_id, id)。Shopify 在其原型中減少了鎖定開銷，並使用 READ COMMITTED 來避免阻塞補貨的間隙鎖。
- 預留會先刪除池中的行，然後插入預留記錄。提交會釋放資料庫鎖，而儲存的佔用會在支付過程中持續；成功支付會在稍後的原子交易中確認分類帳並移除預留。
- MySQL 文件將 SKIP LOCKED 描述為一個不一致的視圖，它會省略鎖定的行。它不建立完整的庫存計數或公平的輪流取用，僅適用於行級鎖，且對於基於語句的複製是不安全的。
- 源示例包含 expires\_at，但 Shopify 未記錄 MySQL 過期清理演算法。影片中的釋放/過期分支是一個解釋性的生命週期要求。
- 其他結帳程式碼中的連接保持時間是最終的吞吐量瓶頸。Shopify 同時寫入兩個系統，比較結果，並透過 Redis 終止開關逐步切換。

## 章節

- 0:00 為什麼將預留移至 MySQL？
- 0:29 為什麼兩個儲存庫有風險？
- 0:52 什麼成為可鎖定的單位？
- 1:15 當池清空時會發生什麼？
- 2:14 SKIP LOCKED 遺漏了什麼？
- 2:26 真正的瓶頸在哪裡？
- 2:38 為什麼我要推出這個設計？

## 翻譯文字稿

譯自英文原版旁白。可用的音訊和字幕由 YouTube 控制。

### 為什麼將預留移至 MySQL？

0:00 結帳需要 Redis 才能保持快速。 Shopify 將庫存預留移至 MySQL，每個單位使用一個行，在 有界限的池中。為什麼選擇 MySQL？ 如何安全地跳過鎖？ 這是 The Daily Diff，幕後。 為什麼快速查詢仍然遇到瓶頸？ Shopify 是一個用於線上和實體銷售的商務平台。 Redis 是一個記憶體資料儲存。

0:21 MySQL 是一個關聯式資料庫，Shopify 已經將其庫存分類帳 儲存在那裡。預留會在買家付款時短暫佔用庫存。

### 為什麼兩個儲存庫有風險？

0:29 他們的 Redis 模型會遞減商品計數器。 確認已付款訂單意味著更新 MySQL 分類帳並清理 Redis。 這些獨立的寫入可能會導致庫存重複銷售或在應可銷售時變得不可用。 舊模型也缺乏位置意識。 替換方案必須從某個可以履行訂單的地方選擇庫存。 錯誤大陸上的倉庫會是一個很好的資料庫條目，但卻是一個 糟糕的交貨承諾。 早期 MySQL 的嘗試使用了數量行，因此競爭的結帳會在同一個

### 什麼成為可鎖定的單位？

0:56 鎖定處排隊。想像一下試算表儲存格周圍的絲絨繩。 增加更多工人只會讓隊伍變長。 Shopify 改變了可鎖定的物件。 每個可用單位都有自己的行。 鎖定讀取會跳過另一個 交易持有的單位並選擇其他符合條件的 單位。不同的工作人員可以取得不同的行， 而熱門計數器則不會擋他們的路。

### 當池清空時會發生什麼？

1:15 該池的上限為每個商品和地點一千行。 補貨從分類帳中提取。 如果它清空了，預留 路徑會與競爭的請求一起在線補貨， 等待補貨鎖。 空池不代表倉庫是空的。 預留會刪除選定的池行，然後在交易中插入預留記錄。 提交會釋放資料庫鎖。

1:35 回溯會撤銷更改。 預留在支付處理過程中作為儲存狀態存活。 成功支付會原子地確認分類帳並移除預留。 資料庫鎖永遠不需要監護支付表單。 他們的複合主鍵以商店、商品、 群組，然後是單位身份。 匹配查詢減少了他們原型中的索引鎖定。 他們還使用 READ COMMITTED 來避免阻塞池

1:57 補貨的間隙鎖，以及一致的表格順序來防止循環等待。 發布的範例記錄了過期時間。 廢棄的支付最終需要釋放庫存，否則購物車就會變成 一個房東。Shopify 的帖子沒有明確說明清理演算法， 所以這個圖表顯示了生命週期要求。 這就是問題所在。

### SKIP LOCKED 遺漏了什麼？

2:14 SKIP LOCKED 排除鎖定的行，所以手冊稱其結果為不一致的 視圖。它既不提供完整的庫存計數，也不提供公平的輪流取用。 保持可用性決策和補貨規則在它周圍。

### 真正的瓶頸在哪裡？

2:26 那個瓶頸呢？ 其他結帳程式碼持有資料庫連接時間過長。 Shopify 標記了呼叫者並測量了連接保持時間， 然後清理了結帳路徑並重新檢視了執行緒併發。 快速查詢仍然可以在查詢之外排隊。

### 為什麼我要推出這個設計？

2:38 他們同時寫入兩個系統，以 Redis 為權威， 比較結果，然後透過終止開關逐步切換。 我的判斷是 SHIP IT。 我會推出共享交易邊界和那個可回溯的推出， 並對整個結帳路徑進行儀器化。 對此有疑問嗎？ 把它寫在評論中。 這就是今天的差異。

2:54 我是來自 Axrisi 的 Niko。 負責任地合併。

## 來源

- [We replaced Redis with MySQL for inventory reservations—and it scaled](https://shopify.engineering/scaling-inventory-reservations) — Shopify Engineering — Emilie Noel
- [Simplified reservation SQL embedded in Shopify's article](https://gist.github.com/CourtneySymons/cb5ecbe86331047aae166d5b1f1d555c) — Shopify Engineering / CourtneySymons on GitHub Gist
- [MySQL 8.0 — Locking Reads](https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html) — Oracle / MySQL Reference Manual
- [MySQL 8.0 — Transaction Isolation Levels](https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html) — Oracle / MySQL Reference Manual
- [What Is Shopify and How Does It Work?](https://www.shopify.com/blog/what-is-shopify) — Shopify
- [Redis quick starts](https://redis.io/docs/latest/develop/get-started/) — Redis documentation
- [What is MySQL?](https://dev.mysql.com/doc/refman/8.0/en/what-is-mysql.html) — Oracle / MySQL Reference Manual
