Harbor

Đấu giá trực tuyến

Trung bìnhTranh chấp ghiHẹn giờ chính xácReal-timeCông bằngGiống eBay Auctions, Yahoo! Auctions

Hiểu đề

Đấu giá trực tuyến là gì? Sàn đấu giá nơi người bán đăng món hàng với giá khởi điểm và giờ kết thúc, người mua trả giá cao dần. Hết giờ, người trả cao nhất thắng; ví dụ một chiếc đồng hồ cổ có 3.000 người theo dõi và 200 lượt trả giá trong 2 phút cuối.

Yêu cầu chức năng

  1. Người bán có thể tạo phiên đấu giá: ảnh, mô tả, giá khởi điểm, bước giá, giờ kết thúc
  2. Người dùng có thể xem phiên và thấy giá cao nhất cập nhật gần như real-time
  3. Người dùng có thể đặt giá, giá mới phải cao hơn giá hiện tại ít nhất một bước giá
  4. Hệ thống đóng phiên đúng giờ, chọn người thắng và báo cho người thắng, người bán

Yêu cầu phi chức năng

  • Nhất quán mạnh ở bước đặt giá: mỗi phiên luôn có đúng 1 người dẫn đầu, 0 bid được nhận sau giờ kết thúc
  • Đặt giá p95 < 300 ms, kể cả lúc 200 bid/s đổ vào một phiên ở phút cuối
  • Giá mới đến người đang xem trong ≤ 1 s (p95)
  • Đóng phiên và gửi kết quả trong ≤ 5 s sau giờ kết thúc
  • Độ sẵn sàng 99,9%, chịu đỉnh ×10 lượt bid vào khung giờ tối

Ngoài phạm vi: Thanh toán, vận chuyển và tranh chấp sau khi thắng; Chi tiết thuật toán đấu giá ủy quyền (proxy bid), chỉ nhắc ở phần công bằng; Các kiểu đấu giá khác: đấu giá kín, đấu giá Hà Lan; Phát hiện bid ảo (shill bidding) và đánh giá người bán.

Thực thể và API

User
Người mua hoặc người bán: id, tên hiển thị, trạng thái xác minh.
Auction
Phiên đấu giá: seller_id, tiêu đề, giá khởi điểm, bước giá, current_price, leader_id, end_at, status OPEN/CLOSED, version.
Bid
Một lượt trả giá: auction_id, bidder_id, amount, created_at lấy theo đồng hồ DB, idempotency_key, kết quả ACCEPTED/REJECTED.
Watch
Người dùng theo dõi một phiên để nhận thông báo khi bị vượt giá hoặc sắp kết thúc.
AuctionEvent (outbox)
Sự kiện bid_accepted, end_extended, auction_closed ghi cùng transaction để phát cho real-time và thông báo.
MethodEndpointGhi chú
POST/v1/auctionsTạo phiên. Ảnh tải thẳng lên S3 bằng presigned URL, body chỉ chứa key ảnh.
GET/v1/auctions/{id}Chi tiết phiên kèm current_price, leader, end_at và server_time để client chỉnh đồng hồ đếm ngược.
POST/v1/auctions/{id}/bidsĐặt giá: body {amount}, header Idempotency-Key. Trả 409 nếu giá không đủ cao, 410 nếu phiên đã đóng.
GET/v1/auctions/{id}/streamSSE: đẩy {price, leader, end_at, seq} mỗi khi giá đổi hoặc phiên được gia hạn.
GET/v1/auctions?q=&category=&endingSoon=Tìm và lọc phiên đang mở, sắp xếp theo giờ kết thúc.
GET/v1/auctions/{id}/bids?cursor=Lịch sử trả giá, phân trang theo cursor.

Thiết kế tổng thể

Đi lần lượt từng yêu cầu chức năng. Mục tiêu ở đây là chạy đúng, việc tối ưu để dành cho phần đào sâu.

1) Người bán có thể tạo phiên đấu giá

Tạo phiên là thao tác ít và đơn giản. Phần nặng là ảnh, nên ảnh đi thẳng lên S3 rồi phục vụ qua CDN, DB chỉ lưu metadata.

  1. 1Client xin presigned URL, tải ảnh lên S3
  2. 2Client gửi POST /v1/auctions với thông tin món hàng, giá khởi điểm, bước giá, end_at
  3. 3API kiểm tra end_at hợp lệ (ví dụ 1-10 ngày), ghi Auction status OPEN
  4. 4API đăng ký hạn kết thúc vào scheduler (hàng DB có index end_at, sau này thêm Redis ZSET)

Nguồn: Grokking Modern System Design · ch.20 Blob Store ↗ · mục "Detailed design" và "Stream a file" ; Grokking Modern System Design · ch.11 Content Delivery Network (CDN) ↗ · mục "Content caching strategies in CDN"

2) Người dùng có thể xem phiên và thấy giá real-time

Trang phiên gồm phần tĩnh (ảnh, mô tả) cache được ở CDN và phần động nhỏ (giá, người dẫn đầu, giờ kết thúc). Phần động được đẩy qua SSE.

  1. 1Client tải trang và ảnh từ CDN
  2. 2Client gọi GET /v1/auctions/{id} lấy giá hiện tại, seq và server_time
  3. 3Client mở SSE /stream, nhận sự kiện bid_accepted, end_extended, auction_closed
  4. 4Mất kết nối thì gọi lại GET để lấy trạng thái mới nhất, bỏ qua sự kiện có seq cũ hơn

Nguồn: Grokking Modern System Design · ch.11 Content Delivery Network (CDN) ↗ · mục "Content caching strategies in CDN" ; Redis in Action ↗ · ch.3 · §3.6 Publish/subscribe

3) Người dùng có thể đặt giá

Đây là chỗ tranh chấp duy nhất cần nhất quán mạnh. Một câu UPDATE có điều kiện trên hàng Auction quyết định ai thắng, kèm kiểm tra giờ kết thúc bằng đồng hồ DB.

  1. 1Client gửi POST /bids với amount và Idempotency-Key
  2. 2API mở transaction: UPDATE auctions SET current_price=:amt, leader_id=:u, version=version+1 WHERE id=:id AND status='OPEN' AND end_at > now() AND current_price + min_increment <= :amt
  3. 31 hàng được cập nhật: INSERT Bid ACCEPTED và AuctionEvent vào outbox trong cùng transaction
  4. 40 hàng: đọc lại phiên, trả 409 kèm giá hiện tại hoặc 410 nếu đã hết giờ
  5. 5Sau commit, publish giá mới cho người xem và báo người vừa bị vượt giá

Nguồn: Microservices Patterns ↗ (.zip) · ch.6 · §6.1.3 Handling concurrent updates using optimistic locking ; Chưa có nguồn trong thư viện: UPDATE có điều kiện là kỹ thuật DB chung

4) Hệ thống đóng phiên đúng giờ và báo kết quả

Việc chặn bid trễ đã nằm trong điều kiện end_at > now() của câu UPDATE, nên scheduler chỉ cần chốt kết quả nhanh. Đóng phiên phải idempotent vì có thể chạy hai lần.

  1. 1Scheduler lấy các phiên có end_at <= now() và status OPEN
  2. 2UPDATE auctions SET status='CLOSED' WHERE id=:id AND status='OPEN' AND end_at <= now()
  3. 3Cập nhật được 1 hàng thì ghi outbox auction_closed với leader_id là người thắng
  4. 4Worker gửi email/push cho người thắng và người bán, đẩy auction_closed cho người đang xem

Nguồn: Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · mục "Task idempotency" ; Redis in Action ↗ · ch.6 · §6.4.2 Delayed tasks (tr. 137)

Hạ tầng theo quy mô

Cùng một sản phẩm, ba mức người dùng. Chọn từng mức để xem sơ đồ, phép tính và cấu hình server thay đổi thế nào.

Người dùng
10.000 DAU, 2.000 phiên đang mở
Tải đỉnh
≈ 135 req/s đỉnh
Chi phí ước lượng
≈ 50–150 USD/tháng (ước lượng)

Sàn đấu giá đồ sưu tầm của một cộng đồng nhỏ. Hai instance Node sau CDN, một Postgres managed giữ cả giá hiện tại lẫn lịch hẹn kết thúc; giá trên trang cập nhật bằng polling.

Người dùngNgười dùngCDNCDNLưu trữS3 ảnhDịch vụAPI Node.jsWorkerJob đóng phiênDatabasePostgreSQLBên ngoàiEmail (SES)HTTPSảnhcache missUPDATE điều kiệnend_at <= now()báo người thắng

Ước lượng

  • Lượt xem: 10.000 DAU × 30 req / 86.400 ≈ 3,5 req/s, đỉnh ×10 ≈ 35 req/s
  • Polling giá: 500 người cùng xem phiên × 1 req / 5 s = 100 req/s
  • API: ceil((35 + 100) / 600) = 1 → chạy 2 instance để dự phòng
  • Bid: 10.000 × 2 bid / 86.400 ≈ 0,23 bid/s; phút cuối phiên hot ≈ 50 bid / 60 s ≈ 0,8 bid/s trên một hàng Auction
  • Kết thúc phiên: 2.000 phiên / 7 ngày ≈ 286 phiên/ngày, quét DB mỗi 1 s là thừa sức
  • Lưu trữ: bid 20.000 × 200 B × 365 ≈ 1,5 GB/năm; ảnh ≈ 290 phiên mới/ngày × 5 ảnh × 500 KB × 365 ≈ 265 GB/năm trên S3

Nguồn con số: Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · mục "Standard numbers to remember" và "Requests estimation" ; Quy tắc ước lượng của khoá học · /hoc/chon-server

Bảng hạ tầng

Thành phầnChọnSố lượngCấu hìnhVì sao
CDNCloudflare (gói free/pro)1Cache ảnh, JS/CSS, trang danh sách phiênẢnh chiếm gần hết băng thông, để CDN gánh là rẻ nhất.
APINode.js (NestJS), Docker trên Cloud Run22 vCPU · 4 GB RAM135 req/s chỉ cần 1 instance, chạy 2 để không chết khi 1 cái lỗi.
DatabasePostgreSQL managed12 vCPU · 8 GB · 50 GB SSDDưới 1 bid/s mỗi phiên, UPDATE có điều kiện trên hàng Auction đảm bảo một người dẫn đầu; dữ liệu 1,5 GB/năm.
Job đóng phiênVòng lặp trong API, SKIP LOCKED1Chạy mỗi 1 s, index (status, end_at)286 phiên/ngày; SKIP LOCKED cho phép cả 2 instance cùng chạy mà không đóng trùng.
Lưu ảnhS3 / GCS1 bucketPresigned upload265 GB/năm, không bao giờ để ảnh trong DB.
EmailAmazon SES / SendGrid1Dịch vụ bên ngoàiVài trăm email kết quả mỗi ngày, gửi thẳng từ API là đủ.
Thay đổi so với mức trước
  • Điểm xuất phát: một API, một Postgres, CDN và S3 cho ảnh
  • Đặt giá bằng UPDATE có điều kiện, chưa cần Redis hay queue
  • Giá trên trang cập nhật bằng polling 5 s, đóng phiên bằng vòng quét mỗi giây
Nút thắt tiếp theo

Polling tăng tuyến tính theo số người xem: 10.000 người cùng xem là 2.000 req/s trống rỗng đổ vào API và DB. Cần đẩy giá chủ động và cache giá hiện tại ngoài DB.

Cấu hình và chi phí là ước lượng để học và trình bày trong phỏng vấn. Xem cách tính ở bài Chọn server.

So sánh nhanh ba mức

Thành phầnMức 1Mức 2Mức 3
Người dùng10.000 DAU, 2.000 phiên đang mở1 triệu DAU, 500.000 phiên đang mở50 triệu DAU, 20 triệu phiên đang mở
Chi phí≈ 50–150 USD/tháng (ước lượng)≈ 3.000–7.000 USD/tháng (ước lượng, tăng dần theo dung lượng ảnh)≈ 100.000–250.000 USD/tháng (ước lượng, phần lớn là lưu ảnh và CDN)
CDN
Cloudflare (gói free/pro)
1 × Cache ảnh, JS/CSS, trang danh sách phiên
Cloudflare / CloudFront
1 × Cache ảnh, trang phiên, danh sách sắp kết thúc 5 s
Chưa cần
API
Node.js (NestJS), Docker trên Cloud Run
2 × 2 vCPU · 4 GB RAM
Chưa cầnChưa cần
Database
PostgreSQL managed
1 × 2 vCPU · 8 GB · 50 GB SSD
PostgreSQL managed (HA)
1 primary + 1 read replica × 4 vCPU · 16 GB · 500 GB SSD
PostgreSQL chia shard theo auction_id (Citus hoặc định tuyến ở app)
16 shard × (1 primary + 1 replica) × 8 vCPU · 32 GB · 1 TB SSD mỗi node
Job đóng phiên
Vòng lặp trong API, SKIP LOCKED
1 × Chạy mỗi 1 s, index (status, end_at)
Chưa cầnChưa cần
Lưu ảnh
S3 / GCS
1 bucket × Presigned upload
Chưa cầnChưa cần
Email
Amazon SES / SendGrid
1 × Dịch vụ bên ngoài
Chưa cầnChưa cần
API (gồm đặt giá)Chưa cần
Node.js (NestJS), container autoscale
2–4 (autoscale) × 2 vCPU · 4 GB RAM
Chưa cần
SSE gatewayChưa cần
Node.js SSE + Redis pub/sub
4 × 2 vCPU · 4 GB RAM
Chưa cần
RedisChưa cần
ElastiCache / Memorystore
1 primary + 1 replica × 2 vCPU · 4 GB
Chưa cần
SchedulerChưa cần
Node.js worker đọc Redis ZSET
2 × 1 vCPU · 2 GB RAM
Node.js worker, mỗi pod giữ lease một bucket
16 pod × 1 vCPU · 2 GB mỗi pod
QueueChưa cần
Amazon SQS / Google Pub/Sub
1 × Managed, có dead-letter queue
Chưa cần
Worker thông báoChưa cần
Node.js consumer
2 × 1 vCPU · 2 GB RAM
Chưa cần
CDN + WAFChưa cầnChưa cần
CloudFront / Cloudflare + bot management
1 × Cache ảnh, trang phiên, danh sách; chặn bot
API đọcChưa cầnChưa cần
Node.js trên Kubernetes, 3 AZ
78 pod (HPA) × 2 vCPU · 4 GB mỗi pod
Bid serviceChưa cầnChưa cần
Node.js trên Kubernetes, 3 AZ
29 pod (HPA) × 2 vCPU · 4 GB mỗi pod
Gateway real-timeChưa cầnChưa cần
Node.js SSE, Redis sharded pub/sub
125 pod × 2 vCPU · 4 GB mỗi pod
Redis ClusterChưa cầnChưa cần
ElastiCache Redis 7 Cluster
3 shard × (1 primary + 1 replica) × 4 vCPU · 16 GB mỗi node
KafkaChưa cầnChưa cần
MSK / Confluent
3 broker × Topic auction-events 24 partition, RF 3
SearchChưa cầnChưa cần
OpenSearch
6 node × 8 vCPU · 32 GB mỗi node

Đào sâu

Mỗi câu hỏi đi từ cách đơn giản đến cách người phỏng vấn mong đợi ở cấp senior.

1) Hàng trăm người trả giá cùng lúc vào một phiên. Làm sao giá cao nhất luôn đúng?

Cách chưa tốt
Đọc giá hiện tại ở app rồi INSERT bid

API SELECT current_price, so sánh trong Node.js rồi ghi bid mới. Hai request đọc cùng giá 1.000.000 đ, cả hai thấy mình hợp lệ và cùng được báo là dẫn đầu.

Đánh đổi: Code dễ viết nhưng sai ngay khi có hai bid đến gần nhau, đúng vào lúc quan trọng nhất của phiên.

⚠ Chưa có nguồn trong thư viện: đọc-rồi-ghi ở app là nhận định chung

Cách tốt
SELECT ... FOR UPDATE trên hàng Auction

Mở transaction, khóa hàng phiên, kiểm tra giá và giờ, cập nhật rồi commit. Các bid cùng phiên xếp hàng chờ khóa nên kết quả đúng. Optimistic locking theo cột version (Microservices Patterns §6.1.3) cũng cho kết quả tương tự, chỉ khác là bên thua phải thử lại.

Đánh đổi: Mỗi bid tốn 2-3 round-trip trong lúc giữ khóa, phiên hot ở phút cuối bị xếp hàng lâu và dễ cạn connection pool.

Nguồn: Microservices Patterns ↗ (.zip) · ch.6 · §6.1.3 Handling concurrent updates using optimistic locking ; Chưa có nguồn trong thư viện: SELECT FOR UPDATE là kỹ thuật DB chung

Cách tốt nhất
Một câu UPDATE có điều kiện + ghi Bid và outbox trong cùng transaction

Đưa toàn bộ luật vào WHERE: status OPEN, end_at > now(), current_price + min_increment <= amount. DB tự khóa hàng trong vài ms và kiểm tra lại điều kiện sau khi chờ, nên chỉ một bid ở mỗi mức giá thắng. Bid thua trả về ngay với giá mới để client hiện cho người dùng. Idempotency-Key chặn việc bấm hai lần.

Đánh đổi: Một hàng chịu được vài trăm bid/s; vượt mức đó thì khóa hàng trở thành nút thắt và cần tuần tự hóa phía trước.

Nguồn: Microservices Patterns ↗ (.zip) · ch.3 · §3.3.7 Transactional messaging ; Chưa có nguồn trong thư viện: UPDATE có điều kiện là kỹ thuật DB chung

Cách tốt nhất
Tuần tự hóa theo auction_id (single writer) khi phiên quá nóng

Bid được đẩy vào partition Kafka chọn theo auction_id, mỗi partition có đúng một consumer xử lý tuần tự và giữ giá hiện tại trong bộ nhớ, loại ngay bid thấp trước khi ghi DB. Câu UPDATE có điều kiện vẫn giữ làm lớp an toàn cuối.

Đánh đổi: Đặt giá thành bất đồng bộ, client phải chờ kết quả qua SSE; thêm độ trễ và phức tạp nên chỉ đáng khi một phiên thật sự vượt vài trăm bid/s.

Nguồn: Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · mục "Ordering of messages" (Strict ordering) ; Chưa có nguồn trong thư viện: single writer theo auction_id là thiết kế của khoá học

2) Làm sao đóng phiên đúng giờ khi có hàng triệu phiên kết thúc rải rác trong ngày?

Cách chưa tốt
Cron 5 phút quét bảng, và bid path không kiểm tra giờ

Cron tìm phiên hết hạn và đóng lại. Trong 5 phút trễ đó API vẫn nhận bid vì chỉ dựa vào status, nên người trả giá sau giờ kết thúc lại thắng.

Đánh đổi: Sai luật đấu giá và mất niềm tin của người dùng; quét cả bảng mỗi lần còn tốn tài nguyên.

⚠ Chưa có nguồn trong thư viện: cron quét là nhận định chung

Cách tốt
Bid kiểm tra end_at > now() + quét DB mỗi giây bằng index

Tính đúng đắn nằm ở bid path: dùng đồng hồ DB, không tin giờ của client hay của từng server. Job mỗi giây chạy SELECT ... WHERE status='OPEN' AND end_at <= now() ORDER BY end_at LIMIT 500 FOR UPDATE SKIP LOCKED, nhiều worker chạy song song không giẫm lên nhau.

Đánh đổi: Đủ tốt tới vài chục phiên/s; khi bảng lớn và nhiều phiên kết thúc vào phút tròn thì việc quét liên tục đè lên primary.

⚠ Chưa có nguồn trong thư viện: quét bằng index và SKIP LOCKED là kỹ thuật DB chung

Cách tốt nhất
Redis ZSET theo end_at chia bucket, worker có lease, đóng phiên idempotent

Mỗi phiên là một phần tử ZSET với score = end_at (mẫu delayed tasks, Redis in Action §6.4.2). Worker gọi ZRANGEBYSCORE 0 now mỗi 200-500 ms trên bucket của mình, đọc lại DB rồi chạy UPDATE ... WHERE status='OPEN' AND end_at <= now(). Nếu phiên đã được gia hạn thì chỉ cần ZADD lại với end_at mới. ZSET mất dữ liệu thì dựng lại từ index end_at trong DB, đúng như cách Grokking ch.23 tách lưu trữ task và phần thực thi.

Đánh đổi: Thêm một thành phần phải theo dõi độ trễ (lag giữa end_at và lúc đóng); nhưng vì bid path tự chặn bid trễ nên scheduler chậm vài giây chỉ làm thông báo đến muộn, không làm sai kết quả.

Nguồn: Redis in Action ↗ · ch.6 · §6.4.2 Delayed tasks (tr. 137) ; Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · mục "Components" (tách lưu task và thực thi)

3) Làm sao đẩy giá mới đến hàng nghìn người đang xem một phiên trong dưới 1 giây?

Cách chưa tốt
Client poll GET /auctions/{id} mỗi giây

3.000 người xem một phiên hot tạo 3.000 req/s chỉ cho một trang, phần lớn trả về dữ liệu không đổi. Nhân lên hàng chục nghìn phiên thì origin và DB gánh không nổi.

Đánh đổi: Dễ làm nhất, chấp nhận được ở quy mô nhỏ với chu kỳ 3-5 s.

⚠ Chưa có nguồn trong thư viện: polling là nhận định chung

Cách tốt
Polling có version và cache 1 s ở CDN

Client gửi seq đang có, server trả 304 hoặc chỉ phần giá mới. Response của mỗi phiên cache 1 s ở CDN nên 3.000 người dùng chung khoảng 1 request/s tới origin.

Đánh đổi: Trễ 1-2 s và vẫn tốn request rỗng; khó đạt p95 ≤ 1 s ở phút cuối.

Nguồn: Grokking Modern System Design · ch.11 Content Delivery Network (CDN) ↗ · mục "Content caching strategies in CDN"

Cách tốt nhất
SSE gateway + pub/sub theo kênh của từng phiên

Sau commit, Bid service publish {price, leader, end_at, seq} lên kênh auction:{id} (Grokking ch.18, Redis in Action §3.6). Gateway chỉ subscribe các phiên mà client của nó đang xem rồi đẩy xuống qua SSE. Phiên quá nóng thì gộp, mỗi 250 ms gửi một bản giá mới nhất. Client so seq để bỏ tin cũ và gọi lại GET khi kết nối lại.

Đánh đổi: Phải vận hành hàng loạt kết nối mở và kênh pub/sub; pub/sub không lưu lại tin nên client luôn cần đường GET để bù.

Nguồn: Grokking Modern System Design · ch.18 Pub-sub ↗ · mục "High-level design" ; Redis in Action ↗ · ch.3 · §3.6 Publish/subscribe

4) Chống "bắn tỉa" ở giây cuối (sniping) và giữ phiên công bằng thế nào?

Cách chưa tốt
Đóng cứng đúng giờ, sắp thứ tự theo giờ của client

Bot gửi bid ở giây cuối, người khác không kịp phản ứng. Lấy timestamp từ client hoặc từ từng API server có đồng hồ lệch nhau thì thứ tự bid còn có thể bị làm giả.

Đánh đổi: Người có bot luôn thắng, người dùng thật thấy bất công và bỏ sàn.

⚠ Chưa có nguồn trong thư viện: sniping là nghiệp vụ đấu giá

Cách tốt
Đấu giá ủy quyền (proxy bid) + giờ chuẩn từ server

Người dùng nhập giá tối đa, hệ thống tự trả thay từng bước giá. Người đã đặt giá tối đa cao thì bị bắn tỉa cũng không thua. Client hiển thị đồng hồ theo server_time để ai cũng thấy cùng một mốc kết thúc.

Đánh đổi: Giảm lợi thế của sniper nhưng không loại bỏ; logic proxy làm câu điều kiện phức tạp hơn.

⚠ Chưa có nguồn trong thư viện: proxy bid là nghiệp vụ đấu giá

Cách tốt nhất
Soft close: bid trong 2 phút cuối gia hạn phiên, làm nguyên tử trong cùng câu UPDATE

Câu UPDATE đặt giá thêm SET end_at = GREATEST(end_at, now() + interval '2 minutes'), extensions = extensions + 1 khi bid rơi vào 2 phút cuối, kèm giới hạn tổng thời gian gia hạn (ví dụ 30 phút). Ai cũng có 2 phút để đáp trả nên bid giây cuối hết tác dụng. Sự kiện end_extended được đẩy cho mọi người xem; scheduler tự ZADD lại khi thấy end_at đã đổi. Thêm rate limit theo user cho endpoint bid để chặn bot spam.

Đánh đổi: Giờ kết thúc không còn cố định nên người mua khó canh giờ; phải hiển thị rõ luật gia hạn và giới hạn tối đa để tránh phiên kéo dài vô tận.

Nguồn: Chưa có nguồn trong thư viện: soft close là nghiệp vụ đấu giá ; Grokking Modern System Design · ch.19 Rate Limiter ↗ · mục "Where to place the rate limiter"

Trong phỏng vấn

Nên nói
  • “Chỉ bước đặt giá cần nhất quán mạnh, tôi đưa toàn bộ luật vào WHERE của một câu UPDATE trên hàng Auction.”
  • “Bid sau giờ bị chặn ngay trong câu UPDATE bằng end_at > now() theo đồng hồ DB, nên scheduler chậm chỉ làm thông báo đến muộn chứ không làm sai kết quả.”
  • “Giá real-time đi qua SSE và pub/sub theo kênh của từng phiên, client luôn có đường GET để bù khi mất tin.”
  • “Soft close gia hạn 2 phút khi có bid cuối, làm nguyên tử trong cùng câu UPDATE và có giới hạn tổng thời gian.”
Bẫy hay gặp
  • Đọc giá ở app rồi mới ghi, hai người cùng được báo là dẫn đầu.
  • Chỉ dựa vào cron để đóng phiên mà không kiểm tra giờ ở bước đặt giá.
  • Lấy timestamp từ client hoặc từ từng API server để quyết định thứ tự bid.
  • Đặt giá qua WebSocket mà không có Idempotency-Key, mạng chập chờn sinh bid trùng.

Đọc thêm

Tranh chấp tài nguyên
Chống bán vượt và đặt trùng khi nhiều request cùng giành một tài nguyên: khóa bi quan, optimistic locking, UPDATE có điều kiện, distributed lock, hàng đợi theo key và giữ chỗ có hạn.
Cập nhật realtime
Đẩy thay đổi tới client trong vài trăm mili giây: chọn giữa polling, long polling, SSE, WebSocket, rồi fan-out qua nhiều gateway, xử lý reconnect và backpressure.
Redis
Redis là kho key-value trong RAM với nhiều cấu trúc dữ liệu: dùng làm cache, bộ đếm, leaderboard, hàng đợi, GEO và pub/sub. Bài này đi qua mô hình dữ liệu, persistence, replication, Cluster, Lua và cách tính RAM.
PostgreSQL
PostgreSQL là lựa chọn mặc định an toàn cho phần lớn hệ thống: ACID, index phong phú, replication, partitioning và hệ extension mạnh. Bài này giải thích MVCC, các loại index, mức isolation, replication, connection pooling, bảng sizing theo cấu hình và dấu hiệu nên rời một node Postgres.
Kafka
Kafka là log phân tán bền vững: topic chia partition, consumer đọc theo offset và có thể đọc lại. Bài này đi qua thứ tự theo partition, consumer group, replication với ISR và acks, giới hạn của exactly-once, cách chọn số partition và so sánh với SQS, Pub/Sub, RabbitMQ.
Sharding
Chia dữ liệu theo chiều dọc và chiều ngang, cách chọn shard key và tránh hotspot, so sánh range, hash và directory, resharding, query và transaction xuyên shard, khi nào chưa nên shard và một Postgres chịu được tới đâu.

Sách trong thư viện của bạn. Số chương có thể lệch nhẹ so với bản PDF.