Harbor

Đặt vé sự kiện

Trung bìnhTranh chấp ghếPhòng chờ ảoĐột biến tảiReal-timeGiống Ticketmaster, Ticketbox

Hiểu đề

Đặt vé sự kiện là gì? Nền tảng bán vé concert, sân khấu, thể thao theo ghế ngồi. Khán giả tìm sự kiện, chọn ghế trên sơ đồ, giữ ghế vài phút và thanh toán; ví dụ một đêm nhạc ở sân Mỹ Đình với 40.000 ghế mở bán cùng lúc.

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

  1. Người dùng có thể tìm và xem sự kiện (tên, nghệ sĩ, địa điểm, ngày)
  2. Người dùng có thể xem sơ đồ ghế và trạng thái ghế gần như real-time
  3. Người dùng có thể giữ tối đa 4 ghế trong 10 phút để thanh toán
  4. Người dùng có thể thanh toán và nhận vé điện tử (QR) qua email/app

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

  • Không bán một ghế cho 2 người: 0 vé trùng, tính nhất quán mạnh ở bước giữ ghế
  • Chịu được đột biến ×100 lúc mở bán, hệ thống không sập khi 2 triệu người chờ
  • Giữ ghế p95 < 300 ms với người đã được vào trang chọn ghế
  • Trạng thái ghế trên sơ đồ trễ tối đa 2-3 s
  • Tìm kiếm sự kiện p95 < 200 ms, độ sẵn sàng 99,9%

Ngoài phạm vi: Bán lại vé / chợ vé thứ cấp; Công cụ cho ban tổ chức thiết kế sơ đồ ghế; Giá động theo nhu cầu; Chống phe vé bằng xác minh danh tính (chỉ nhắc ở mức giới hạn số vé).

Thực thể và API

Event
Sự kiện: tên, nghệ sĩ, venue, thời gian, giờ mở bán, trạng thái.
Venue
Địa điểm và sơ đồ khu/hàng/ghế (ví dụ SVĐ Mỹ Đình, 40.000 ghế).
Seat (EventSeat)
Một ghế của một sự kiện: khu, hàng, số, giá, status (AVAILABLE/HELD/SOLD), held_by, hold_expires_at.
Booking
Đơn giữ/mua của một người: danh sách ghế, tổng tiền, trạng thái PENDING/PAID/EXPIRED, idempotency key.
Ticket
Vé điện tử phát hành sau khi thanh toán xong, có mã QR ký số.
QueueToken
Vé vào phòng chờ: số thứ tự, event_id, thời điểm được cho vào, chữ ký.
MethodEndpointGhi chú
GET/v1/events?q=&city=&date=Tìm sự kiện. Kết quả cache ngắn ở CDN.
GET/v1/events/{id}/seats?since={version}Sơ đồ ghế. Có since thì chỉ trả các ghế đổi trạng thái.
POST/v1/events/{id}/queueVào phòng chờ, nhận QueueToken. Trạng thái vị trí đọc qua CDN.
POST/v1/bookingsGiữ ghế: body {eventId, seatIds[]}, header Idempotency-Key và token đã được cho vào. Trả 409 nếu ghế đã bị giữ.
POST/v1/bookings/{id}/payTạo phiên thanh toán ở cổng (VNPay, MoMo...), trả URL chuyển hướng.
POST/v1/payments/webhookCổng thanh toán gọi về. Xử lý idempotent theo mã giao dịch.

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 dùng có thể tìm và xem sự kiện

Trang sự kiện gần như tĩnh và được đọc rất nhiều, nhất là ngay trước giờ mở bán. Render sẵn và đẩy lên CDN, API chỉ trả dữ liệu động như số ghế còn lại.

  1. 1Client gọi GET /v1/events?q=... qua CDN
  2. 2CDN trả cache nếu còn hạn (TTL 30-60 s), hết hạn thì gọi Event API
  3. 3Event API truy vấn Postgres (full-text) hoặc search index, trả danh sách
  4. 4Trang chi tiết sự kiện là HTML/JSON tĩnh, cập nhật khi ban tổ chức sửa

Nguồn: Grokking Modern System Design · ch.11 Content Delivery Network (CDN) ↗ · Content Consistency in CDN · mục "Time-to-live (TTL)" ; Grokking Modern System Design · ch.11 CDN ↗ · mục "Pull CDN" ; Seven Databases in Seven Weeks ↗ (.zip) · ch.2 PostgreSQL · §2.4 Day 3: Full-Text and Multidimensions (tr. 35) ; Grokking Modern System Design · ch.21 Distributed Search ↗ · mục "Inverted index" (Build a searchable index)

2) Người dùng có thể xem sơ đồ ghế gần real-time

Sơ đồ đầy đủ (vị trí ghế, giá) gần như không đổi nên cache ở CDN. Chỉ trạng thái ghế thay đổi, và gửi phần thay đổi đó qua SSE hoặc polling theo version.

  1. 1Client tải layout ghế tĩnh từ CDN
  2. 2Client gọi GET /seats để lấy trạng thái hiện tại kèm version
  3. 3Client mở kết nối SSE, nhận các sự kiện seat_held / seat_released / seat_sold
  4. 4Mất kết nối thì gọi lại /seats?since={version} để bù phần thiếu

Nguồn: Grokking Modern System Design · ch.11 Content Delivery Network (CDN) ↗ · Content Consistency in CDN · mục "Time-to-live (TTL)" ; Grokking Modern System Design · ch.18 Pub-sub ↗ · mục "High-level design" ; Grokking Modern System Design · ch.36 WhatsApp ↗ · mục "Connection with a WebSocket server" ; Chưa có nguồn trong thư viện: SSE, CDN TTL của file trạng thái và hàng đợi ảo không có sách trong thư viện

3) Người dùng có thể giữ ghế trong 10 phút

Bước giữ ghế là chỗ tranh chấp duy nhất cần nhất quán mạnh. Dùng một câu UPDATE có điều kiện trên Postgres để đổi AVAILABLE sang HELD kèm hạn giữ, một ghế chỉ một người thắng.

  1. 1Client gửi POST /v1/bookings với seatIds và Idempotency-Key
  2. 2Booking API kiểm tra token phòng chờ và giới hạn 4 ghế/người
  3. 3UPDATE event_seats SET status='HELD', held_by=?, hold_expires_at=now()+10 phút WHERE id IN (...) AND (status='AVAILABLE' OR (status='HELD' AND hold_expires_at < now()))
  4. 4Số hàng cập nhật khác số ghế yêu cầu thì rollback, trả 409
  5. 5Thành công thì tạo Booking PENDING và phát sự kiện seat_held

Nguồn: Microservices Patterns ↗ (.zip) · ch.4 · §4.3 Handling the lack of isolation · "Countermeasures for handling the lack of isolation" (semantic lock, tr. 128) ; Redis in Action ↗ · ch.4 · §4.4 Redis transactions (tr. 78) ; Microservices Patterns ↗ (.zip) · ch.3 · §3.3.6 Handling duplicate messages (tr. 95) ; Chưa có nguồn trong thư viện: câu UPDATE có điều kiện với hold_expires_at là thiết kế của khoá học

4) Người dùng có thể thanh toán và nhận vé

Thanh toán đi qua cổng bên ngoài nên chậm và có thể gọi webhook nhiều lần. Webhook xác nhận đơn trong một transaction, kèm bản ghi outbox để worker phát hành vé và gửi email.

  1. 1Client gọi /pay, nhận URL cổng thanh toán và chuyển hướng
  2. 2Cổng gọi POST /payments/webhook với mã giao dịch
  3. 3Trong 1 transaction: kiểm tra ghế vẫn HELD bởi đúng người, đổi sang SOLD, Booking sang PAID, ghi outbox
  4. 4Worker đọc outbox, sinh vé QR, gửi email/app
  5. 5Nếu hold đã hết hạn và ghế đã bán cho người khác thì hoàn tiền tự động

Nguồn: Microservices Patterns ↗ (.zip) · ch.3 · §3.3.7 Transactional messaging (tr. 97–100) ; Microservices Patterns ↗ (.zip) · ch.3 · §3.3.6 Handling duplicate messages (tr. 95) ; Microservices Patterns ↗ (.zip) · ch.4 · §4.2 Coordinating sagas (tr. 117–126) ; Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · mục "Motivation" (Improved performance, Granular scalability)

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, sự kiện ≤ 2.000 ghế
Tải đỉnh
≈ 500 req/s trong phút mở bán
Chi phí ước lượng
≈ 50–150 USD/tháng (ước lượng)

Trang bán vé của một ban tổ chức nhỏ. Hai instance Node sau CDN và một Postgres managed là đủ; bước giữ ghế dùng UPDATE có điều kiện ngay trên Postgres.

Người dùngNgười dùngCDNCDNDịch vụAPI Node.jsWorkerCron dọn holdDatabasePostgreSQLBên ngoàiCổng thanh toánBên ngoàiEmail (SES)HTTPScache missUPDATE điều kiệntạo thanh toángửi vénhả ghế hết hạn

Ước lượng

  • Bình thường: 10.000 DAU × 15 req / 86.400 ≈ 1,7 req/s
  • Mở bán show 2.000 ghế: 5.000 người × 6 req trong 60 s = 30.000 / 60 = 500 req/s
  • API: ceil(500 / (1.000 × 0,6)) = 1 → lấy tối thiểu 2 instance để dự phòng
  • Giữ ghế: 2.000 vé bán trong 10 phút ≈ 2.000 / 600 ≈ 3,3 vé/s, tính cả lần thử thất bại ×5 ≈ 17 ghi/s
  • Lưu trữ: 50 sự kiện/năm × 2.000 vé × 1 KB = 100.000 KB ≈ 100 MB/năm

Nguồn con số: Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · mục "Examples of Resource Estimation" (Number of servers required, Storage requirements, Bandwidth requirements) ; Quy tắc ước lượng của khoá học · /hoc/chon-server (1.000 req/s mỗi máy 2 vCPU, chạy tối đa 60% CPU, Redis ×1,3) ; Chưa có nguồn trong thư viện: hệ số đột biến mở bán (×100), 20.000 kết nối SSE mỗi instance là giả định của khoá học, cần load test

Bảng hạ tầng

Thành phầnChọnSố lượngCấu hìnhVì sao
CDNCloudflare (gói free/pro)1Cache trang sự kiện, layout ghế, JS/CSSĐỡ phần lớn lượt xem trước giờ mở bán, gần như miễn phí.
APINode.js (NestJS), Docker trên Cloud Run22 vCPU · 4 GB RAM500 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 SSD17 ghi/s và dữ liệu 100 MB/năm là rất nhỏ; UPDATE có điều kiện đảm bảo không bán trùng.
Dọn ghế hết hạnCron job (Cloud Scheduler) mỗi phút1Chạy trong chính APIĐổi HELD hết hạn về AVAILABLE cho sơ đồ gọn; query đã tự coi hold hết hạn là trống.
Thanh toánVNPay / MoMo1Dịch vụ bên ngoàiKhông tự xử lý thẻ, chỉ nhận webhook.
Email véSendGrid / Amazon SES1Dịch vụ bên ngoàiVài nghìn email mỗi sự kiện, gửi trực tiếp 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 phía trước
  • Giữ ghế bằng status + hold_expires_at, chưa cần Redis
  • Sơ đồ ghế cập nhật bằng polling 5 s là đủ cho 2.000 ghế
Nút thắt tiếp theo

Khi một show hot kéo 100.000+ người vào cùng phút, API và Postgres nhận toàn bộ tải trực tiếp và sập. Cần phòng chờ để điều tiết lượng người vào luồng mua.

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, sự kiện ≤ 2.000 ghế1 triệu DAU, show lớn 20.000 ghế5 triệu DAU; 2 triệu người cùng chờ một show 40.000 ghế
Chi phí≈ 50–150 USD/tháng (ước lượng)≈ 2.000–5.000 USD/tháng (ước lượng, tăng trong ngày mở bán)≈ 15.000–40.000 USD/tháng (ước lượng, chưa tính chi phí scale trong ngày mở bán)
CDN
Cloudflare (gói free/pro)
1 × Cache trang sự kiện, layout ghế, JS/CSS
Cloudflare / CloudFront
1 × Cache trang sự kiện, layout ghế, /seats 1 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 · 200 GB SSD
PostgreSQL HA (Aurora / Cloud SQL), multi-AZ
1 primary + 2 replica × 8 vCPU · 32 GB · 500 GB SSD
Dọn ghế hết hạn
Cron job (Cloud Scheduler) mỗi phút
1 × Chạy trong chính API
Chưa cầnChưa cần
Thanh toán
VNPay / MoMo
1 × Dịch vụ bên ngoài
Chưa cầnChưa cần
Email vé
SendGrid / Amazon SES
1 × Dịch vụ bên ngoài
Chưa cầnChưa cần
Phòng chờChưa cần
Node.js + Redis sorted set
2–12 (autoscale) × 2 vCPU · 4 GB RAM
Chưa cần
Booking APIChưa cần
Node.js (NestJS), container autoscale
2–9 (autoscale) × 2 vCPU · 4 GB RAM
Node.js trên Kubernetes, 3 AZ
17 pod (HPA) × 2 vCPU · 4 GB mỗi pod
SSE gatewayChưa cần
Node.js SSE + Redis pub/sub
2 × 2 vCPU · 4 GB RAM
Node.js SSE, 3 AZ
3 pod × 2 vCPU · 4 GB mỗi pod
RedisChưa cần
ElastiCache / Memorystore
1 primary + 1 replica × 2 vCPU · 4 GB
Chưa cần
QueueChưa cần
Amazon SQS / Google Pub/Sub
1 × Managed, có dead-letter queue
Chưa cần
WorkerChưa cần
Node.js consumer
2 × 1 vCPU · 2 GB RAM
Node.js consumer trên Kubernetes
6 pod × 1 vCPU · 2 GB mỗi pod
CDN + WAFChưa cầnChưa cần
Cloudflare / CloudFront + bot management
1 × Cache trang, layout ghế, file vị trí hàng đợi
Queue serviceChưa cầnChưa cần
Node.js trên Kubernetes
14 pod (HPA) × 2 vCPU · 4 GB mỗi pod
Redis ClusterChưa cầnChưa cần
ElastiCache Redis Cluster
3 shard × (1 primary + 1 replica) × 2 vCPU · 8 GB mỗi node
KafkaChưa cầnChưa cần
MSK / Confluent
3 broker × Topic bookings 6 partition, RF 3
SearchChưa cầnChưa cần
OpenSearch
3 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) Giữ ghế trong lúc khách đang thanh toán (5-10 phút) thế nào để không bán trùng?

Cách chưa tốt
SELECT ... FOR UPDATE và giữ transaction đến khi trả tiền xong

Khóa hàng ghế trong DB rồi mở transaction suốt thời gian khách ở trang cổng thanh toán. Đúng về mặt nhất quán nhưng mỗi khách chiếm một connection và một khóa trong nhiều phút.

Đánh đổi: Vài trăm người cùng thanh toán là cạn connection pool, khách bỏ ngang thì khóa treo đến khi timeout.

Nguồn: Redis in Action ↗ · ch.6 · §6.2 Distributed locking (khóa giữ lâu làm tắc nghẽn các client khác, tr. 116) ; Microservices Patterns ↗ (.zip) · ch.4 · §4.3 Handling the lack of isolation · "Countermeasures for handling the lack of isolation" (semantic lock, tr. 128)

Cách tốt
Khóa Redis có TTL: SET seat:{id} {userId} NX PX 600000

Ai SET NX thành công thì giữ ghế, TTL 10 phút tự nhả khi khách bỏ đi. Nhanh, không chiếm connection DB, đây là mẫu khóa có timeout trong Redis in Action §6.2.5.

Đánh đổi: Redis failover có thể mất key vừa ghi nên hai người cùng giữ một ghế, và trạng thái ghế nằm ở hai nơi (Redis và DB) dễ lệch nhau.

Nguồn: Redis in Action ↗ · ch.6 · §6.2 Distributed locking · §6.2.5 Locks with timeouts (tr. 126) ; Grokking Modern System Design · ch.9 Databases ↗ · Data Replication · mục "Single leader/primary-secondary replication" và "Synchronous versus asynchronous replication" (failover bất đồng bộ có thể mất bản ghi cuối)

Cách tốt nhất
Trạng thái HELD + hold_expires_at trong DB, UPDATE có điều kiện, Redis chỉ làm lớp lọc

Nguồn sự thật là cột status và hold_expires_at trên hàng ghế. Một câu UPDATE ... WHERE status='AVAILABLE' OR hold đã hết hạn là nguyên tử, khóa hàng chỉ vài ms, ghế hết hạn được coi là trống ngay khi đọc nên không phụ thuộc job dọn. Đây là semantic lock (Microservices Patterns §4.3). Khi tranh chấp rất cao thì thêm SET NX trên Redis phía trước để loại bớt request thua cuộc trước khi chạm DB.

Đánh đổi: Phải kiểm tra lại hạn giữ ở bước xác nhận thanh toán và có luồng hoàn tiền cho trường hợp tiền về sau khi hold đã hết.

Nguồn: Microservices Patterns ↗ (.zip) · ch.4 · §4.3 Handling the lack of isolation · "Countermeasures for handling the lack of isolation" (semantic lock, tr. 128) ; Microservices Patterns ↗ (.zip) · ch.4 · §4.3 (tr. 126–132) ; Redis in Action ↗ · ch.6 · §6.2 Distributed locking · §6.2.5 Locks with timeouts (tr. 126) ; Chưa có nguồn trong thư viện: UPDATE ... WHERE status='AVAILABLE' OR hold hết hạn là thiết kế của khoá học

2) Lúc mở bán, 2 triệu người bấm F5 cùng lúc. Làm sao hệ thống sống sót?

Cách chưa tốt
Chỉ dựa vào autoscaling

Cho API tự scale theo CPU. Đột biến xảy ra trong vài giây còn instance mới cần 1-3 phút để lên, và DB thì không scale ngang kịp.

Đánh đổi: Hệ thống sập đúng lúc quan trọng nhất, người dùng bấm thử lại khiến tải còn tăng thêm.

Nguồn: Grokking Modern System Design · ch.4 Non-functional System Characteristics ↗ · Scalability · mục "Vertical scalability--scaling up" và "Horizontal scalability--scaling out" ; Quy tắc ước lượng của khoá học · /hoc/chon-server (1.000 req/s mỗi máy 2 vCPU, chạy tối đa 60% CPU, Redis ×1,3) (autoscaling cần 1-3 phút để bật máy mới)

Cách tốt
CDN cho trang sự kiện + rate limit + scale trước giờ mở bán

Trang sự kiện, sơ đồ ghế, JS/CSS nằm hết trên CDN. Rate limit theo IP và user (token bucket, Grokking ch.19) ở gateway để chặn bot. Scale sẵn số instance 30 phút trước giờ G vì đã biết lịch.

Đánh đổi: Rate limit chỉ cắt bớt tải, người thật vẫn nhận lỗi 429 ngẫu nhiên và trải nghiệm không công bằng.

Nguồn: Grokking Modern System Design · ch.19 Rate Limiter ↗ · mục "Algorithms for rate limiting" (Token bucket algorithm) ; Grokking Modern System Design · ch.19 Rate Limiter ↗ · mục "Where to place the rate limiter" ; Grokking Modern System Design · ch.11 Content Delivery Network (CDN) ↗ · Content Consistency in CDN · mục "Time-to-live (TTL)"

Cách tốt nhất
Phòng chờ ảo (virtual waiting room) trước luồng mua

Mọi người vào phòng chờ, nhận QueueToken có số thứ tự (ai vào trước giờ mở bán thì được xáo trộn ngẫu nhiên cho công bằng). Hệ thống cho vào theo tốc độ cố định, ví dụ 2.000 người/phút, tính từ năng lực của Booking API. Vị trí hiện tại được công bố thành một file JSON nhỏ trên CDN, client đọc file đó nên origin gần như không chịu tải. Khi ghế đã hết thì báo cho cả hàng đợi ngay.

Đánh đổi: Thêm một thành phần phải vận hành và phải ký token chống làm giả; người ở cuối hàng đợi chờ lâu nhưng biết rõ vị trí.

Nguồn: Chưa có nguồn trong thư viện: SSE, CDN TTL của file trạng thái và hàng đợi ảo không có sách trong thư viện ; Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · mục "Motivation" (Improved performance, Granular scalability) · mục "Rate limiting" (queue hấp thụ đột biến tải)

3) Sơ đồ ghế cập nhật real-time cho hàng chục nghìn người xem thế nào?

Cách chưa tốt
Client poll toàn bộ sơ đồ mỗi giây

40.000 ghế × khoảng 30 B là 1,2 MB mỗi lần, nhân với hàng chục nghìn người mỗi giây. Băng thông và DB đều không chịu nổi.

Đánh đổi: Đơn giản nhất nhưng tốn kém nhất, chỉ ổn cho sự kiện vài trăm ghế.

Nguồn: Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · mục "Bandwidth requirements" (cách tính băng thông) ; Chưa có nguồn trong thư viện: 30 B mỗi ghế là giả định của khoá học

Cách tốt
Polling theo version, chỉ trả phần thay đổi

Mỗi lần ghế đổi trạng thái thì tăng version của sự kiện. Client gọi /seats?since=v mỗi 3 s và chỉ nhận vài chục ghế vừa đổi. Response cache 1 s ở CDN nên nhiều người dùng chung một kết quả.

Đánh đổi: Trễ 1-3 s và vẫn có request rỗng khi không có gì đổi.

Nguồn: Grokking Modern System Design · ch.11 Content Delivery Network (CDN) ↗ · Content Consistency in CDN · mục "Time-to-live (TTL)" ; Chưa có nguồn trong thư viện: polling theo version là thiết kế của khoá học

Cách tốt nhất
SSE đẩy thay đổi qua Redis pub/sub, polling làm dự phòng

Booking API publish seat_held/seat_sold lên kênh của sự kiện (pub/sub, Grokking ch.18). SSE gateway subscribe và đẩy xuống client. SSE là một chiều, chạy trên HTTP thường nên đi qua proxy dễ hơn WebSocket. Client mất kết nối thì gọi lại /seats?since=v.

Đánh đổi: Phải giữ hàng chục nghìn kết nối mở; nếu gateway quá tải thì giảm cấp về polling.

Nguồn: Grokking Modern System Design · ch.18 Pub-sub ↗ · mục "High-level design" ; Grokking Modern System Design · ch.36 WhatsApp ↗ · mục "Connection with a WebSocket server" ; Chưa có nguồn trong thư viện: SSE (Server-Sent Events) không có sách trong thư viện

4) Tìm kiếm sự kiện theo tên, nghệ sĩ, thành phố, ngày nên làm thế nào?

Cách chưa tốt
WHERE name LIKE '%keyword%'

Không dùng được index nên quét cả bảng, không xử lý được tiếng Việt có dấu/không dấu (gõ 'son tung' vẫn phải ra 'Sơn Tùng').

Đánh đổi: Chạy được với vài nghìn sự kiện, chậm dần và kết quả kém.

Nguồn: Seven Databases in Seven Weeks ↗ (.zip) · ch.2 PostgreSQL · §2.4 Day 3: Full-Text and Multidimensions (tr. 35) (LIKE không tận dụng được chỉ mục toàn văn) ; Grokking Modern System Design · ch.9 Databases (mục "Types of NoSQL databases" đối chiếu) ↗ ; Chưa có nguồn trong thư viện: hành vi LIKE '%x%' quét toàn bảng là kiến thức chung

Cách tốt
Postgres full-text + unaccent + pg_trgm, index B-tree cho city/date

Tạo cột tsvector đã bỏ dấu, index GIN; pg_trgm cho gõ sai chính tả. Lọc city, date bằng index thường. Số sự kiện đang mở bán chỉ vài chục nghìn nên đủ nhanh.

Đánh đổi: Không cần thêm hệ thống; facet, xếp hạng theo độ phổ biến và gợi ý khi gõ thì làm khó.

Nguồn: Seven Databases in Seven Weeks ↗ (.zip) · ch.2 PostgreSQL · §2.4 Day 3: Full-Text and Multidimensions (tr. 35) ; Chưa có nguồn trong thư viện: pg_trgm, unaccent, GIN index là tiện ích riêng của PostgreSQL

Cách tốt nhất
Elasticsearch/OpenSearch đồng bộ qua outbox hoặc CDC

Index sự kiện với analyzer asciifolding cho tiếng Việt, facet theo thành phố/thể loại/giá, geo theo khoảng cách, boost theo lượt xem. Thay đổi từ Postgres đi qua outbox/Kafka vào index.

Đánh đổi: Dữ liệu tìm kiếm trễ vài giây và thêm một cụm phải vận hành; chỉ đáng khi lượng tìm kiếm và yêu cầu lọc đủ lớn.

Nguồn: Grokking Modern System Design · ch.21 Distributed Search ↗ · mục "Inverted index" (Build a searchable index) ; Grokking Modern System Design · ch.21 Distributed Search ↗ · mục "Indexing" ; Microservices Patterns ↗ (.zip) · ch.3 · §3.3.7 Transactional messaging (tr. 97–100) ; Chưa có nguồn trong thư viện: analyzer asciifolding của Elasticsearch/OpenSearch (thư viện không có sách về Elasticsearch)

Trong phỏng vấn

Nên nói
  • “Chỉ bước giữ ghế cần nhất quán mạnh; xem sự kiện, tìm kiếm, sơ đồ ghế đều chấp nhận trễ vài giây.”
  • “Tôi giữ ghế bằng UPDATE có điều kiện trên status và hold_expires_at, ghế hết hạn tự được coi là trống.”
  • “Tải của ngày mở bán do số người chờ quyết định, nên tôi đặt phòng chờ ảo và cho vào theo tốc độ cố định.”
  • “Webhook thanh toán phải idempotent và kiểm tra lại hạn giữ ghế, có luồng hoàn tiền khi tiền về muộn.”
Bẫy hay gặp
  • Giữ transaction DB mở suốt lúc khách thanh toán.
  • Dùng khóa Redis làm nguồn sự thật duy nhất cho ghế.
  • Tin rằng autoscaling kịp đỡ đột biến xảy ra trong vài giây.
  • Đẩy toàn bộ sơ đồ ghế mỗi giây cho mọi client.

Đọ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.
Elasticsearch
Elasticsearch là search engine phân tán dựa trên inverted index. Bài này giải thích analyzer (kể cả tiếng Việt), shard và replica, near-real-time refresh, cách đồng bộ từ database chính và khi nào Postgres full-text là đủ.
Quy trình nhiều bước
Giữ dữ liệu nhất quán khi một nghiệp vụ đi qua nhiều service: saga choreography và orchestration, hành động bù trừ, trạng thái PENDING, transactional outbox, idempotent consumer và workflow engine như Temporal.

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