1. Bối cảnh doanh nghiệp & phạm vi
Một nền tảng du lịch trực tuyến (OTA) bán vé máy bay nội địa, phòng khách sạn và tour ghép, xử lý trung bình 40.000 giao dịch đặt chỗ mỗi ngày, riêng đợt khuyến mãi Tết hoặc hè có thể vọt lên 300.000 giao dịch trong một ngày duy nhất. Mỗi giao dịch trải qua hai bước bắt buộc: giữ chỗ tạm thời (hold) trong một khoảng thời gian sống nhất định (thường 10–15 phút) để khách hoàn tất thanh toán, và xác nhận cuối cùng (confirm) khi thanh toán thành công. Nếu hệ thống giữ chỗ sai — cho phép hai khách hàng cùng giữ một ghế máy bay hạng thương gia cuối cùng hoặc cùng một phòng suite duy nhất còn trống — hậu quả không chỉ là hoàn tiền và bồi thường mà còn là mất uy tín với đối tác hãng bay/khách sạn, những đơn vị đã tin tưởng giao tồn kho thực cho nền tảng quản lý. Một sàn OTA khu vực từng phải bồi thường hàng loạt và tạm ngừng bán vé của một hãng bay đối tác trong 2 tuần sau sự cố double-booking hàng loạt vào đợt cao điểm lễ.
Phạm vi bài viết bao trùm toàn bộ vòng đời một booking: tìm kiếm & khoá giá tạm thời, giữ chỗ (hold) với TTL rõ ràng, thanh toán, xác nhận cuối cùng, huỷ/hoàn tiền theo chính sách, và hậu kiểm đối soát tồn kho định kỳ. Ràng buộc nghiệp vụ quan trọng: mỗi ghế máy bay hoặc phòng khách sạn là một đơn vị tồn kho không thể chia nhỏ (indivisible unit) — không có khái niệm 'bán thừa 0.5 ghế'; và mọi thao tác ghi vào tồn kho phải đi qua một lớp khoá tập trung (Inventory Lock Service) để tránh race condition khi nhiều luồng đọc-sửa-ghi song song. Ràng buộc tuân thủ gồm: log giao dịch giữ chỗ/huỷ phải lưu tối thiểu 1 năm để phục vụ tranh chấp khiếu nại, và số tiền hoàn trả phải khớp chính xác với số tiền đã thu (không làm tròn sai lệch).
Phạm vi tự động hoá
- Kiểm thử API cho luồng hold/pay/confirm/cancel và service khoá tồn kho
- Kiểm thử đồng thời (concurrency) mô phỏng nhiều khách cùng giữ 1 ghế/phòng
- Kiểm thử k6 cho tải cao giờ cao điểm săn vé khuyến mãi
2. Kiến trúc hệ thống & luồng nghiệp vụ
Kiến trúc gồm 5 thành phần chính: Booking API Gateway (điều phối request từ client, sinh idempotency key cho mỗi thao tác ghi), Inventory Lock Service (giữ khoá phân tán theo seatId/roomId, đảm bảo chỉ một luồng được sửa trạng thái ghế tại một thời điểm), Hold Expiry Scheduler (quét định kỳ các hold quá TTL để tự động trả ghế về tồn kho), Payment Service (xử lý thanh toán, gắn liền idempotency key với giao dịch), và Booking State Store (lưu trạng thái booking theo máy trạng thái hữu hạn HOLD → PAID → CONFIRMED, hoặc HOLD → EXPIRED, hoặc CONFIRMED → CANCELLED). Luồng nghiệp vụ chuẩn: khách chọn ghế, Booking API gọi Inventory Lock Service để khoá ghế và tạo bản ghi Hold với expiresAt = now + TTL; nếu khoá thành công, khách được chuyển sang bước thanh toán; khi Payment Service xác nhận thu tiền thành công trước thời điểm expiresAt, booking chuyển sang CONFIRMED và ghế được trừ vĩnh viễn khỏi tồn kho khả dụng.
Điểm khó khi kiểm thử
Điểm khó nhất là kiểm chứng tính đúng đắn của khoá phân tán dưới tải đồng thời cao: hai request hold cùng một seatId gửi tới trong khoảng vài mili-giây phải có đúng một request thắng, request còn lại phải nhận lỗi rõ ràng (409 Conflict) thay vì cả hai đều 'thành công' rồi ghi đè lẫn nhau ở tầng dữ liệu — đây là lỗi race condition kinh điển mà kiểm thử tuần tự thông thường không bao giờ phát hiện được. Điểm khó thứ hai là xử lý biên thời gian: khi request thanh toán đến rất sát thời điểm hold hết hạn, hệ thống cần một quy tắc rõ ràng (ví dụ: chỉ chấp nhận nếu Payment Service ghi nhận thành công TRƯỚC khi Hold Expiry Scheduler xử lý bản ghi đó) để tránh tình trạng vừa hết hạn vừa được xác nhận thanh toán — hai trạng thái mâu thuẫn tồn tại song song. Cuối cùng, việc trả ghế về tồn kho khi hold hết hạn phải là thao tác nguyên tử (atomic) và idempotent, vì scheduler có thể quét trùng cùng một bản ghi ở hai lần chạy gần nhau do lỗi mạng hoặc restart.
3. Mô hình dữ liệu & bất biến nghiệp vụ (oracle)
Mô hình dữ liệu cốt lõi gồm 5 thực thể: Inventory (seatId/roomId, tripId, status AVAILABLE/HELD/SOLD), Hold (id, seatId, bookingId, createdAt, expiresAt, version), Booking (id, customerId, items, status HOLD/PAID/CONFIRMED/EXPIRED/CANCELLED, totalAmount), Payment (bookingId, amount, idempotencyKey, status), và AuditLog (mọi thay đổi trạng thái, ghi kèm actor và lý do). Bất biến quan trọng nhất — cũng là oracle chính của toàn bộ bài toán — là: tại mọi thời điểm, mỗi Inventory item chỉ được liên kết với đúng một Hold hoặc Booking đang hiệu lực (không EXPIRED, không CANCELLED); nói cách khác, không bao giờ tồn tại hai booking CONFIRMED cùng trỏ tới một seatId — đây chính là bất biến chống double-booking.
- Bất biến 1: Không double-booking — 1 seatId chỉ có tối đa 1 Hold/Booking hiệu lực tại một thời điểm
- Bất biến 2: Hold hết hạn (expiresAt < now, chưa PAID) phải được trả về tồn kho AVAILABLE trong thời gian ngắn (SLA quét)
- Bất biến 3: Booking.totalAmount = Σ(giá từng item + phụ phí), khớp chính xác với Payment.amount đã thu
- Bất biến 4: Mọi thao tác pay/confirm/cancel đều idempotent theo idempotencyKey — gửi lại không tạo bản ghi trùng
- Bất biến 5: Trạng thái Booking chỉ đi theo đúng máy trạng thái hữu hạn, không có bước nhảy trái phép (ví dụ EXPIRED không thể chuyển thẳng sang CONFIRMED)
💬 Bình luận (0)
Chưa có bình luận nào. Hãy là người đầu tiên!