# Shopify 將庫存預留移至 MySQL

Published: 2026-10-10

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

Canonical: https://thedailydiff.dev/zh-HK/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
