Giao diện
Tiền & sổ cái
Dữ liệu tiền phải trả lời được “vì sao số dư là con số này” ở bất kỳ thời điểm nào. Chỉ lưu và UPDATE một cột số dư thì không trả lời được.
Sổ cái chỉ thêm (append-only ledger)
Mỗi giao dịch là một hoặc nhiều dòng không bao giờ sửa, không bao giờ xoá. Sai thì ghi bút toán đảo ngược, không sửa dòng cũ.
sql
CREATE TABLE ledger_entries (
id BIGINT PRIMARY KEY,
transaction_id BIGINT NOT NULL, -- nhóm các dòng của một giao dịch
account_id BIGINT NOT NULL,
amount BIGINT NOT NULL, -- đơn vị nhỏ nhất; dương = ghi có, âm = ghi nợ
currency CHAR(3) NOT NULL,
created_at TIMESTAMP NOT NULL
);
CREATE INDEX idx_ledger_account ON ledger_entries (account_id, created_at);Ghi kép (double-entry)
Mỗi giao dịch gồm các dòng có tổng bằng 0: chuyển 100.000 từ ví A sang ví B là hai dòng A: −100.000 và B: +100.000. Tiền không tự sinh ra hay mất đi; tổng mọi tài khoản luôn đối soát được. Phí, hoàn tiền, khuyến mãi đều là các tài khoản riêng (tài khoản doanh thu phí, tài khoản khuyến mãi…).
Số dư
Số dư = tổng amount của tài khoản. Cộng dồn mỗi lần đọc thì chậm dần, nên thường lưu sẵn trong accounts.balance và cập nhật trong cùng transaction với lệnh ghi sổ cái:
sql
BEGIN;
UPDATE accounts SET balance = balance - 100000
WHERE id = :from AND balance >= 100000; -- 0 dòng: không đủ tiền, rollback
UPDATE accounts SET balance = balance + 100000 WHERE id = :to;
INSERT INTO ledger_entries (…) VALUES (…, :from, -100000, …), (…, :to, 100000, …);
COMMIT;Khoá hai tài khoản theo thứ tự id tăng dần để tránh deadlock khi có chuyển tiền chéo (MySQL chương 7). Job đối soát định kỳ so balance với tổng sổ cái.
Idempotency key
Client retry khi timeout không được làm trừ tiền hai lần. Mỗi request mang một idempotency_key do client sinh ra, lưu trong bảng giao dịch với ràng buộc UNIQUE. Request lặp lại đụng UNIQUE thì trả về kết quả của lần đầu, không thực hiện lại.
Kiểu dữ liệu
Lưu số nguyên theo đơn vị nhỏ nhất (đồng, xu, cent) kèm mã tiền tệ, hoặc DECIMAL đủ chữ số. Không bao giờ dùng FLOAT/DOUBLE (MySQL chương 2).
Ghi nhớ
- Sổ cái chỉ thêm; sai thì ghi bút toán đảo, không sửa.
- Ghi kép: mỗi giao dịch có tổng bằng 0.
- Số dư lưu sẵn, cập nhật cùng transaction với sổ cái, đối soát định kỳ.
- Idempotency key + UNIQUE để retry an toàn.