Thanh toán — tổng quan case không lệch một đồng
Lệnh trừ tiền không đảo được, cổng thanh toán có lúc im lặng, sổ phải giữ mười năm không sửa: từ ràng buộc tới ba quyết định, mỗi cái kèm cái giá.
TL;DR: Case này đặt một bài toán cụ thể: 3.000 lượt thanh toán mỗi giây lúc đỉnh sale, tiền đi qua một nhà cung cấp bên ngoài, lệnh trừ tiền đã thành công thì không huỷ tức thời được, và 0,2% giao dịch không có kết quả rõ sau một phút. Bạn sẽ tự ước lượng số tiền thực sự bị treo, tái hiện các kiểu lệch tiền trên Postgres của mình, rồi tự chọn ba quyết định trên ba trục: thứ tự bước quanh lệnh trừ tiền, cách ghi sổ, cách chốt và đối soát. Mỗi lựa chọn đều kèm một cái giá được nêu ra và đo ra.
Chín giờ tối, đợt sale mở. Một khách quét QR, app ngân hàng báo trừ 250.000 đ, rồi khách đóng trình duyệt trước khi trang chuyển về. Phía bạn, đơn vẫn ghi "chờ thanh toán", và mười phút sau hàng giữ cho đơn đó tự quay về kho. Không có dòng code nào sai cú pháp, cũng không có exception nào được log. Module này đi từ luồng "nghe đủ" như vậy tới chỗ biết chính xác tiền lệch ở bước nào, vì sao, và sửa bằng cách đổi lấy cái gì.
1. Bài toán và bảng ràng buộc
Bảng dưới là bản rút gọn của bảng ở bài 01. Các bài sau trích lại đúng bộ số này, không đặt số mới.
| Ràng buộc | Giá trị |
|---|---|
| Đỉnh đợt sale | 3.000 lượt thanh toán mỗi giây trong 10 phút |
| Giá trị đơn trung bình | 250.000 đ |
| Hai kênh | 70% trang cổng (QR, thẻ ATM, thẻ quốc tế), 30% một chạm (ví liên kết, thẻ đã lưu) |
| Giao dịch mập mờ | 0,2% lượt không có kết quả rõ sau 1 phút, khoảng 60% trong số đó đã trừ tiền |
| Giữ hàng chờ thanh toán | 10 phút, trong khi khách có 15 phút để trả trên trang cổng |
| Hoàn tiền | ví khoảng 1 ngày làm việc, thẻ quốc tế 5 đến 10 ngày, có khi tới 30 |
| Lưu vết | mọi bút toán giữ ít nhất 10 năm, không sửa, không xoá |
| File đối soát | 6 giờ sáng hôm sau, ngày chốt theo giờ Việt Nam |
Hai chữ cần định nghĩa ngay vì cả module dùng chúng. Trang cổng là trang thanh toán của nhà cung cấp: khách tự trả trên đó, bạn chỉ biết kết quả qua IPN (Instant Payment Notification, lời gọi nhà cung cấp gửi về server của bạn để báo kết quả) hoặc qua API tra cứu. Giao dịch mập mờ là giao dịch mà sau một phút phía bạn vẫn không biết tiền đã bị trừ hay chưa.
Thử nhân vài dòng của bảng với nhau. Mười phút ở 3.000 lượt mỗi giây là 1.800.000 lượt, 0,2% của số đó là 3.600 giao dịch mập mờ, và 60% của 3.600 là 2.160 giao dịch khách đã mất tiền mà đơn chưa ghi nhận. Nhân với 250.000 đ thì ra khoảng 540 triệu đồng đang treo sau mỗi đợt sale. Con số đó không phải lỗi hiếm; nó là chuyện chắc chắn xảy ra, và câu hỏi chỉ là bạn chốt nó trong bao lâu.
2. Sau module này bạn sẽ
- Ước lượng số lượt thanh toán mỗi giây lúc đỉnh, số giao dịch mập mờ, số tiền thực sự bị treo và số lượt ghi vào ba tài khoản chung từ bảng ràng buộc tự đặt, rồi so với trần ghi một hàng đo trên máy bạn.
- Chẩn đoán các kiểu lệch tiền (khách đã trả mà đơn vẫn chưa thanh toán, khách bị trừ hai lần, đơn đã thanh toán mà sổ thiếu bút toán, hàng đã về kho trong khi khách vẫn trả thành công) khi luồng tin kết quả trả về đồng bộ gặp timeout, khách đóng trình duyệt hoặc tiến trình chết.
- Thiết kế thứ tự bước quanh lệnh trừ tiền không đảo được (giữ hàng rồi trừ tiền, trừ tiền rồi tạo đơn, hay giữ tiền rồi thu sau), kèm trạng thái chưa rõ và bước bù mà thứ tự đó đòi hỏi.
- Chọn cách ghi sổ cho từng nhóm tài khoản (cột số dư kèm bảng lịch sử, sổ cái bút toán kép chỉ ghi thêm lấy số dư bằng tổng, hay sổ cái kèm số dư dẫn xuất cập nhật cùng transaction) bằng cách so tranh chấp ghi trên tài khoản chung và độ trễ đọc số dư.
- Thiết kế cơ chế chốt và đối soát với nhà cung cấp (IPN, tra cứu định kỳ trong giới hạn API, so sổ với file đối soát) sao cho không giao dịch nào mập mờ quá 24 giờ và mọi khoản lệch lộ ra trước 12 giờ trưa hôm sau.
- Giải thích vì sao một hệ thanh toán thật chọn khác thiết kế của bạn dựa trên ràng buộc của họ.
3. Lộ trình bảy bài, mỗi bài một vai
- Bài 01, ràng buộc và ước lượng: chốt bảng số, tính tiền treo và tải lên ba tài khoản chung, tự đo trần ghi một hàng. Chưa có thiết kế nào, chỉ có số.
- Bài 02, phương án ngây thơ: tin trang chuyển về và kết quả đồng bộ, timeout thì báo thất bại rồi thử lại. Bạn tự đếm số đơn lệch tiền mà nó gây ra.
- Bài 03, thứ tự bước: quyết định thứ nhất, trục "đặt lệnh trừ tiền không đảo được ở đâu".
- Bài 04, sổ tiền: quyết định thứ hai, trục "ghi sổ thế nào để mười năm sau vẫn giải thích được từng đồng".
- Bài 05, chốt và đối soát: quyết định thứ ba, trục "làm sao biết chắc kết quả khi nhà cung cấp im lặng".
- Bài 06, đối chiếu thực tế: các hệ thanh toán đã công bố kiến trúc chọn gì, và ràng buộc nào của họ khác bảng của bạn.
- Bài 07, tổng kết: ráp ba quyết định thành một thiết kế và gom các cái giá vào một bảng.

Vì sao ba quyết định đứng song song chứ không nối tiếp? Vì mỗi cái sửa một kiểu lệch khác nhau mà bài 02 phơi ra. Thứ tự bước quyết định hàng có về kho trước khi khách trả xong hay không. Cách ghi sổ quyết định đơn đã thanh toán có thể thiếu bút toán hay không. Cách chốt quyết định giao dịch mập mờ nằm đó bao lâu. Đổi lựa chọn trên trục này hầu như không lật đáp án của trục kia, nên sau bài 02 bạn đọc theo thứ tự nào cũng được, dù thứ tự 03, 04, 05 là đường dễ nhất vì bài 05 dùng lại trạng thái "chưa rõ" của bài 03.
4. Cần chuẩn bị gì
Đây là case thứ tư, case khó nhất của tuyến, và nó đứng trên năm khoá trước (khoá news-feed cũng nằm trong chuỗi tiên quyết của tuyến, nhưng case này không gọi lại nó):
- sql-database: transaction và isolation level, vì bút toán bốn dòng phải vào sổ cùng lúc hoặc không dòng nào vào.
- data-phan-tan: timeout không có nghĩa là thất bại, và gọi lại thì phải idempotent (gọi nhiều lần cho cùng một kết quả như gọi một lần).
- data-phai-sinh: dữ liệu dẫn xuất từ một log chỉ ghi thêm, nền của sổ cái ở bài 04.
- tkht-flash-sale và tkht-thong-bao: hàng nóng, giữ chỗ có hạn, gửi lại có trùng. Case này gặp lại cả ba, chỉ khác là lần này thứ bị trùng hay bị mất là tiền.
Các bài dùng <Recall> để nhắc lại nguyên lý từ những khoá trên thay vì dạy lại. Về công cụ, bạn cần Postgres chạy bằng Docker, psql và pgbench. Số đo khác máy, nên bài dạy theo tỉ lệ, và máy bạn cho số khác là bình thường.
5. Thời lượng
Cả module mất khoảng 4 đến 4,5 giờ: chừng 2,5 giờ đọc, gần 2 giờ chạy script và tự trả lời trước khi xem đáp án mẫu. Đừng gộp vào một buổi.
| Bài | Đọc | Tự làm |
|---|---|---|
| 01 Ràng buộc và ước lượng | ~20 phút | ~20 phút đo trần |
| 02 Phương án ngây thơ | ~20 phút | ~20 phút mô phỏng |
| 03 Thứ tự bước | ~25 phút | ~20 phút |
| 04 Sổ tiền | ~25 phút | ~25 phút đo tranh chấp |
| 05 Chốt và đối soát | ~25 phút | ~20 phút |
| 06 Đối chiếu thực tế | ~20 phút | — |
| 07 Tổng kết | ~15 phút | ~10 phút tự đánh giá |
6. Cách học case này
Bốn thói quen, lặp lại ở mọi bài:
- Tự quyết trước khi đọc lời giải. Gặp
<DesignChoice>thì chọn một phương án rồi mới đọc phần phân tích. Mọi cái giá đã hiện sẵn, việc của bạn là chọn cái giá mà ràng buộc gánh được. - Đổi tính từ thành tiền. Bảng ràng buộc có giá của một cuộc gọi tổng đài (15.000 đ), của một lần trừ hai lần (khoảng 18.000 đ), của một khoản lệch sổ (100.000 đ). Khi định viết "phương án này rủi ro hơn", hãy nhân ra số đồng.
- Hỏi "nếu tiến trình chết ngay sau dòng này". Đặt câu đó sau từng bước của luồng thanh toán. Kiểu lệch nào cũng bắt đầu từ một chỗ chết mà thiết kế chưa tính.
- Tự đo. Mỗi bài có một script nhỏ chạy trên Postgres của bạn. Con số của chính bạn thuyết phục hơn con số trong bài.
Bắt đầu với bài 01.
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Thảo luận & góp ý
Hỏi đáp về bài này