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ì

Các pool CIF ráp với nhau thế nàoCác pod onboarding lấy mã từ pool của từng loại khách trong PostgreSQL bằng SKIP LOCKED. Cron nạp thêm mã sinh bằng Feistel theo từng lô có giới hạn, job thu hồi trả lại mã pod chết bỏ dở, gauge độ sâu pool bắn alert.Onboardingkhách mớiPod onboardingPool CIFloại Aloại Bloại Ctrốngđang giữđã dùngAlertpool sắp cạnFeistelseed · prefix · độ dàiCron nạptheo lô có giới hạnJob thu hồimã bị bỏ dở123lấy mãSKIP LOCKED
1. Pod lấy một mã trống mà không phải chờ pod khác. 2. Cron nạp thêm khi pool xuống dưới ngưỡng. 3. Job thu hồi trả lại mã mà pod chết đã giữ nhưng chưa kịp dùng.

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.

Một mã CIF ra đời thế nàoBộ đếm đi qua các vòng Feistel của pool. Ra ngoài khoảng mã thì đi thêm vòng nữa, rồi ghép prefix và độ dài cố định.Bộ đếmsố n kế tiếpVòng Feistelkhoá bằng seedTrong khoảng?không thì đi tiếpĐịnh dạngprefix + độ dàiCIFkhông trùngcycle-walkCùng đầu vào luôn ra cùng kết quả, và hai đầu vào không bao giờ đụng nhau.
Không trùng là nhờ hoán vị, nên chẳng cần dò bảng nào.
// 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.

Chi tiết đã được đơn giản hoá và không nêu tên khách hàng.