JAVA 如何設計一個秒殺系統

來源:搜不狐,

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–或者發現庫存竟然沒了,則觸發分佈式緩存重新加載數據庫庫存,同時返回給用戶秒殺失敗

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章