We replaced Redis with MySQL for inventory reservations, and it scaled
Bài tóm tắt Shopify 2026 4. Điều chỉnh Độ khó: Nâng cao
Shopify · 2026 · 4. Điều chỉnh · Độ khó: Nâng cao — bài gốc
Bối cảnh
- Giữ hàng chống bán quá số lượng (oversell protection):
- Reserve giữ hàng tạm trong lúc thanh toán (vài phút);
- Claim trừ hẳn vào sổ tồn kho (inventory ledger, nguồn sự thật) khi thanh toán xong.
- Sai theo hướng này thì bán trùng món cuối; sai theo hướng kia thì báo hết hàng nhầm.
- Quy mô: Black Friday 2025, đỉnh 5,1 triệu USD doanh số/phút (tăng 11% so với năm trước); theo Shopify họ chiếm hơn 14% thương mại điện tử Mỹ.
- Mô hình cũ trên Redis:
DECR/INCRtrên khoá số lượng. Concurrency ổn, nhưng reservation (Redis) và sổ tồn kho (MySQL) nằm ở hai hệ thống, nên bước claim không nguyên tử được và có thể oversell hoặc undersell. Không hỗ trợ nhiều kho; tốn thêm một cluster phải vận hành. - Các lần thử trước dùng một hàng có cột số lượng đều thất bại vì tranh chấp khoá trên hàng đó.
Thay đổi chính
- Mỗi đơn vị hàng một hàng +
SELECT ... FOR UPDATE SKIP LOCKED(MySQL 8), lấy cảm hứng từ 37signals: hàng nào đang bị khoá thì bỏ qua, lấy hàng khác, không phải chờ. Reservation và sổ tồn kho chung một DB nên có ACID cho cả reserve lẫn claim. - Pool có giới hạn: tối đa 1.000 hàng cho mỗi cặp (món, kho), do một tiến trình bổ sung từ sổ tồn kho. Nếu pool cạn giữa flash sale thì bổ sung ngay trong request, có khoá để chỉ một transaction bổ sung, còn lại chờ, tránh thundering herd.
- Tinh chỉnh khoá:
- khoá chính ghép chứa các cột lọc, nên từ 2 xuống 1 row lock mỗi reservation;
READ COMMITTEDcho các transaction này để tránh gap lock gây deadlock khi bổ sung (lần đầu codebase dùng isolation level không mặc định);- thứ tự khoá thống nhất giữa reserve và claim để hết deadlock vòng;
- gom truy vấn của giỏ nhiều món bằng
UNION ALL.
- Nút thắt thật là connection, không phải CPU. Latency ổn, CPU chưa đầy, vậy mà vẫn chạm trần. Họ gắn tag vào mọi câu SQL (
/* conn_tag:checkout_completion */), ProxySQL đo thời gian giữ connection theo từng quy trình nghiệp vụ. Kết quả: các phần khác của luồng checkout giữ connection quá lâu; reservation chỉ là "giọt nước tràn ly". Dọn luồng checkout bỏ được 50% lượt đọc và 33% transaction trên primary. Tăng InnoDB thread concurrency (cấu hình từ nhiều năm trước, chưa xem lại). - Cutover: "shadow mode" ghi kép Redis + MySQL, Redis vẫn là nguồn sự thật. So sánh rồi mới chuyển nguồn sự thật sang MySQL; còn kill switch để quay về Redis. Rollout từng pod, từ pod ít traffic tới khách lớn nhất (Shopify 2018).
Kết quả
Đạt mục tiêu throughput ở đỉnh 2025. Trong flash sale, CPU writer dưới 50%, reader dưới 16%.
Vì sao
Theo Shopify: chiến lược thống nhất database; ACID giữa reservation và sổ tồn kho để loại hẳn một lớp bug; bớt một hệ thống phải vận hành.
Bài học cho SWE
- Xem lại các quyết định cũ. Điều không làm được trên MySQL 5 năm trước nay làm được nhờ
SKIP LOCKED; cấu hình "theo kinh nghiệm" cũng cần đo lại. - Kho chuyên dụng (Redis) để lấy tốc độ có thể làm mất tính nguyên tử với nguồn sự thật. Đưa về chung DB đổi tốc độ thô lấy tính đúng đắn.
- CPU thấp mà vẫn xếp hàng thì hãy đo toàn đường đi, kể cả connection pool. Gắn tag ở app và tổng hợp ở proxy là cách rẻ để biết ai đang giữ tài nguyên.
- Mục tiêu thật là làm reservation thành "hàng xóm an toàn" của các phần khác dùng chung DB, không chỉ làm nó nhanh.
- Prototype tối giản (script Ruby + MySQL, quan sát khoá ở một terminal khác) cho phản hồi nhanh hơn lý thuyết.
Liên quan
Chọn database theo workloadHot partition và thundering herdMigration không downtimeGiao dịch phân tán và nhất quán xuyên service
Được nhắc tới trong
Chủ đề (5)
Công ty (1)
Bài tóm tắt (4)