Mình đang code 1 website cá nhân, không đăng kí doanh nghiệp nên xài API chùa của bank để check lịch sử giao dịch rồi cộng tiền.
Ý tưởng là khách sẽ chuyển tiền vào bank mình theo cú pháp kiểu như:
- Số:
nap 0123456
, - Username:
nap username
, - Email:
nap [email protected]
- Id Mongodb:
nap 507f191e810c19729de860ea
Nhìn thôi cũng biết mình chọn cách nào rồi đúng không, email thì quá dài và 1 số bank không cho nhập kí tự đặc biệt, id mongodb thì đúng là thảm hoạ cho người dùng, username và số là ok nhất rồi.
Tuy nhiên collection bên mình không có field username
, mà chỉ có email
và _id
(mặc định của mongodb) là không trùng lặp, nên giờ mình cần tạo filed mới để cho user nạp tiền, ở đây mình đặt tên nó là payment_id
, field này type Number, tăng dần, unique . Xài Sql thì dễ rồi, cho auto inc số là xong, còn mongodb thì làm thế nào đây ?
Dưới đây là bản phác thảo mình viết để hệ thống lại. Bài toán trên của mình thì mình xài cách 1 dưới đây do số lượng User đăng kí đồng thời khá thấp và mức delay chấp nhận được. Các cách khác sau mình khi cần scale mình sẽ dùng tới nên code có thể chưa được clean nhé!
Atomic Update
Đúng như cái tên của của nó, findOneAndUpdate là giải pháp đầu tiên mà bất kì coder nào cũng nghĩ tới.
Tạo 1 collection PaymentCounter
có filed là count
.
Sau đó mỗi 1 lần user đăng kí mới, bạn sẽ
let{payment_id}=await PaymentCounter.findOneAndUpdate({},{$inc:{count:1}},{new:true});let user =newUser({
email,....
payment_id
});// update user and return payment_id here
Cơ mà với bài toán của mình, mình sẽ thêm 1 đoạn code nữa để khi thanh toán, nếu user không có payment_id
thì mình mới update payment_id
của user đó vào db.
Thực ra cũng có 1 cách nữa mình nghĩ tới đó là vứt luôn cái đống auto-inc unique mongodb này vào sọt rác, khi người dùng thanh toán thì mới bắt họ update username nếu chưa có là xong, nhưng thôi, thử thách bản thân 1 tí xem sao.
Rồi lại nói dông dài, thế cuối cùng là thử thách gì, có mỗi 1 cái findOneAndUpdate cũng lên bài. Nào my friend, bĩnh tĩnh….
Bây giờ web bạn đủ lớn thì cái giải pháp này chính là thứ bopdai web của bạn. Ví dụ giờ có 200 cái request đăng kí (hoặc user nạp lần đầu nếu bạn theo phương pháp của mình) post lên server 1 phát thì sao. Đúng rồi, cả 200 request này sẽ bị block mới lệnh atomic udpate findOneAndUpdate của bộ đếm.
Vẫn là Atomic Update nhưng trên multiple document
Ý tưởng đơn giản lắm, thay vì atomic trên 1 cái document thì giờ atomic trên nhiều cái thôi, ez game
Ví dụ mình tạo 10 bộ đếm là 10 doc, mỗi 1 doc đại diện cho 1 khoảng số, ví dụ từ 10.000
-> 19.999
, 20.000
-> 29.999
…. 90.000
-> 99.999
.
Khởi tạo bộ đếm
constNUM_OF_COUTER=10;constRANGING=1000;for(let i =1; i <=NUM_OF_COUTER; i++){await PaymentCounter.findOneAndUpdate({ id_counter: i},{
$setOnInsert:{
start_count: i*RANGING,
max_count:(i+1)*RANGING-1,
count: i*RANGING,}},{upsert:true})}
Chỗ dùng bộ đếm bây giờ sẽ đổi thành
let random =RandomIntMinMax(0,10);let{counter}=await PaymentCounter.findOneAndUpdate({id_counter: random},{$inc:{count:1}},{new:true});
Ơ, thế ví đụ đếm nó nó full luôn rồi thì sao, kiểu id_counter = 1 mà count nó lên
20.000
thì sao?
Dễ, thì reset bộ đếm qua 1 bộ đếm khác
let random =RandomIntMinMax(0,10);let id_counter = random;let{ start_count, count, max_count }=await PaymentCounter.findOneAndUpdate({ id_counter },{ $inc:{ count:1}},{new:true});if(count > max_count){let overflow_count = max_count - count;let old_start_count = start_count;
start_count =((old_start_count /RANGING)+NUM_OF_COUTER)*RANGING;({ start_count, count, max_count }=await PaymentCounter.findOneAndUpdate({ id_counter },{
$set:{
count: start_count + overflow_count,
start_count: start_count,
max_count: new_max_count
}},{new:true}));}
((old_start_count / RANGING) + NUM_OF_COUTER) * RANGING
là gì thế?
Ví dụ NUM_OF_COUTER=3
, RANGING=100
thì ta sẽ có
Lỡ có trường hợp có nhiều request cùng dùng 1 bộ đếm bị full đồng thời, thì nó có chạy ổn không? Thử tự đưa ra đáp án và lời giải thích nhé. (1)
Vẫn là Atomic Update trên multiple document nhưng thêm queue
Chỗ này ngắn ngọn dễ hiểu đối với ai đã biết cách dùng Queue (nếu chưa biết thì bạn có thể tham khảo qua các bài viết của anh @monmen nhé). Thay vì random thì đẩy nó vào queue, và tạo số lượng worker = bộ đếm mà bạn có, để phân bổ đều trên các bộ đếm. Chỗ reset bộ đếm khi full dễ dàng hơn (vì không cần lo nhiều request đồng thời cùng gọi đến 1 bộ đếm đã full).
routerCounterQueue.process(1,()=>{let router_count =await redis.incr('router_count');let job =await CouterQueue.add({id_counter: router_count %NUM_OF_COUTER});return job;})
CouterQueue.process(NUM_OF_COUTER,(job)=>{let{id_counter}= job.data;let{ start_count, count, max_count }=await PaymentCounter.findOneAndUpdate({ id_counter },{ $inc:{ count:1}},{new:true});if(count > max_count){let old_start_count = start_count;
start_count =((old_start_count /RANGING)+NUM_OF_COUTER)*RANGING;({ start_count, count, max_count }=await PaymentCounter.findOneAndUpdate({ id_counter },{
$set:{
count: start_count,
start_count: start_count,
max_count: new_max_count
}},{new:true}));}});
Random số ngẫu nhiên rồi kiểm tra xem đã tồn tại trong DB chưa
Một trong các cách mình nghĩ tới, Tuy nhiên chỉ phát huy tác dụng khi mà có dải số rộng, không phù hợp với bài toán của mình cho lắm.
Chuyển Atomic qua cho Redis và update vào DB
Cách này cũng khá đơn giản, thay vì mỗi 1 lần call couter mình lại gọi tới db dẫn tới thắt cổ chai, xử lí 1 lệnh trên db như vậy mất dỡ 40ms, nghĩa là 1 giây chỉ xử lí đc vài chục request. Thay vào đó mình sẽ chuyển bài toán tăng atomic qua cho redis nắm giữ và chỉ update vào database mỗi 10s.
Code chay thì đơn giản, tuy nhiên phải catch được lỗi và retry hoặc pause các queue có xử dụng tới router khi app mới khởi động, xử lí được việc chậm delay update từ reds vào mongodb, listen event redis tắt save vào db. Good luck ae!
Bài viết tới đây là hết rồi. Có lỗi lầm hay thiếu sót gì các bạn cứ góp ý nhiệt tình nhé.
Nguồn: viblo.asia