Flash sale: chống bán quá số lượng
7/8
Bài 7 / 8~14 phútChống bán quá số lượngMiễn phí… lượt xem

Đối chiếu thực tế: hệ thống bán vé và sàn TMĐT đã làm gì?

Đặt thiết kế của bạn cạnh các sự cố và bài viết kỹ thuật công khai: năm nào, ràng buộc gì, và vì sao họ chọn khác bạn.

TL;DR: Bài này đặt thiết kế của bạn cạnh bảy case công khai (Shopify, Cloudflare, Alibaba, Ticketmaster, Ticketbox). Với mỗi case bạn tách ba thứ: ràng buộc của họ, lựa chọn của họ, trục bị tác động. Lựa chọn khác bạn thường là hệ quả của một ràng buộc không có trong bảng của bạn.

Bạn đã có một thiết kế: 1 SKU, tồn kho 1.000, 200.000 người chờ, tải đỉnh khoảng 50.000 lượt/giây, và một row Postgres chỉ chịu được cỡ 1.200 lượt/giây ở 50 kết nối trên máy tác giả. Câu hỏi quen thuộc là "vậy người ta làm thế nào?". Hỏi vậy dễ thành học thuộc; hỏi xem họ thiếu gì so với bạn thì có ích hơn.

Bài này đi qua bảy case công khai, mỗi case gắn một ràng buộc khác bảng của bạn. Số liệu đều kèm năm và link; nguồn chỉ là báo chí thì bài ghi rõ.

1. Cách đọc một case: tìm ràng buộc khác bạn

Bảng ràng buộc ở bài 01 gồm một SKU, 1.000 sản phẩm, 200.000 người chờ, tải đỉnh 50.000 lượt/giây, không bán quá, phản hồi dưới 1 giây, chỗ giữ 10 phút. Edge là tầng sát người dùng, nơi request đi qua trước khi vào hệ thống chính (load balancer, CDN). Với mỗi case dưới đây, bạn làm ba việc theo thứ tự. Trước hết đoán, tức là nhìn ràng buộc của họ rồi đoán họ chặn cửa hay nâng trần. Sau đó đọc lời giải. Cuối cùng tự hỏi: nếu bảng của tôi mang ràng buộc đó, thiết kế của tôi có đổi không.

Trần của một row là thứ bạn đã đo: UPDATE có điều kiện cỡ 1.200 lượt/giây ở 50 kết nối, thêm kết nối không thêm thông lượng. Mọi case dưới đây đều là cách khác nhau để thoát khỏi con số đó, hoặc để không bao giờ chạm tới nó.

2. Case: nền tảng nhiều cửa hàng, edge không biết tồn kho

Thử đoán trước

Một nền tảng chạy hàng nghìn cửa hàng, edge (load balancer) của họ chỉ được phép không lưu trạng thái, và một đợt bán của một cửa hàng làm nghẽn việc ghi vào cơ sở dữ liệu chung. Họ nên chặn cửa hay nâng trần của hàng nóng? Viết ra lựa chọn và lý do trước khi đọc tiếp.

Shopify 2017 và 2022: phòng chờ không biết còn hàng hay hết

Trong bài kỹ thuật về load balancer lập trình được, phần II (Shopify Engineering, tháng 2/2017), Shopify chặn tải ngay ở edge bằng leaky bucket (thùng rò: chỉ cho một lượng cố định lọt qua mỗi nhịp). Bản đầu cho người vượt hạn mức thăm dò lại ngẫu nhiên (random polling), và bài viết ghi nhận phương sai cao: median và p95 của thời gian chờ chênh nhau rất xa. Bản sửa lưu timestamp lần thử đầu tiên vào một cookie có chữ ký, ai đến trước thì được ưu tiên. Mỗi load balancer tự tính ngưỡng riêng, vì họ muốn giữ edge không lưu trạng thái: theo bài viết, một kho dữ liệu chung sẽ thành điểm lỗi mới đúng lúc tải cao nhất.

Ràng buộc khác bạn nằm ở đây. Bạn có một Postgres duy nhất cầm sự thật về tồn kho, nên có chỗ để đặt cờ hết hàng. Edge của Shopify thì không được phép biết gì về tồn kho, nên chỉ còn công cụ điều tiết lượng người đi vào. Lựa chọn của họ là chặn cửa, và là lựa chọn duy nhất còn lại.

Cái giá hiện ra năm 2022. Phần tóm tắt của bài nói tại Strange Loop 2022 của Shopify, "A Commerce-centric Approach to Queuing Fairly at High Throughput", nêu ví dụ minh hoạ: người dùng có thể chờ 30 phút, rồi phát hiện sản phẩm trong giỏ đã hết từ 20 phút trước. Họ chuyển việc điều tiết từ edge vào tầng ứng dụng Rails để hàng đợi hiểu được nghiệp vụ thương mại. Đối chiếu với bài 05: cờ hết hàng của bạn chính là thứ phòng chờ Shopify 2017 thiếu.

Hai luồng cạnh nhau: phòng chờ leaky bucket của Shopify 2017 không có đường nào từ DB về nên không biết còn hàng hay hết; thiết kế của bạn có cờ hết hàng đi từ Postgres ngược về phòng chờ

Cloudflare 2021: tách hẳn hàng đợi khỏi tồn kho

Cloudflare Waiting Room (Cloudflare blog, 22/1/2021) là sản phẩm chạy trên edge của Cloudflare với Workers và Durable Objects, ra đời cùng lúc với chương trình Project Fair Shot cho các trang đăng ký tiêm vaccine COVID-19. Ràng buộc khác bạn rất rõ: Cloudflare phục vụ trang của người khác, không hề biết tồn kho hay schema của họ. Tham số cấu hình chính của phòng chờ là số người hoạt động tối đa trên site.

Gần chín tháng sau, bài về hàng đợi ngẫu nhiên (Cloudflare blog, 7/10/2021) trả lời đúng câu hỏi công bằng. FIFO (vào trước ra trước) hợp khi nhu cầu kéo dài, vì thứ tự quen thuộc và ước tính thời gian chờ chính xác. Hàng đợi ngẫu nhiên hợp với đợt bán ngắn, nhiều hype, sản phẩm có hạn: nếu xếp hàng FIFO dài thì sản phẩm có thể hết trước khi người đến sau kịp vào, và lợi thế của người vào sớm bị giảm mạnh. Với đợt bán 2 giây đầu của bạn, bài này gợi ý bạn xem lại thứ tự của phòng chờ ở bài 05: FIFO nghe công bằng nhưng thưởng cho người vào trước, nên ai có mạng tốt và vào sớm thì thắng.

3. Khi nào nâng trần thay vì chặn cửa?

Thử đoán trước

Hai công ty cùng dùng MySQL cho tồn kho. Công ty A tự sửa được nhân cơ sở dữ liệu. Công ty B bỏ Redis đang giữ chỗ để đưa giữ chỗ về chung bảng với sổ cái. Mỗi bên chọn chặn cửa hay nâng trần, và nút cổ chai thật của B nằm ở hàng nóng, ở CPU, hay ở chỗ khác?

Alibaba 2020: gộp ghi trên một row, nhờ sửa được nhân DB

Trong bài "How to Process Hot Spot Data in E-commerce Scenarios" (Alibaba Cloud, 2020), tác giả viết thông lượng của một hàng nóng xấp xỉ 1 giây chia cho thời gian giữ khoá của transaction. Nhớ lại phép nhẩm ở bài 01: giữ khoá 1 ms thì mỗi giây tối đa 1.000 lượt; rút còn 0,1 ms thì 10.000 lượt. Đó là lý do Alibaba tìm cách rút ngắn thời gian giữ khoá (bớt câu SQL, bớt round-trip) và gộp nhiều lượt trừ cùng một row thành một lần ghi. Một transaction hàng nóng điển hình trong bài gồm năm câu SQL và chín lượt trao đổi mạng giữa client và database. Ba hướng họ nêu: rút ngắn thời gian giữ khoá (đưa thông lượng một luồng lên gần 10.000 TPS), gộp commit (tăng thông lượng "nhiều lần"), và group update, nơi nhiều transaction cùng sửa một row được gộp thành một lần ghi (mười transaction cùng trừ được gộp thành một lần ghi). Cú pháp hint Commit On Success, Rollback On Fail, Target Affect Row gộp kiểm tra, ghi và commit vào một câu. Bài viết không công bố con số TPS trước và sau bằng chữ, chỉ có biểu đồ, nên bài này cũng không in số nào ngoài những gì bài viết ghi.

Ràng buộc khác bạn: họ vận hành MySQL phiên bản riêng của Alibaba Cloud, nên thêm được cú pháp mới vào chính database. Trên Postgres managed bạn không thêm được Rollback On Fail, nên con đường gộp ghi trên engine đóng với bạn. Công thức của họ vẫn dạy được bạn một điều: bài 03 đo cho thấy ở 50 kết nối UPDATE một câu (A) và FOR UPDATE hai round-trip (B) gần như ngang nhau (B bằng 0,88 đến 1,00 lần A): round-trip thừa của B nằm trong vùng giữ khoá nên kéo trần xuống cỡ 10%, ngang dải nhiễu. Cái giá thật của B lộ ra khi có việc chen giữa hai câu. Cùng một logic rút ngắn việc giữ khoá, họ đẩy tới cùng cực.

Shopify 2026: bỏ Redis, chia thành row khoá độc lập

Bài "We replaced Redis with MySQL for inventory reservations—and it scaled" (Shopify Engineering, 12/5/2026) mô tả mô hình cũ: giữ chỗ nằm ở Redis, sổ cái nằm ở MySQL, bước chốt cập nhật MySQL rồi dọn Redis bằng các thao tác không thể làm nguyên tử, nên có thể bán quá hoặc bán thiếu. Mô hình mới đưa giữ chỗ vào cùng MySQL với sổ cái để giữ và chốt nằm trong một transaction ACID. Mỗi tổ hợp item và location có một pool row, mỗi row là một đơn vị bán được, pool giới hạn 1.000 row (trần của pool, không phải tồn kho), con số chọn từ tốc độ giữ chỗ đỉnh quan sát trong các đợt flash sale, và SKIP LOCKED cho phép nhiều luồng lấy các row khác nhau mà không chờ nhau.

Shopify để mỗi đơn vị là một row riêng, như những chiếc ghế đánh số, nhưng chỉ giữ một pool tối đa 1.000 ghế còn trống cho mỗi item và location, nạp lại từ sổ cái. Ghế nào đang bị người khác giữ thì SKIP LOCKED bỏ qua để lấy ghế khác. Cái giá: nếu để toàn bộ tồn kho thành row thì việc quét chậm dần, nên pool phải có trần.

Ràng buộc khác bạn: họ có nhiều địa điểm tồn kho và một sổ cái đã nằm sẵn trong MySQL, và điều họ không chịu nổi là hai nơi lưu không nguyên tử. Nhớ lại bài 04: chỗ giữ 10 phút và worker thu hồi chỉ trọn vẹn khi giữ chỗ và đơn hàng cùng một nơi lưu, vì chỉ khi đó transaction mới nguyên tử. Với Shopify, lý do bỏ Redis không phải tốc độ mà là tính nguyên tử giữa giữ chỗ và sổ cái. Phép đo cho thấy độ trễ P90 của giữ chỗ vẫn ổn, CPU chưa chạm trần; cái nghẽn là các thread phải xếp hàng chờ trong MySQL và kết nối cạn ở ProxySQL. Gắn nhãn từng câu SQL theo quy trình nghiệp vụ thì thấy nhiều đoạn khác của checkout giữ kết nối quá lâu, giữ chỗ chỉ là giọt nước làm tràn ly.

Một row tồn kho: ba luồng cùng đổ vào một row, hai luồng phải chờ khoá; pool row với SKIP LOCKED: mỗi luồng lấy một row khác nhau, không ai chờ ai

Phép đo chia N shard của bạn (trên máy tác giả) tăng dưới tuyến tính: N bằng 64 chỉ đạt khoảng 8,3 lần so với một row. Shopify gặp cùng quy luật ở dạng khác, vì sau khi chia nhỏ thì khoá trên từng row không còn là nút cổ chai, kết nối mới là thứ cạn trước.

4. Case: đợt bán có bot và người không có mã

Thử đoán trước

Trong đợt bán vé có cả người thật lẫn bot, phòng chờ FIFO hay ngẫu nhiên có đủ không? Nếu chưa đủ, bạn thêm bước chặn ở đâu: trước phòng chờ, trong phòng chờ, hay ở bước thanh toán?

Ticketmaster, Eras Tour, 15/11/2022: xác minh trước cả phòng chờ

Số liệu dưới đây là theo chính Ticketmaster trong thông cáo "Taylor Swift | The Eras Tour Onsale Explained" (19/11/2022). Họ ghi nhận 3,5 tỉ system request, gấp 4 lần đỉnh trước đó. Hơn 3,5 triệu người đăng ký Verified Fan (cơ chế đăng ký trước rồi phát mã bán trước cho một phần người đăng ký), khoảng 1,5 triệu người được gửi mã, 2 triệu người còn lại vào danh sách chờ (waitlist). Hơn 2 triệu vé bán ra ngày 15/11. Ticketmaster đổ lỗi một phần cho số lượng lớn các vụ tấn công của bot, cùng những người không có mã vẫn vào trang. Theo CBS News (17-18/11/2022), đợt mở bán chung bị huỷ vì nhu cầu quá cao đè lên hệ thống bán vé trong khi lượng vé còn lại không đủ.

Ràng buộc khác bạn: bảng của bạn không có bot và không có định danh, mỗi người tối đa một sản phẩm là hết. Họ phải thêm một tầng chặn cửa trước phòng chờ: đăng ký và xác minh trước để loại bot, rồi cấp mã theo đợt để hạn chế số người vào cùng lúc. Chặn cửa vẫn chưa đủ, và lý do do chính Ticketmaster nêu: bot tấn công và người không có mã vẫn vào trang. Phiên điều trần của Thượng viện Mỹ ngày 24/1/2023 với đại diện Ticketmaster và SeatGeek, theo Semafor (23/1/2023), diễn ra sau sự cố này và xoay quanh độc quyền và thiếu cạnh tranh của ngành bán vé.

Ticketbox, 12/11/2024: chỉ quan sát được từ ngoài

Nguồn duy nhất của case này là báo chí, và bài này không suy đoán kiến trúc nội bộ. Tuổi Trẻ, theo PLO (12/11/2024) viết rằng, theo các diễn đàn, hệ thống Ticketbox sập ngay lúc mở bán concert "Anh trai vượt ngàn chông gai" tại Hà Nội, khoảng 150.000 người chờ, và vé hết chỉ sau 40 phút. Những gì suy ra được từ hành vi quan sát: lượng người chờ cùng bậc với 200.000 trong bảng của bạn, và người dùng chạm vào lỗi trước khi biết còn vé hay hết. Bài báo không nói lỗi nằm ở tầng nào, nên bạn cũng đừng gán nó cho kho hay cho kết nối. Cái đáng mang về là câu hỏi: nếu phản hồi "còn hay hết" của bạn không nằm ở một chỗ rẻ và không phụ thuộc kho, người dùng sẽ trả lời thay bạn bằng cách bấm lại.

5. Bảng tổng hợp: ràng buộc nào đổi lựa chọn nào

Chuỗi ba câu hỏi ràng buộc: có bot thì xác minh trước phòng chờ như Ticketmaster 2022; không được chạm vào kho thì phòng chờ ở edge như Shopify 2017 và Cloudflare 2021; sửa được nhân DB thì gộp ghi trên một row như Alibaba 2020; còn lại sửa ở ứng dụng và schema như Shopify 2022, 2026 và bảng của bạn

CaseNămRàng buộc khác bạnLựa chọnTrục
Shopify, load balancer lập trình được2017Edge không lưu trạng thái, không có kho chungLeaky bucket cộng cookie timestampTải đổ vào
Shopify, hàng đợi công bằng2022Hàng đợi ở edge không biết đã hết hàngChuyển điều tiết vào tầng Rails hiểu nghiệp vụTải đổ vào, giữ chỗ
Cloudflare Waiting Room2021Phục vụ site người khác, không thấy tồn khoPhòng chờ ở edge, FIFO hoặc ngẫu nhiênTải đổ vào
Alibaba, hàng nóng2020Sửa được nhân MySQL của chính mìnhGộp ghi và gộp commit trên một rowCơ chế trên một row
Shopify, giữ chỗ2026Nhiều địa điểm, sổ cái đã ở MySQL, cần nguyên tửBỏ Redis, pool row cộng SKIP LOCKEDGiữ chỗ
Ticketmaster, Eras Tour2022Bot và định danh, 3,5 triệu người đăng kýVerified Fan: xác minh trước, cấp mã theo đợtTải đổ vào
Ticketbox (báo chí)2024Không rõ, chỉ quan sát được lỗi và 40 phút hết véKhông suy đoánTải đổ vào (quan sát)

Năm dòng đổ về trục tải đổ vào vì họ đứng ở chỗ không chạm được vào kho (edge, site của người khác), gặp bot mà tồn kho không giải được, hoặc không rõ. Chỉ Alibaba và Shopify 2026 chạm được vào kho, nên chỉ hai nơi này sửa ở cơ chế trên một row và ở giữ chỗ.

6. Tự kiểm tra

Tự kiểm tra
0/6 câu đã trả lời
  1. Q1
    Vì sao Shopify 2026 không giữ Redis cho giữ chỗ, dù Redis vốn là kho đếm trong bộ nhớ?
  2. Q2
    Vì sao Alibaba nâng trần ngay trên một row còn Shopify 2017 và Cloudflare chặn cửa, dù cùng gặp hàng nóng hoặc tải dồn?
  3. Q3
    Bạn xây phòng chờ cho một nền tảng mà bạn không sở hữu tồn kho của khách. Bạn chọn gì, và nêu cái giá?
  4. Q4
    Cloudflare khuyên dùng hàng đợi ngẫu nhiên cho đợt bán ngắn có sản phẩm giới hạn. Với đợt 2 giây đầu của bạn, phòng chờ FIFO có hại ở đâu?
  5. Q5
    Đợt bán có bot và người không có mã. Bảng ràng buộc ở bài 01 của bạn thiếu gì, và bạn thêm tầng nào vào thiết kế?
  6. Q6
    Hệ thống của bạn giữ chỗ ở Redis và sổ cái ở Postgres, và thỉnh thoảng bán quá hoặc bán thiếu. Theo bài học Shopify 2026, bạn đổi gì, và cái giá là gì?

Tóm tắt

  • Ticketbox 2024 chỉ nên đọc như quan sát từ ngoài, không suy ra kiến trúc.

Liên hệ các bài khác

Bài tiếp theo tổng kết lại cả module và đối chiếu từng quyết định với ràng buộc ban đầu: Tổng kết module.

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Tổng kết case flash sale: thiết kế hoàn chỉnh và các cái giá