併發場景
-
秒殺
秒殺系統是可以籠統的稱為多使用者對同一資源發起請求,正確響應次數少於使用者請求量。此時最安全的做法是使用悲觀鎖,資料級層面的鎖,例如oracle的sql:select for update.但是悲觀鎖的缺點在高併發場景也是很明顯,就是允許的併發量低,容易造成504,就像安檢一樣,一次只能通過一個人,效率和體驗都十分低下。
所以應該使用樂觀鎖,或者利用redis的原子性做併發量限制,再使用mq進行任務分發。正常的流程:
使用者下單->redis併發庫存鎖,減少庫存->通過mq生產訂單任務->mq消費者消費任務,生成訂單以及更新庫存一系列操作。
-
樂觀鎖:
redis原生提供樂觀鎖 watch,watch是基於連結的,而主流nodejs裡面模組redis是基於pipeline做的,無連線池,所以,watch單單基於業務來說對於nodejs並無作用,只要正確利用好redis的原子性即可。
但是系統大了以後就會牽扯到叢集的問題,在多系統(多連結)的設計下,watch就尤為重要了,個人認為watch可以提供一個“次級操作”的空間,對於秒殺系統來說,庫存的更新與秒殺業務是可以同事存在的
例如:賣家在秒殺期間補充庫存、由於業務問題鎖住庫存等。這個時候watch可以提供優先順序,即當管理員鎖住庫存(清零)與多個買家發起秒殺同一時間發出請求,可以保證管理員的請求是正確通過的,而買家由於更新庫存,該次請求失效。const redis = Redis.createClient(); let lock = async function(key) { let transactionStatus = false; await redis.watchAsync(key); let stock = await redis.getAsync(key); if(+stock < 1) { //庫存不足的情況 } let reply = await redis.multi().decr(key).execAsync(); if(!reply) { // 當事務失敗的時候reply為null,進行錯誤處理 } else if(reply[0] < 0) { // 當事務成功的時候返回array,multi可理解為Promise.all相當 // 健壯處理超賣情況,此時應該補redis的庫存避免以後因為負數庫存導致以後補充庫存出錯,並且與事務失敗執行一致操作 redis.incr(key); } else { // 事務成功且正確減少庫存的時候 transactionStatus = true; } return transactionStatus; }; let produceOrder = function(orderId,userId){ let payload = JSON.stringify({ orderId, userId }); let productor = new Productor(); return productor.produce(TOPIC.ORDER,payload); }; let buy = async function(orderId,userId){ let key = `lock:order:${orderId}:${userId}`; let lockResult = await lock(key); if(lockResult) { await produceOrder(orderId,userId); } return Promise.resolve(); };
-
-
關注
關注併發可以作為同一使用者對於同一資源重複請求的代表,例如:在網路差的時候,使用者點選關注某人的時候由於相應過慢,同時發出了多次請求。這個相當於是過濾無用請求,客戶端需要做相應處理,但是一個健壯的後臺,也必須要考慮這種情況。
-
正規化設計的資料庫可以利用設定唯一聯合索引來避免
-
可利用redis的set、hash、bitmap等資料結構做去重
-
如果是反正規化設計(類似mongo的內嵌陣列設計),可利用db層上($in、$addToSet等操作符)做去重
-
使用悲觀鎖,從邏輯層做去重
可以利用redis的原子性或setex操作來完成悲觀鎖:
const redis = Redis.createClient(); let checkLock = async function(key){ let ttl = await redis.ttlAsync(key); if(+ttl === -1) { //如果ttl為-1,為無過期時間,立即設定過期時間避免死鎖 redis.expire(key, 10) } return Promise.resolve(); }; let lock = async function(key){ let result = await redis.incrAsync(key); if(+result !== 1) { checkLock(key) //已被鎖住,進行錯誤處理 } else { //成功上鎖,設定過期時間避免死鎖 redis.expire(key, 10); } return Promise.resolve(); }; let releaseLock = function(key){ return redis.setAsync(key,0) }; let follow = async function(followId,userId){ let key = `lock:follow:${followId}:${userId}`; try { await lock(key); // 關注邏輯:查詢、遍歷、插入等 } catch(err) { // 錯誤處理 } finally { await releaseLock(key) } };
-