Làm rõ trước: khách chỉ xem được shipper của đơn của mình, cần cập nhật mỗi 3-5 giây (không phải 60 lần/giây), và chỉ trong lúc đơn đang giao. Giả sử 20.000 shipper hoạt động, mỗi người ping 5 giây một lần → 4.000 ghi/giây. Con số này quyết định toàn bộ thiết kế: đây là bài ghi nhiều, dữ liệu sống ngắn.
Đường ghi: app shipper gửi POST /locations {lat, lng, ts, orderId} (gộp vài điểm một lần khi mạng yếu). Vị trí hiện tại ghi vào Redis, không ghi Postgres mỗi ping.
SET courier:{id}:pos "lat,lng,ts" EX 60
GEOADD couriers {lng} {lat} {courierId} -- de tim shipper gan donLịch sử hành trình chỉ cần cho đối soát → đẩy vào queue, worker ghi theo lô xuống bảng dạng time-series, phân vùng theo ngày.
Đường đọc: khách mở màn hình theo dõi → SSE hoặc WebSocket đẩy vị trí xuống. SSE đủ vì luồng một chiều và tự reconnect; WebSocket chỉ cần khi có tương tác hai chiều (chat với shipper).
Điểm nghẽn và cách xử lý:
- Ghi Postgres mỗi ping sẽ chết trước nhất → Redis là nơi giữ trạng thái nóng, DB chỉ nhận dữ liệu đã gộp.
- Kết nối realtime là có trạng thái → cần sticky routing hoặc pub/sub giữa các node để node đang giữ kết nối của khách nhận được cập nhật.
- Mạng 4G rớt liên tục → client đệm điểm và gửi lại, server loại điểm cũ theo ts thay vì tin thứ tự đến.
Đánh đổi: giảm tần suất ping (5s thay vì 1s) làm marker giật hơn nhưng giảm tải 5 lần — bù bằng nội suy chuyển động ở client. Đó là cách đổi lấy chi phí đúng chỗ: mượt là việc của client, chính xác là việc của server.