Case study, ngân hàng số
Cấp 11.000 mã khách hàng mỗi ngày mà không trùng cái nào
Trong luồng onboarding của một ngân hàng số, mình bỏ cách cấp mã khách hàng theo sequence và chuyển sang các pool cấp sẵn. Chuyện đằng sau là thế này.
Bài toán
Ai mở tài khoản ở ngân hàng cũng được cấp một số CIF, cái mã đi theo họ qua mọi sản phẩm. Ngân hàng có khoảng 3 triệu khách, và mỗi ngày luồng onboarding cấp chừng 11.000 số như vậy. Thiết kế cũ lấy số từ một sequence: không trùng thật, nhưng nhìn số trước là đoán được số sau.
Mã phải làm được gì
- Không bao giờ trùng. Trùng CIF là hai người xài chung một danh tính ngân hàng, nghe thôi đã thấy toang.
- Không để lộ số liền trước hay liền sau.
- Mỗi loại khách một định dạng riêng, có prefix và độ dài riêng.
- Nhiều pod cùng cấp một lúc mà không phải chờ nhau.
Mỗi loại khách một pool
Mỗi loại khách có pool riêng, mỗi pool giữ Feistel seed, prefix và độ dài mã của riêng nó.
Sao lại là Feistel
Mạng Feistel biến một bộ đếm thành một con số đã xáo trộn, cùng kích thước. Nó là một hoán vị nên hai đầu vào khác nhau không bao giờ ra cùng một kết quả, và mã tự khắc không trùng mà khỏi cần dò bảng nào. Cấp một mã tốn thời gian cố định, không phải sinh trước, cũng chẳng phải hỏi database xem mã đã có ai dùng chưa.
// Simplified sketch, not the production code.
long permute(long counter, long domain, long[] roundKeys) {
long x = counter;
do {
x = feistel(x, roundKeys);
} while (x >= domain); // cycle-walk until the result fits the ID length
return x;
}
Lấy mã không cần xếp hàng
Các pod lấy mã bằng FOR UPDATE SKIP LOCKED của PostgreSQL. Pod này đang khoá một dòng thì pod kia bỏ qua và lấy dòng khác, nên các request onboarding chạy song song không phải đứng đợi nhau.
-- Simplified sketch.
SELECT id FROM cif_pool
WHERE customer_type = :type AND status = 'AVAILABLE'
ORDER BY id
LIMIT 1
FOR UPDATE SKIP LOCKED;
Giữ cho pool luôn đầy
Một cron job nạp thêm khi pool xuống dưới ngưỡng tối thiểu. Mỗi lần chạy bị giới hạn bởi max-batches-per-run, nên pool có cạn cũng không dội một cục insert vào database. Còn có một job thu hồi, nhặt lại những mã đã giữ chỗ mà pod lăn ra chết trước khi kịp dùng.
Theo dõi
Micrometer gauge báo độ sâu của từng pool, pool sắp cạn là có alert trước.
Chỉnh bảng nóng
Bảng cấp mã là bảng bận nhất trong luồng. Mình cấu trúc lại để giảm write amplification, và bỏ luôn một index chống trùng đã thành nút thắt throughput.
Kết quả
Các pool cấp khoảng 11.000 CIF mỗi ngày, chưa trùng lần nào.
- Java
- Spring Boot
- PostgreSQL
- Micrometer
- Kubernetes
Chi tiết đã được đơn giản hoá và không nêu tên khách hàng.