System Design — Bài 20: Thiết kế ví điện tử: ledger, idempotency và đối soát
Client thấy timeout sau khi chuyển tiền rồi bấm thử lại. Một hệ thống đúng phải nhận ra cùng ý định chuyển tiền và trả kết quả đã ghi. Bài tập này tập trung vào thiết kế phần mềm: nguồn sự thật, dữ liệu bất biến, kiểm soát đồng thời và trạng thái chưa biết khi gọi đối tác.
Khóa học System Design: mục lục 21 bài — bạn đang đọc bài 20.
Các con số dưới đây là giả định phục vụ học thiết kế, không phải số liệu hay kiến trúc nội bộ của sản phẩm được nhắc tên. Dung lượng dùng đơn vị thập phân; capacity thực tế phải đo theo workload và mục tiêu chất lượng.
Yêu cầu đảo ngược mọi ưu tiên
Phạm vi gồm nạp, rút, chuyển tiền, lịch sử và đối soát. Cần xác định đơn vị tiền, giới hạn giao dịch và trạng thái mà API được phép báo thành công. Trong code thường biểu diễn số tiền bằng đơn vị nhỏ nhất hoặc decimal chính xác theo quy tắc tiền tệ, tránh số thực nhị phân cho tính toán cần chính xác.
Giả định 600 giao dịch mỗi giây là đầu vào bài tập. Chưa có lý do tự động kết luận phải shard. Hãy đo cả tài khoản nóng, contention và độ trễ đuôi, vì một merchant tập trung nhiều giao dịch có thể là điểm khó nhất.
Khi không đủ điều kiện bảo đảm bất biến, luồng chuyển tiền cần từ chối hoặc giữ pending theo thiết kế thay vì ghi đoán. Nhưng không có nghĩa toàn bộ ví phải ngừng phục vụ mọi chức năng: đọc lịch sử, thông báo và chuyển tiền có thể có chính sách khác nhau.
Ledger ghi kép
Với giao dịch chuyển khoản đơn giản, ledger ghi tác động tương ứng ở hai tài khoản và phải cân bằng theo quy tắc kế toán, trong cùng loại tiền. Một giao dịch có phí hoặc nhiều bên có thể có nhiều dòng; “ghi kép” không có nghĩa mọi giao dịch luôn đúng hai bản ghi.
Các dòng đã ghi nhận được giữ append-only theo thiết kế. Nếu cần sửa một giao dịch, tạo bút toán điều chỉnh hoặc đảo có liên kết tới giao dịch gốc, để lịch sử vẫn giải thích được. Số dư có thể tính từ ledger hoặc giữ bảng tổng hợp cập nhật trong cùng transaction.
Append-only không tự làm audit đáng tin: còn cần quyền truy cập, tính toàn vẹn và đối soát. Cũng phải định nghĩa rõ debit và credit theo loại tài khoản, không giản lược chúng thành “luôn trừ” và “luôn cộng” cho mọi mô hình kế toán.
Chống double-spend và chống trùng
Trong phạm vi một database, gói kiểm tra điều kiện, cập nhật số dư, ghi ledger và kết quả idempotency vào transaction phù hợp. Có thể dùng khóa dòng hoặc conditional update; cần kiểm tra số dòng cập nhật để biết điều kiện còn đúng. Optimistic version cũng là lựa chọn nếu mức xung đột phù hợp.
Idempotency key phải có phạm vi rõ như khách hàng và thao tác. Cùng key cùng payload trả lại kết quả đã ghi; cùng key nhưng payload khác cần bị từ chối theo hợp đồng. Unique constraint giúp điều phối cạnh tranh, nhưng chỉ giữ key riêng mà không gắn với transaction nghiệp vụ vẫn có cửa sổ lỗi.
Nếu client timeout sau commit, retry cùng key phải tìm lại giao dịch thành công, không trừ tiền lần nữa. Với hai tài khoản, khóa theo thứ tự thống nhất giúp giảm deadlock. Sharding chỉ nên xét khi đã có cách giữ bất biến chuyển tiền xuyên shard và recovery.
Ra ngoài hệ thống & đối soát
Nạp hoặc rút qua đối tác cần một state machine có pending và cơ chế tra cứu. Timeout khi gọi đối tác là trạng thái chưa biết kết quả, không nên tự coi là thất bại rồi gửi một giao dịch mới không liên quan.
Giữ định danh ổn định, xử lý callback trùng, xác minh callback và chủ động query trạng thái theo hợp đồng. Nếu cần bù, bút toán bù cũng có định danh và được quản lý như một bước có thể lỗi. Event cho các hệ downstream có thể phát qua outbox để tách commit nội bộ khỏi publish.
Đối soát so ledger nội bộ với dữ liệu đối tác, tìm chênh lệch do thời điểm chốt, phí, trùng hoặc trạng thái chưa hoàn tất. Cần quy trình phân loại và xử lý, không tự sửa số dư để ép hai tổng bằng nhau. Khả năng lần theo một giao dịch từ đầu tới cuối là phần cốt lõi của thiết kế.
Biên transaction cho chuyển khoản nội bộ
BEGIN
nhận idempotency_key theo phạm vi khách hàng + thao tác
kiểm tra payload khớp nếu key đã có
khóa tài khoản theo thứ tự ổn định
kiểm tra số dư và điều kiện chuyển tiền
cập nhật số dư + các dòng ledger cân bằng theo đồng tiền
ghi kết quả idempotency + outbox nếu cần
COMMIT
Timeout sau COMMIT -> retry cùng key -> trả kết quả cũ
Cùng key, payload khác -> từ chối theo hợp đồng APIĐây là luồng khái niệm, cần hiện thực ràng buộc unique và cách xử lý transaction cạnh tranh trong DB cụ thể. Tiền dùng số nguyên theo đơn vị đã chốt hoặc decimal chính xác. Chưa thể suy từ 600 TPS giả định rằng bắt buộc phải shard.
Thuật ngữ cần nhớ
ACID — Atomicity, Consistency, Isolation, Durability: Bốn thuộc tính giao dịch: nguyên tử (tất cả hoặc không); nhất quán (giữ bất biến dữ liệu); cô lập (kiểm soát ảnh hưởng đồng thời); bền vững (dữ liệu đã commit tồn tại theo cam kết). Chữ C ở đây khác consistency trong định lý CAP.
TPS — Transactions Per Second: Số giao dịch mỗi giây; phải định nghĩa ranh giới giao dịch nghiệp vụ hoặc giao dịch dữ liệu trước khi so sánh.
API — Application Programming Interface: Giao diện lập trình ứng dụng: hợp đồng để thành phần khác gọi chức năng, gồm đầu vào, đầu ra và quy tắc lỗi.
Bài tập tự thực hành
Client timeout sau khi transfer đã commit rồi gửi lại.
Gợi ý kiểm tra lời giải
Cùng key và payload trả kết quả cũ; key khác payload bị từ chối; ledger không ghi hai lần.
Tự kiểm tra sau bài học
Ledger cân bằng theo đồng tiền và không sửa mất lịch sử.
Dedup và chuyển tiền nằm trong cùng biên transaction.
Timeout đối tác giữ trạng thái chưa biết và có đường tra cứu/đối soát.
Đọc thêm từ tài liệu gốc
Tiếp tục lộ trình
Bài trước — Thiết kế giao đồ ăn: geo query, vị trí và dispatch an toàn
Bài tiếp — Thiết kế Flash Sale: chịu tải đột biến và không oversell
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.