來源:搜不狐,
sobuhu.com/program/2013/04/07/how-to-design-seckill.html
這篇文章已經很久了,當初的設想是單機編程,每臺服務器獲得自己能夠賣出多少產品,用戶被隨機分配到每臺機器上進行處理。
一、題目
1, 這是一個秒殺系統,即大量用戶搶有限的商品,先到先得
2, 用戶併發訪問流量非常大, 需要分佈式的機器集羣處理請求
3, 系統實現使用Java
二、模塊設計
1, 用戶請求分發模塊:使用Nginx或Apache將用戶的請求分發到不同的機器上。
2, 用戶請求預處理模塊:判斷商品是不是還有剩餘來決定是不是要處理該請求。
3, 用戶請求處理模塊:把通過預處理的請求封裝成事務提交給數據庫,並返回是否成功。
4, 數據庫接口模塊:該模塊是數據庫的唯一接口,負責與數據庫交互,提供RPC接口供查詢是否秒殺結束、剩餘數量等信息。
第一部分就不多說了,配置HTTP服務器即可,這裏主要談談後面的模塊。
用戶請求預處理模塊
經過HTTP服務器的分發後,單個服務器的負載相對低了一些,但總量依然可能很大,如果後臺商品已經被秒殺完畢,那麼直接給後來的請求返回秒殺失敗即可,不必再進一步發送事務了,示例代碼可以如下所示:
package seckill;
import org.apache.http.HttpRequest;
/**
* 預處理階段,把不必要的請求直接駁回,必要的請求添加到隊列中進入下一階段.
*/
public class PreProcessor {
// 商品是否還有剩餘
private static boolean reminds = true;
private static void forbidden() {
// Do something.
}
public static boolean checkReminds() {
if (reminds) {
// 遠程檢測是否還有剩餘,該RPC接口應由數據庫服務器提供,不必完全嚴格檢查.
if (!RPC.checkReminds()) {
reminds = false;
}
}
return reminds;
}
/**
* 每一個HTTP請求都要經過該預處理.
*/
public static void preProcess(HttpRequest request) {
if (checkReminds()) {
// 一個併發的隊列
RequestQueue.queue.add(request);
} else {
// 如果已經沒有商品了,則直接駁回請求即可.
forbidden();
}
}
}
併發隊列的選擇
Java的併發包提供了三個常用的併發隊列實現,分別是:
-
ConcurrentLinkedQueue
-
LinkedBlockingQueue
-
ArrayBlockingQueue
ArrayBlockingQueue是初始容量固定的阻塞隊列,我們可以用來作爲數據庫模塊成功競拍的隊列,比如有10個商品,那麼我們就設定一個10大小的數組隊列。
ConcurrentLinkedQueue使用的是CAS原語無鎖隊列實現,是一個異步隊列,入隊的速度很快,出隊進行了加鎖,性能稍慢。
LinkedBlockingQueue也是阻塞的隊列,入隊和出隊都用了加鎖,當隊空的時候線程會暫時阻塞。
由於我們的系統入隊需求要遠大於出隊需求,一般不會出現隊空的情況,所以我們可以選擇ConcurrentLinkedQueue來作爲我們的請求隊列實現:
package seckill;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.apache.http.HttpRequest;
public class RequestQueue {
public static ConcurrentLinkedQueue<HttpRequest> queue =
new ConcurrentLinkedQueue<HttpRequest>();
}
用戶請求模塊
package seckill;
import org.apache.http.HttpRequest;
public class Processor {
/**
* 發送秒殺事務到數據庫隊列.
*/
public static void kill(BidInfo info) {
DB.bids.add(info);
}
public static void process() {
BidInfo info = new BidInfo(RequestQueue.queue.poll());
if (info != null) {
kill(info);
}
}
}
class BidInfo {
BidInfo(HttpRequest request) {
// Do something.
}
}
數據庫模塊
數據庫主要是使用一個ArrayBlockingQueue來暫存有可能成功的用戶請求。
package seckill;
import java.util.concurrent.ArrayBlockingQueue;
/**
* DB應該是數據庫的唯一接口.
*/
public class DB {
public static int count = 10;
public static ArrayBlockingQueue<BidInfo> bids = new ArrayBlockingQueue<BidInfo>(10);
public static boolean checkReminds() {
// TODO
return true;
}
// 單線程操作
public static void bid() {
BidInfo info = bids.poll();
while (count-- > 0) {
// insert into table Bids values(item_id, user_id, bid_date, other)
// select count(id) from Bids where item_id = ?
// 如果數據庫商品數量大約總數,則標誌秒殺已完成,設置標誌位reminds = false.
info = bids.poll();
}
}
}
三、後續
這是幾年前寫的內容,比較粗糙,現在已經忘了幾種併發包的區別了,總結一下目前比較理想的實現方式:
-
用戶請求首先進入請求分發集羣,該集羣通過OCS等分佈式緩存初步判斷分發條件是否合適(本例中爲是否還有庫存,沒有庫存直接在這一步就停了)
-
分發集羣發現還有庫存,則將請求路由到負載較低的業務處理機器
-
業務處理機對分佈式緩存進行count–原子操作(或在分佈式緩存上自己實現CAS操作),如果成功則進入數據庫處理邏輯
-
數據庫正確的進行了count–操作,給用戶返回成功秒殺
-
如果數據庫操作沒有完成count–或者發現庫存竟然沒了,則觸發分佈式緩存重新加載數據庫庫存,同時返回給用戶秒殺失敗