Ứng dụng gọi xe
Hiểu đề
Yêu cầu chức năng
- Tài xế bật trạng thái nhận chuyến và app tự gửi vị trí khoảng 4 giây một lần
- Khách xem xe đang trống quanh mình và giá dự kiến trước khi đặt
- Khách đặt xe, hệ thống mời tài xế phù hợp gần nhất, tài xế nhận hoặc từ chối trong vài giây
- Trong chuyến, hai bên thấy vị trí xe và ETA cập nhật liên tục
- Kết thúc chuyến thì tính tiền và thu qua thẻ, ví điện tử hoặc tiền mặt
Yêu cầu phi chức năng
- Tìm tài xế gần p95 < 200 ms
- Vị trí xe trên màn hình khách trễ không quá 5 s
- Ghép xong tài xế trong < 30 s cho 95% yêu cầu ở giờ cao điểm
- 0 trường hợp một tài xế nhận 2 chuyến cùng lúc, 0 lần trừ tiền 2 lần cho một chuyến
- Luồng đặt xe sẵn sàng 99,95% (≈ 22 phút downtime/tháng)
Ngoài phạm vi: Giao đồ ăn, giao hàng, đi chung nhiều khách; Tự xây bản đồ và định tuyến (dùng Maps API có sẵn); Đăng ký tài xế, KYC, chấm điểm tài xế; Phát hiện gian lận chi tiết.
Thực thể và API
| Method | Endpoint | Ghi chú |
|---|---|---|
| WS | /v1/driver/stream | Tài xế gửi vị trí 4 s/lần qua WebSocket và nhận lời mời chuyến trên cùng kết nối. |
| GET | /v1/drivers/nearby?lat=&lng=&type= | Trả tối đa 20 xe trống gần nhất để vẽ lên bản đồ. Toạ độ được làm tròn cho khách. |
| POST | /v1/fare-quotes | Gửi điểm đón, điểm đến, loại xe. Trả quoteId, giá, ETA đón, hệ số surge. |
| POST | /v1/trips | Header Idempotency-Key, body có quoteId. Trả 201 với trip ở trạng thái MATCHING. |
| POST | /v1/offers/{offerId}/accept | Tài xế nhận chuyến. Trả 409 nếu lời mời đã hết hạn hoặc chuyến đã có người nhận. |
| POST | /v1/trips/{tripId}/events | Tài xế báo ARRIVED, PICKED_UP, COMPLETED. Server kiểm tra chuyển trạng thái hợp lệ. |
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) Tài xế gửi vị trí liên tục
Vị trí chỉ cần bản mới nhất để tìm xe, nên giữ trong bộ nhớ thay vì ghi DB mỗi lần. Lịch sử điểm GPS trong chuyến được đẩy sang hàng đợi để lưu sau, không nằm trên đường nóng.
- 1App tài xế gửi {lat, lng, ts} qua WebSocket tới Location gateway
- 2Gateway bỏ điểm cũ hơn điểm đã có (ts nhỏ hơn) rồi cập nhật Redis GEO, đặt TTL 30 s cho trạng thái online
- 3Nếu tài xế đang chạy chuyến, gateway đẩy điểm vào hàng đợi; worker gom theo chuyến và ghi xuống kho lưu trữ
- 4Trip service phát vị trí mới cho app khách đang theo dõi chuyến đó
Nguồn: Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Design" (tài xế báo vị trí mỗi 4 s, bảng băm giữ vị trí mới nhất, QuadTree cập nhật thưa hơn) ; Chưa có nguồn trong thư viện: Redis GEO/GEOSEARCH và TTL 30 s trạng thái online không có trong sách
2) Khách xem xe gần và giá dự kiến
Truy vấn bán kính trên geo index trả danh sách tài xế trống. Giá được tính một lần và chốt thành quote có hạn để khách không bị đổi giá giữa chừng.
- 1App khách gọi GET /drivers/nearby với vị trí hiện tại
- 2Ride API chạy GEOSEARCH bán kính 2 km, lọc theo loại xe và trạng thái AVAILABLE
- 3Khách nhập điểm đến, POST /fare-quotes lấy ETA từ Maps API và hệ số surge của ô bản đồ
- 4Server lưu FareQuote với hạn 5 phút và trả quoteId
Nguồn: Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Workflow of our application" (tìm tài xế gần trong vùng) ; Grokking Modern System Design · ch.29 Design Yelp ↗ · mục "QuadTree" (tìm theo bán kính) ; Chưa có nguồn trong thư viện: Redis GEOSEARCH và Maps API
3) Đặt xe và ghép tài xế
Matching service mời từng tài xế một, theo thứ tự gần và phù hợp. Mỗi tài xế bị khoá trong lúc đang được mời để không ai khác mời trùng.
- 1Khách POST /trips kèm quoteId và Idempotency-Key, trip được tạo ở trạng thái MATCHING
- 2Matching lấy 10 tài xế gần nhất, thử khoá từng người bằng SET NX PX 15000
- 3Khoá được ai thì gửi lời mời cho người đó qua WebSocket, chờ tối đa 10 s
- 4Tài xế nhận: cập nhật trip sang ACCEPTED có điều kiện; từ chối hoặc hết giờ: nhả khoá, mời người tiếp theo
Nguồn: Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Workflow of our application" (matching chọn tài xế, gửi lời mời, tài xế nhận) ; Redis in Action ↗ · ch.6 · §6.2.5 Locks with timeouts, §6.2.3 Building a lock in Redis (SETNX) ; Microservices Patterns ↗ (.zip) · ch.3 · §3.3.6 Handling duplicate messages
4) Theo dõi chuyến và thanh toán
Trip service giữ máy trạng thái của chuyến và chỉ cho phép chuyển trạng thái hợp lệ. Thanh toán gắn idempotency key theo chuyến nên gọi lại bao nhiêu lần cũng chỉ thu một lần.
- 1Tài xế báo ARRIVED, PICKED_UP, COMPLETED; mỗi lần là một UPDATE có điều kiện trên trạng thái cũ
- 2Khi COMPLETED, Trip service ghi trip và một bản ghi outbox trong cùng transaction
- 3Payment service đọc sự kiện, gọi cổng thanh toán với key payment:{tripId}
- 4Kết quả từ cổng (kể cả webhook gửi lại) được chống trùng theo key rồi mới cập nhật Payment
Nguồn: Microservices Patterns ↗ (.zip) · ch.3 · §3.3.7 Transactional messaging (tr. 97–100) (ghi DB và phát sự kiện cùng transaction) ; Microservices Patterns ↗ (.zip) · ch.3 · §3.3.6 Handling duplicate messages ; Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Payment Service and Fraud Detection" ; Building Microservices ↗ (.zip) · ch.11 Microservices at Scale · mục "Idempotency" (tr. 215)
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.
Một hãng taxi ở Hà Nội ra app riêng. Một API NestJS vừa phục vụ REST vừa giữ WebSocket, Redis GEO cho vị trí và khoá, Postgres cho chuyến. Hai instance là đủ, chạy 2 chỉ để dự phòng.
Ước lượng
- Vị trí: 1.000 tài xế online × 1 lần / 4 s = 250 cập nhật/s (đã là số online lúc đỉnh, không nhân thêm)
- Request khác: 10.000 khách × 30 req / 86.400 ≈ 3,5 req/s, đỉnh ×10 ≈ 35 req/s
- Chuyến: 8.000 chuyến/ngày / 86.400 ≈ 0,09 chuyến/s, giờ tan tầm ×10 ≈ 1 chuyến/s
- API: (250 + 35 + 1) / (1.000 × 0,6) ≈ 0,48 → 1 instance, chạy 2 để dự phòng
- Redis: 1.000 tài xế × (20 + 60 + 100) B × 1,3 ≈ 234 KB
- Lộ trình GPS: 8.000 chuyến × 300 điểm (20 phút, 4 s/điểm) × 40 B ≈ 96 MB/ngày ≈ 35 GB/năm
Nguồn con số: Grokking Modern System Design · ch.30 Design Uber ↗ · bài "Requirements of Uber's Design" · mục "Resource estimation" (vị trí tài xế cập nhật 4 s, số tài xế, số chuyến) ; Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · mục "Requests estimation" ; Quy tắc ước lượng của khoá học · /hoc/chon-server (600 req/s mỗi instance, 60% CPU, Redis ×1,3, 100.000 ops/s mỗi node là giả định của khoá học)
Bảng hạ tầng
| Thành phần | Chọn | Số lượng | Cấu hình | Vì sao |
|---|---|---|---|---|
| Load balancer | Cloud Load Balancing (hỗ trợ WebSocket) | 1 | managed | Chia tải cho 2 instance và giữ kết nối WebSocket lâu. |
| API + WebSocket | NestJS trong Docker, chạy VM hoặc Cloud Run | 2 | 2 vCPU · 4 GB RAM | 286 req/s đỉnh chỉ cần 1 instance; 1.000 kết nối WebSocket rất nhẹ. |
| Geo index + khoá | Redis (Memorystore) với GEOADD / GEOSEARCH, SET NX PX | 1 | 1 GB, bản Standard có replica | Dữ liệu chưa tới 1 MB; 250 ghi/s không đáng kể với Redis. |
| Database | PostgreSQL managed (Cloud SQL), bảng điểm GPS chia partition theo tháng | 1 + standby | 2 vCPU · 8 GB RAM · 100 GB SSD | Chuyến và thanh toán cần transaction; 35 GB/năm lộ trình vẫn vừa một node. |
| Bản đồ, ETA | Maps API (Google Maps hoặc Goong) | - | trả theo lượt gọi | Tự làm routing không đáng ở quy mô này. |
| Thanh toán | Cổng thanh toán (MoMo, ZaloPay, VNPay, thẻ) + tiền mặt | - | - | Gọi qua một Payment module có idempotency key. |
| Thông báo | FCM / APNs | - | - | Đánh thức app tài xế khi WebSocket bị hệ điều hành ngắt. |
- Điểm xuất phát: một service, một Redis, một Postgres
- Vị trí đã nằm trong Redis ngay từ đầu vì rẻ và tránh ghi DB 250 lần/s
- Lộ trình GPS ghi theo lô vào Postgres, chưa cần hàng đợi
Khi mở rộng ra cả Hà Nội và TP.HCM, cập nhật vị trí tăng lên hàng chục nghìn/s và chiếm hết CPU của API, làm chậm luồng đặt xe. Bảng điểm GPS trong Postgres cũng phình lên vài TB mỗi năm.
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ần | Mức 1 | Mức 2 | Mức 3 |
|---|---|---|---|
| Người dùng | ≈ 10.000 khách/ngày · 1.000 tài xế online đỉnh | ≈ 1 triệu khách/ngày · 60.000 tài xế online đỉnh | ≈ 20 triệu khách/ngày · 1 triệu tài xế online đỉnh |
| Chi phí | ≈ 150–300 USD/tháng (chưa tính phí Maps API) | ≈ 4.000–8.000 USD/tháng (ước lượng, chưa tính phí Maps API) | ≈ 100.000–200.000 USD/tháng (ước lượng, chưa tính Maps và phí cổng thanh toán) |
| Load balancer | Cloud Load Balancing (hỗ trợ WebSocket) 1 × managed | Cloud Load Balancing, tách route /driver/stream và /v1/* 1 × managed | Chưa cần |
| API + WebSocket | NestJS trong Docker, chạy VM hoặc Cloud Run 2 × 2 vCPU · 4 GB RAM | Chưa cần | Chưa cần |
| Geo index + khoá | Redis (Memorystore) với GEOADD / GEOSEARCH, SET NX PX 1 × 1 GB, bản Standard có replica | Chưa cần | Chưa cần |
| Database | PostgreSQL managed (Cloud SQL), bảng điểm GPS chia partition theo tháng 1 + standby × 2 vCPU · 8 GB RAM · 100 GB SSD | PostgreSQL (Cloud SQL HA), bảng trips partition theo tháng 1 primary + 1 replica × 8 vCPU · 32 GB RAM · 1 TB SSD | Chưa cần |
| Bản đồ, ETA | Maps API (Google Maps hoặc Goong) - × trả theo lượt gọi | Chưa cần | Chưa cần |
| Thanh toán | Cổng thanh toán (MoMo, ZaloPay, VNPay, thẻ) + tiền mặt - × - | Chưa cần | Chưa cần |
| Thông báo | FCM / APNs - × - | Chưa cần | Chưa cần |
| Location gateway | Chưa cần | NestJS (ws) trên GKE, autoscale theo CPU 25 × 2 vCPU · 4 GB RAM | Pod trên Kubernetes (GKE), HPA theo số message 417 pod × 2 vCPU · 4 GB RAM |
| Ride API | Chưa cần | NestJS, stateless, autoscale 3–10 3 × 2 vCPU · 4 GB RAM | Chưa cần |
| Matching | Chưa cần | Service riêng, đọc Redis GEO, khoá tài xế bằng SET NX PX 3 × 2 vCPU · 4 GB RAM | Service partition theo ô H3, mỗi ô do một consumer xử lý 20 pod × 2 vCPU · 4 GB RAM |
| Trip + Payment | Chưa cần | NestJS, máy trạng thái chuyến, outbox, gọi cổng thanh toán 2 × 2 vCPU · 4 GB RAM | Chưa cần |
| Redis | Chưa cần | Memorystore, một instance cho mỗi thành phố (HN, HCM) 2 primary + 2 replica × 5 GB mỗi instance | Chưa cần |
| Hàng đợi | Chưa cần | GCP Pub/Sub, topic location-points và trip-events - × managed | Chưa cần |
| Lưu lộ trình | Chưa cần | Worker gom điểm theo chuyến, ghi file lên GCS 2 worker × 2 vCPU · 4 GB RAM | Chưa cần |
| Edge | Chưa cần | Chưa cần | Global LB, định tuyến theo quốc gia về region gần nhất (Singapore, Jakarta) 2 region × managed |
| Ride + Trip API | Chưa cần | Chưa cần | NestJS trên GKE, stateless 58 pod × 2 vCPU · 4 GB RAM |
| Geo index | Chưa cần | Chưa cần | Redis Cluster, key theo ô H3 độ phân giải 9 9 primary + 9 replica × 4 vCPU · 8 GB RAM |
| Database chuyến | Chưa cần | Chưa cần | PostgreSQL shard theo city_id (Citus) hoặc Cloud Spanner 6 shard × (1 primary + 1 replica) × 8 vCPU · 32 GB RAM |
| Event bus | Chưa cần | Chưa cần | Kafka, topic location, trip-events, payments; key theo ô hoặc tripId 6 broker × 8 vCPU · 32 GB RAM · NVMe |
| Surge + đặc trưng ETA | Chưa cần | Chưa cần | Flink đọc Kafka, đếm cung cầu theo ô H3 mỗi phút, ghi hệ số vào Redis 8 TaskManager × 4 vCPU · 16 GB RAM |
| Lưu trữ lịch sử | Chưa cần | Chưa cần | Cassandra cho chuyến đã xong + GCS/data lake cho lộ trình GPS 6 node Cassandra × 8 vCPU · 32 GB · 4 TB |
Đà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) Tài xế gửi vị trí 4 giây một lần. Lưu và đánh chỉ mục vị trí thế nào để vừa chịu ghi vừa tìm xe nhanh?
Mỗi UPDATE sinh một bản ghi mới trong Postgres (MVCC), 15.000 lần/s tạo rất nhiều dead tuple và WAL. Index B-tree trên lat, lng riêng rẽ chỉ lọc được một chiều, chiều còn lại phải quét.
Đánh đổi: Đơn giản nhưng DB chính bị vắt kiệt bởi dữ liệu vốn chỉ cần sống vài giây.
Nguồn: Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Design" (vì sao không cập nhật QuadTree/DB mỗi 4 s: tốn thời gian sửa cấu trúc) ; Chưa có nguồn trong thư viện: cơ chế MVCC, dead tuple và WAL của Postgres không có trong sách
PostGIS có kiểu geography và ST_DWithin, index GiST tìm bán kính đúng nghĩa. Ở mức vài trăm cập nhật/s như mức 1 thì chạy tốt và chỉ cần một DB.
Đánh đổi: Tốc độ ghi vẫn giới hạn bởi một primary; lên hàng chục nghìn cập nhật/s thì vacuum và WAL thành vấn đề.
Nguồn: Chưa có nguồn trong thư viện: PostGIS và index GiST không có trong sách ; Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Design" (giải pháp QuadTree)
Redis GEO là sorted set với điểm là geohash 52 bit, nên GEOADD và tìm bán kính đều nhanh. Một node chịu khoảng 100.000 ops/s, đủ cho 15.000 cập nhật/s. Chia key theo thành phố (geo:hn, geo:hcm) để mỗi set nhỏ và dễ tách node.
Đánh đổi: Mất node là mất vị trí, nhưng chấp nhận được vì tài xế gửi lại sau 4 s. Một key thành phố lớn vẫn là hot key.
Nguồn: Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Design" (bảng băm cập nhật mỗi 4 s, QuadTree làm mới mỗi 10-15 s) ; Chưa có nguồn trong thư viện: Redis GEO (geohash 52 bit) và key theo thành phố không có trong sách
Mỗi tài xế thuộc một ô H3 (lục giác, khoảng 0,1 km² ở độ phân giải 9). Tìm xe gần là đọc ô hiện tại và vòng ô lân cận, nên chi phí cố định. Ô là đơn vị shard tự nhiên, cũng là đơn vị tính surge. Grokking dùng QuadTree cho Yelp, nhưng với điểm di chuyển liên tục thì sửa cây tốn kém, nên sách đề xuất bảng hash cập nhật 4 s và chỉ làm mới QuadTree mỗi 10–15 s.
Đánh đổi: Phải tự xử lý tài xế ở mép ô và đổi ô; H3 lục giác cho khoảng cách đều hơn geohash nhưng cần thêm thư viện.
Nguồn: Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Design" (bảng băm vị trí 4 s + QuadTree làm mới 10-15 s, tr. mục "Dynamic segment solution") ; Grokking Modern System Design · ch.29 Design Yelp ↗ · mục "QuadTree" (chia bản đồ thành ô) ; Chưa có nguồn trong thư viện: H3/geohash không có trong sách
2) Làm sao ghép khách với tài xế mà không giao một tài xế cho hai chuyến cùng lúc?
Nhiều chuyến cùng mời một tài xế, tài xế bấm nhận ở hai màn hình gần như cùng lúc. Không có chỗ nào đảm bảo chỉ một lần nhận thắng.
Đánh đổi: Ghép nhanh nhưng sinh tranh chấp, tài xế bị spam lời mời.
Nguồn: Microservices Patterns ↗ (.zip) · ch.4 · §4.1.3 Using the Saga pattern to maintain data consistency (vấn đề thiếu cô lập khi nhiều giao dịch chạm cùng dữ liệu)
UPDATE drivers SET status='ON_TRIP', trip_id=? WHERE id=? AND status='AVAILABLE'. Chỉ một transaction cập nhật được 1 dòng, các lần khác nhận 0 dòng và trả 409.
Đánh đổi: Đúng tuyệt đối nhưng mọi lần mời đều phải chạm DB, và vẫn có thể mời trùng rồi mới từ chối ở bước nhận.
Nguồn: Microservices Patterns ↗ (.zip) · ch.4 · §4.1.3 Using the Saga pattern to maintain data consistency (đảm bảo một lần cập nhật thắng qua điều kiện trạng thái) ; Chưa có nguồn trong thư viện: UPDATE ... WHERE status là kỹ thuật SQL cơ bản
Trước khi mời, matching chạy SET lock:driver:{id} {tripId} NX PX 15000. Không lấy được khoá thì bỏ qua tài xế đó. Khoá tự hết hạn nếu matching chết giữa chừng. Khi tài xế nhận, trip chuyển MATCHING sang ACCEPTED bằng UPDATE ... WHERE status='MATCHING', đây mới là nguồn sự thật. Ở quy mô lớn, mỗi ô bản đồ do một matcher xử lý (partition theo ô) nên tranh chấp gần như biến mất.
Đánh đổi: Redis lock không tuyệt đối khi failover, nên bước cuối vẫn phải là điều kiện trong DB; lời mời tuần tự làm khách chờ lâu hơn khi nhiều tài xế từ chối.
Nguồn: Redis in Action ↗ · ch.6 · §6.2.3 Building a lock in Redis, §6.2.5 Locks with timeouts (khoá có hạn tự hết) ; Chưa có nguồn trong thư viện: mỗi matcher xử lý một ô bản đồ là thiết kế của khoá học
3) Tính ETA và giá surge thế nào cho đủ nhanh và đủ đúng?
Ở Hà Nội, hai điểm cách nhau 1 km theo đường thẳng có thể phải đi 4 km vì đường một chiều và sông. ETA sai thì khách huỷ và giá cũng sai theo.
Đánh đổi: Rẻ, nhanh, nhưng sai nhiều nhất đúng ở những chỗ khách để ý.
⚠ Chưa có nguồn trong thư viện: ETA theo đường chim bay không được bàn trong sách; ví dụ Hà Nội là giả định của khoá học
Dùng routing có dữ liệu giao thông. Cache kết quả theo cặp ô H3 đi và đến trong 2–5 phút để giảm số lần gọi.
Đánh đổi: Phí API tăng theo số báo giá, thường là khoản lớn nhất trong hoá đơn hạ tầng ở mức 2.
Nguồn: Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Workflow of our application" (ETA/ngoại lai) ; Chưa có nguồn trong thư viện: Maps API (Google, Goong) và cache theo cặp ô
Đường được tìm trên đồ thị chia vùng với contraction hierarchies, trọng số cạnh là tốc độ thực tế lấy từ GPS tài xế. Một model hiệu chỉnh sai số theo giờ, thời tiết, khu vực. Surge: stream job đếm yêu cầu và tài xế trống trên mỗi ô H3 mỗi phút, hệ số làm mượt và có trần, sau đó chốt vào quote.
Đánh đổi: Tốn công xây và vận hành, chỉ đáng khi số báo giá đủ lớn để phí Maps API vượt chi phí đội ngũ.
⚠ Chưa có nguồn trong thư viện: contraction hierarchies, model hiệu chỉnh ETA và surge pricing không có trong sách; chỉ nêu ý tưởng
4) Quản lý trạng thái chuyến và thanh toán sao cho mạng chập chờn không gây sai trạng thái hay trừ tiền hai lần?
Request COMPLETED gửi lại đến sau CANCELLED có thể đưa chuyến về trạng thái sai. App retry lệnh thanh toán thì cổng thanh toán thu hai lần.
Đánh đổi: Ít code nhưng sai dữ liệu tiền, loại lỗi đắt nhất.
Nguồn: Microservices Patterns ↗ (.zip) · ch.3 · §3.3.6 Handling duplicate messages ; Building Microservices ↗ (.zip) · ch.11 · mục "Idempotency" (tr. 215)
Bảng chuyển hợp lệ: ACCEPTED → ARRIVED → IN_PROGRESS → COMPLETED. Mỗi lệnh là UPDATE ... WHERE id=? AND status=<trạng thái trước>. Request trùng hoặc đến muộn cập nhật 0 dòng và được trả kết quả hiện tại.
Đánh đổi: Trạng thái đúng, nhưng chưa xử lý trường hợp đã lưu COMPLETED mà gửi lệnh thanh toán thất bại.
Nguồn: Microservices Patterns ↗ (.zip) · ch.4 · §4.1.3 Using the Saga pattern to maintain data consistency (trạng thái từng bước) ; Chưa có nguồn trong thư viện: máy trạng thái bằng UPDATE có điều kiện
COMPLETED và sự kiện TripCompleted ghi chung transaction (outbox). Payment service tiêu thụ sự kiện theo kiểu idempotent, gọi cổng thanh toán với key payment:{tripId}, chống trùng cả webhook trả về. Thẻ được giữ tiền (authorize) lúc đặt và thu (capture) lúc kết thúc. Sổ cái ghi kép giúp đối soát. Tiền mặt vẫn đi qua cùng sổ cái để tính hoa hồng tài xế.
Đánh đổi: Nhiều thành phần hơn và thanh toán là bất đồng bộ, khách có thể thấy biên lai sau vài giây.
Nguồn: Microservices Patterns ↗ (.zip) · ch.3 · §3.3.7 Transactional messaging (outbox, tr. 97–100) ; Microservices Patterns ↗ (.zip) · ch.3 · §3.3.6 Handling duplicate messages ; Building Microservices ↗ (.zip) · ch.11 · mục "Idempotency" ; Chưa có nguồn trong thư viện: sổ cái ghi kép, authorize/capture của cổng thanh toán
Trong phỏng vấn
- “Vị trí tài xế là dữ liệu sống vài giây, nên tôi giữ trong bộ nhớ và chấp nhận mất khi node chết, tài xế sẽ gửi lại sau 4 giây.”
- “Tôi tách hai luồng có tải khác hẳn nhau: cập nhật vị trí (hàng chục nghìn/s, chịu mất) và đặt xe (ít hơn nhiều, không được sai).”
- “Khoá Redis có TTL để tránh mời trùng, nhưng nguồn sự thật cuối cùng là UPDATE có điều kiện trên trạng thái chuyến trong DB.”
- “Thanh toán đi qua outbox và idempotency key theo tripId, nên retry hay webhook trùng cũng chỉ thu một lần.”
- Ghi mọi cập nhật vị trí vào DB quan hệ rồi mới nghĩ tới cache
- Gửi lời mời cho nhiều tài xế cùng lúc mà không nói cách chặn hai người cùng nhận
- Dùng QuadTree cho điểm di chuyển liên tục mà không nói chi phí cập nhật cây
- Quên tiền mặt và quên trường hợp app tài xế mất sóng giữa chuyến
Đọc thêm
Sách trong thư viện của bạn. Số chương có thể lệch nhẹ so với bản PDF.