Trình bày case flash sale trong 45 phút phỏng vấn thiết kế
Đổi thiết kế flash sale bạn đã ráp thành một buổi trình bày 45 phút: hỏi ràng buộc, ước lượng, endpoint và dữ liệu, sơ đồ, đào sâu, mỗi câu dựa vào số đã đo.
TL;DR: Bài này là một buổi luyện có bấm giờ. Người phỏng vấn đưa một đề mơ hồ về đợt bán giày giới hạn, bạn có 45 phút để đi từ câu hỏi ràng buộc tới phần đào sâu. Bạn tự chia phút cho năm bước, tự chọn endpoint (địa chỉ HTTP mà client gọi vào) và bảng dữ liệu, tự quyết hai trục để đào sâu, rồi mới so với bản trình bày mẫu. Tiêu chí chính là neo: mỗi câu bạn nói phải truy ngược được về một dòng trong bảng ràng buộc hoặc một số đo bạn tự lấy trong các bài trước, không dựa vào tên công nghệ. Thang Mid, Senior, Staff ở mục 7.1 chỉ để tham khảo.
Các bài trước cho bạn đủ vật liệu: bảng ràng buộc, số đo trần một row, ba quyết định kèm cái giá, và một thiết kế ba lớp. Biết là một chuyện. Nói lại được trong 45 phút, trước một người cứ ngắt lời hỏi "sao không dùng Redis?", là chuyện khác hẳn.
1. Đề bài
Người phỏng vấn nói đúng một câu:
"Thiết kế giúp tôi trang bán 1.000 đôi giày bản giới hạn. Mở bán lúc 20:00."
Không có số người dùng, không có yêu cầu độ trễ, không nói gì về thanh toán. Việc của bạn là biến câu đó thành một buổi trình bày 45 phút, nói to thành tiếng như đang đứng trước bảng trắng. Đặt đồng hồ, ghi âm lại nếu được, và đừng mở các bài trước trong lúc nói.
Luật chơi:
- Tự hỏi ra ràng buộc. Người phỏng vấn sẽ trả lời bằng đúng bảng ràng buộc của case khi bạn hỏi trúng, còn không hỏi thì họ không nói.
- Mỗi lựa chọn phải kèm một cái giá, và cái giá đó phải có số.
- Hết 45 phút thì dừng, kể cả khi chưa nói xong. Phần chưa kịp nói cũng là dữ liệu cho lần sau.
2. Đầu vào, quá trình, đầu ra
| Nội dung | |
|---|---|
| Đầu vào | Câu đề ở mục 1, cộng những gì bạn đã đo trên Postgres của mình ở các bài 01 đến 05 |
| Quá trình | Năm bước: hỏi ràng buộc, ước lượng, endpoint và dữ liệu, sơ đồ tổng, đào sâu; năm phút cuối dành cho câu "đổi ràng buộc thì sao" |
| Đầu ra | Một bảng trắng (hoặc tờ giấy) có bảng ràng buộc ở góc, danh sách endpoint, các bảng dữ liệu, một sơ đồ đi theo đường của một lượt mua, và hai trục đã đào sâu |
Endpoint là một địa chỉ HTTP mà client gọi vào để làm một việc, ví dụ POST /sales/:id/holds. Trong các bài trước bạn chỉ làm việc với SQL và pgbench, nên đây là lần đầu module bắt bạn nói hệ thống nhận yêu cầu qua cửa nào.
3. Bài này ráp lại những gì
Mỗi bước trong buổi trình bày lấy vật liệu từ một bài và chứng minh một outcome của module:
| Bước | Lấy từ bài | Kiểm điều gì |
|---|---|---|
| Hỏi ràng buộc | Bài 01 | Estimate tải đỉnh và trần một row |
| Ước lượng | Bài 01, Bài 05 | Estimate tải đỉnh và trần một row |
| Endpoint và dữ liệu | Bài 03, Bài 04 | Choose cơ chế trừ tồn kho; Design luồng giữ chỗ |
| Sơ đồ tổng | Bài 07 | Cả ba trục quyết định |
| Đào sâu | Bài 02 đến Bài 05 | Diagnose bán quá; Choose cơ chế; Design giữ chỗ; Choose chặn cửa hay nâng trần |
| Đổi ràng buộc | Bài 06, Bài 07 | Justify vì sao hệ thống thật chọn khác |
Cả buổi kiểm outcome cuối của module: Justify thiết kế flash sale trong một buổi phỏng vấn 45 phút bằng ràng buộc và số đo của chính mình.
4. Starter: khung để bạn tự điền
Chép khung dưới ra giấy và điền hết các ô trống trước khi bấm giờ. Cột phút cộng lại phải bằng 45.
| Bước | Phút (từ đến) | Bạn sẽ nói gì | Dựa vào số nào |
|---|---|---|---|
| 1. Hỏi ràng buộc | ___ | ___ | ___ |
| 2. Ước lượng | ___ | ___ | ___ |
| 3. Endpoint và dữ liệu | ___ | ___ | ___ |
| 4. Sơ đồ tổng | ___ | ___ | ___ |
| 5. Đào sâu (chọn hai trục) | ___ | ___ | ___ |
| Đổi ràng buộc | ___ | ___ | ___ |
Thêm hai khung nữa cho phần endpoint và dữ liệu. Bao nhiêu dòng là do bạn quyết, thêm hay bớt tuỳ thiết kế:
Endpoint | Ai gọi | Việc nó làm | Trả về gì
_______ | ______ | ___________ | _________
...
Bảng dữ liệu | Cột chính | Ràng buộc giữ bất biến
____________ | _________ | ______________________
...
5. Gợi ý
Đọc khi bị kẹt ở một ô, không đọc trước. Mỗi gợi ý là một câu hỏi, câu trả lời vẫn là việc của bạn.
- Bước 1: bảng ràng buộc của bài 01 có tám dòng. Dòng nào mà người phỏng vấn không tự nói nhưng thiếu nó thì bạn không ước lượng được gì?
- Bước 2: tải đỉnh bằng số lượt trong cửa sổ dồn tải chia cho độ dài cửa sổ. Số kết nối bận cùng lúc bằng số lượt mỗi giây nhân với thời gian mỗi lượt giữ kết nối. Tự tính cả hai, rồi đặt cạnh trần một row và trần kết nối của Postgres. Cái nào vỡ trước?
- Bước 3: có bao nhiêu loại người gọi vào hệ thống lúc 20:00? Loại nào đông nhất, và loại đó có cần chạm DB không? Có bên nào gọi vào mà không phải người dùng?
- Bước 3: với mỗi endpoint, hỏi "client mất mạng rồi gọi lại thì chuyện gì xảy ra". Endpoint nào mà gọi hai lần ra hai kết quả khác nhau thì đang có lỗi.
- Bước 4: vẽ theo đường đi của một lượt mua, từ ngoài vào trong, thay vì vẽ một đống hộp rồi nối mũi tên.
- Bước 5: trục nào bạn có số đo của chính mình thì nói trục đó trước.
6. Lời giải: một bản trình bày mẫu
Đây là một cách chia, không phải cách duy nhất. Bạn chia phút khác mà vẫn kịp đào sâu thì vẫn đạt.
6.1 Năm bước theo phút
| Bước | Phút | Bạn nói gì, lấy từ đâu |
|---|---|---|
| 1. Hỏi ràng buộc | 0–5 | Hỏi lại tồn kho, số người chờ, cửa sổ dồn tải, giới hạn mỗi người, hạn thanh toán, tỉ lệ bỏ giữa chừng, yêu cầu phản hồi. Viết bảng của bài 01 vào góc bảng |
| 2. Ước lượng | 5–10 | 100.000 lượt trong 2 giây là 50.000 lượt mỗi giây; một row gánh cỡ 1.200 ở 50 kết nối, tức tải đỉnh gấp chừng 40 lần. Kết nối bận: 50.000 × 0,002 = 100 (giả định 2 ms mỗi lượt như bài 05; nếu 20 ms như bài 01 thì 1.000), vượt 97 kết nối ứng dụng dùng được. Kết luận: kết nối vỡ trước khoá row |
| 3. Endpoint và dữ liệu | 10–15 | Ba endpoint, hai bảng và một worker ở mục 6.2 đến 6.4 |
| 4. Sơ đồ tổng | 15–25 | Ba lớp của bài 07, đi theo đường của một lượt mua |
| 5. Đào sâu | 25–40 | Để người phỏng vấn chọn, hoặc tự đề xuất hai trong ba trục: cơ chế trên một row (bài 03), giữ chỗ và callback trễ (bài 04), chặn cửa hay nâng trần (bài 05) |
| Đổi ràng buộc | 40–45 | Đổi một dòng trong bảng, nói quyết định nào lật, quyết định nào giữ nguyên |
Bước 1 tốn tới năm phút là có chủ đích: chính bước này cho người nghe thấy bạn có tự đặt ràng buộc trước khi vẽ hay không, và mọi con số về sau đều đứng trên nó. Bước 2 thì ngược lại: nói nhanh, viết ba phép tính lên bảng, chốt một câu kết luận rồi đi tiếp.
Bài 01 đã đo: 1 kết nối được khoảng 1.800 TPS, 50 kết nối chỉ còn khoảng 1.200 TPS trên máy tác giả, vì mọi UPDATE xếp hàng sau khoá row. Trong buổi phỏng vấn, nói số của máy bạn, rồi nói tỉ lệ: tải đỉnh gấp hàng chục lần trần, máy nào cũng vậy.
Câu "kết nối vỡ trước khoá row" ở bước 2 cần nói kèm lý do, không thì nghe như học thuộc. Một nghìn lượt thắng chỉ mất chưa tới một giây; sau đó câu UPDATE ... WHERE stock > 0 trả 0 row mà không chờ khoá. Còn 50.000 lượt mỗi giây thì vẫn đổ vào, mỗi lượt vẫn cần một kết nối, và Postgres 16 với max_connections mặc định 100 (giữ lại 3 cho superuser) sẽ từ chối kết nối thứ 98 của ứng dụng bằng FATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute.
6.2 Ba endpoint
Lúc 20:00 có ba loại người gọi với ba mức tải rất khác nhau, nên tách thành ba endpoint để mỗi cửa được bảo vệ theo cách riêng.
| Endpoint | Ai gọi | Việc nó làm | Trả về |
|---|---|---|---|
GET /sales/:id/status | Mọi người đang chờ, đông nhất | Đọc cờ hết hàng và vị trí trong phòng chờ, không chạm DB | Còn hàng, hết hàng, hoặc vị trí đang chờ |
POST /sales/:id/holds | Chỉ lượt đã qua phòng chờ | Tạo chỗ giữ và trừ stock trong một transaction | 201 kèm hold_id, expires_at; 409 khi đã hết |
POST /payments/callback | Cổng thanh toán, không phải người dùng | Chuyển chỗ giữ từ pending sang paid bằng UPDATE có điều kiện | 200 cả khi cổng giao lại |
Cờ hết hàng cần nói cho đúng. Nếu nó nằm trong bộ nhớ ứng dụng, mỗi instance (một tiến trình server đang chạy) chỉ bật cờ khi chính nó thấy UPDATE trả 0 row. Chạy mười instance thì có mười bản cờ, bật lệch nhau vài mili giây; muốn đồng bộ thì đặt cờ ở cache dùng chung. Instance chưa có cờ vẫn gửi lượt xuống DB và nhận 0 row, kết quả vẫn đúng, chỉ tốn tải. Và cờ chỉ được sai theo hướng từ chối oan, không bao giờ theo hướng báo còn khi đã hết.
6.3 Idempotent: gọi lại không đổi kết quả
Đây là chỗ quan trọng nhất của phần endpoint. Một thao tác idempotent là thao tác gọi một lần hay nhiều lần đều để lại cùng một trạng thái. HTTP chỉ coi PUT, DELETE và các method chỉ đọc là idempotent (RFC 9110 §9.2.2); POST thì không, nên hai endpoint POST ở trên phải tự làm cho mình idempotent.
Với POST /holds, client mất mạng sau khi gửi rồi gọi lại là chuyện thường. Câu INSERT ... ON CONFLICT DO NOTHING của bài 04 chèn 0 row ở lần thứ hai. Đừng trả lỗi cho trường hợp đó: đọc lại chỗ còn sống của người này rồi trả chính hold_id cũ, để người dùng không thấy một lỗi giả cho đúng thứ họ đã có.
Với callback, cổng thanh toán giao lại khi không nhận được 200, nên một lần thanh toán có thể tới hai, ba lần. Câu chốt ở bài 04 tự chặn được chuyện chốt trùng:
UPDATE holds SET status = 'paid'
WHERE id = :hold_id AND status = 'pending' AND expires_at > now();
Một row bị ảnh hưởng là chốt xong. Không row nào thì đọc status: paid nghĩa là callback giao lại, trả 200 và không làm gì; expired, hoặc pending mà đã quá hạn, nghĩa là zombie, tức người đã trả tiền khi chỗ giữ đã hết hạn. Chiều ngược lại cũng cần: lúc gọi cổng thanh toán, gửi hold_id làm khoá idempotency để cổng không trừ tiền hai lần khi bạn retry.
Bài 04 đã chọn chính sách cho zombie: hoàn tiền tự động, thử trừ lại nếu còn hàng, hoặc gia hạn chỗ giữ khi người dùng bắt đầu thanh toán. Trong phỏng vấn, chỉ cần nói bạn chọn cách nào và cái giá của nó, đừng giảng lại cả ba.
6.4 Hai bảng và một worker
Dữ liệu chỉ có hai bảng của bài 04: items giữ stock với CHECK (stock >= 0), và holds giữ status, expires_at cùng unique partial index trên (user_id, item_id) cho chỗ còn sống. Worker thu hồi không phải endpoint, nó là một job quét holds theo expires_at bằng FOR UPDATE SKIP LOCKED, đánh dấu expired rồi cộng trả stock trong cùng một câu.
Không có endpoint POST /orders là cố ý: đơn hàng thật chỉ thành hình khi callback chốt paid, nên tách giữ chỗ khỏi thanh toán là quyết định của bài 04 chứ không phải quên.
Sơ đồ ba lớp của bài 07 cho thấy cả ba endpoint nằm ở đâu trên đường đi của một lượt mua:

6.5 Câu hay nghe và câu nên nói
Cùng một ý, nói kèm con số thì nghe như người đã làm, nói trống thì nghe như thuộc bài:
| Câu hay nghe | Câu nên nói |
|---|---|
| "Dùng Redis cho nhanh." | "Tải đỉnh gấp khoảng 40 lần trần một row. Redis đỡ được kết nối nhưng thêm một source of truth, replication lại bất đồng bộ nên failover có thể mất lượt trừ. Tôi sẽ đối soát và để Postgres giữ lời cuối." |
| "Khoá row lại là xong." | "Một câu UPDATE có điều kiện, trả 0 row là hết. Khoá giữ tới COMMIT nên transaction phải ngắn, không gọi thanh toán bên trong." |
| "Chia 64 shard thì gấp 64 lần." | "Trên máy tôi, 64 shard chỉ được khoảng 8 lần, và pool kết nối vẫn cạn trước." |
| "Thêm PgBouncer." | "Pooler chỉ cho default_pool_size lượt, mặc định 20, chạm Postgres; khi đã nâng max_client_conn (mặc định 100), phần dư xếp hàng tới 120 giây. Lỗi kết nối biến thành độ trễ, p99 dưới 1 giây vẫn vỡ nếu không chặn cửa." |
| Không nhắc gì tới người trả tiền sau hạn | "Chỗ giữ hết hạn mà callback tới sau thì người đó là zombie; tôi chọn hoàn tiền tự động, cái giá là khiếu nại tăng theo số zombie." |
Bài 05 đo được 64 shard gấp 8,3 lần trên máy 10 nhân của tác giả và tính ra 100 kết nối bận so với 97 khả dụng. Hai số đó là lý do bạn chọn chặn cửa cộng cờ hết hàng, và cũng là câu trả lời cho mọi câu "sao không dùng X" ở bảng trên.
7. Mở rộng
7.1 Tự chấm bản trình bày
Nghe lại bản ghi âm, rồi làm ba việc theo thứ tự:
- Với mỗi câu khẳng định, đánh dấu nó neo vào dòng ràng buộc nào hoặc số đo nào của bạn.
- Đếm những câu không neo được vào đâu, thường là câu chỉ có tên công nghệ.
- Với mỗi câu không neo, viết lại thành câu có số, hoặc gạch bỏ.
Sau đó mới đối chiếu với thang ba cấp. Đây là thang gợi ý, mỗi công ty đặt kỳ vọng khác nhau, đừng coi nó là chuẩn ngành:
| Cấp | Đủ khi bạn |
|---|---|
| Mid | Đưa ra UPDATE có điều kiện đúng, giải thích được vì sao đếm đơn rồi ghi vẫn bán quá |
| Senior | Gắn cái giá có số cho từng lớp, tự chỉ ra kết nối cạn trước khoá row, xử lý được callback trễ và callback giao lại |
| Staff | Nói được thiết kế lật khi ràng buộc đổi, và kế hoạch vận hành: kiểm bất biến ngay trong đợt bán, đối soát khi có Redis, điều gì xảy ra nếu primary Postgres failover giữa đợt, chặn bot |
Cột Staff có hai thứ case này chưa dạy kỹ. Failover của primary Postgres giữa đợt bán chưa có bài nào; bài 05 mới nói tới chuyện Redis mất lượt trừ khi failover. Chống bot thì chỉ chạm qua ở case Ticketmaster của bài 06. Biết mình chưa trả lời được câu nào cũng là một phần của buổi luyện, và nói thẳng "phần này tôi chưa đo" vẫn tốt hơn bịa.
7.2 Làm lại với ràng buộc khác
Chạy lại cả buổi 45 phút, mỗi lần đổi một dòng, và để ý bước nào trong lời giải phải viết lại:
- 1.000 sản phẩm thành 100.000 sản phẩm: hàng nóng còn nguội đi sau một giây không?
- Mỗi người được mua 5 đôi: unique partial index và câu
UPDATEphải đổi gì? - Hạn thanh toán 10 phút thành 30 giây: endpoint nào chịu thêm tải, zombie tăng hay giảm?
8. Điều bạn vừa làm được
Bạn vừa tự thiết kế ba endpoint lần đầu trong module, và biết chỗ nào phải idempotent vì mạng sẽ giao lại.
Đào sâu
- RFC 9110 §9.2.2 Idempotent Methods: định nghĩa idempotent, và chỉ
PUT,DELETEcùng các method an toàn được coi là idempotent,POSTkhông nằm trong đó. - Stripe: Idempotent requests: Stripe lưu status code và body của lần gọi đầu theo khoá idempotency, retry cùng khoá nhận lại đúng kết quả đó; khoá có thể bị dọn sau ít nhất 24 giờ.
- PgBouncer configuration:
default_pool_sizemặc định 20,query_wait_timeoutmặc định 120 giây.
Liên hệ các bài khác
- Bài 07: Tổng kết có bảng cái giá và checklist bất biến, số đo, bẫy; đem cả hai vào phòng phỏng vấn trong đầu.
- Bài 04: Giữ chỗ có hạn là nguồn của hai bảng dữ liệu và câu chốt callback ở mục 6.3 và 6.4.
- Bài 05: Tải vượt trần cho bạn hai con số đáng nói nhất buổi: 100 kết nối bận và 8,3 lần ở 64 shard.
- Bài 06: Đối chiếu thực tế là vật liệu cho câu hỏi "công ty X làm khác, sao bạn không làm vậy?".
Bài tiếp theo
Case flash sale khép lại ở đây. Muốn đào sâu phần chia dữ liệu mà bài 05 chỉ chạm qua, đọc thêm (không phải tiên quyết) Key range vs hash trong data-phan-tan.
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.
Tổng kết case flash sale: thiết kế hoàn chỉnh và các cái giá
Khoá tiếp theoNews feed: trả giá lúc ghi hay lúc đọc
Case thứ hai của tuyến Thiết kế Hệ thống. Bạn tự đặt ràng buộc đọc/ghi lệch, đo chi phí dựng feed trên Postgres của mình, thấy phương án gộp lúc đọc vỡ p99 ở đâu, rồi tự chọn chỗ trả giá fan-out, cách xử lý tài khoản hàng triệu người theo dõi và điều người dùng thấy khi fan-out chạy trễ — mỗi lựa chọn kèm cái giá — trước khi đối chiếu cách các mạng xã hội thật đã làm.
8 bài · ~1.5 giờThảo luận & góp ý
Hỏi đáp về bài này