妹紙小A的計數工做

文中所述事情均是YY。數據庫

小A是一個呆萌的妹紙,最近剛剛加入小B的團隊,這幾天小B交給她一個任務,讓她天天統計一下團隊裏九點半以前到公司的人數。併發


九點半以前到公司人數

因而,天天早上小A都早早來到公司,而後拿一個本子來記,來一我的就記一下。 <sup>[1]<sup>dom

這裏,其實小A的作法和下面的代碼同樣:code

public class SimpleCounter1 {
    List<CheckRecordDO> counter = new LinkedList<CheckRecordDO>();
    public void check(long id) {
        CheckRecordDO checkRecordDO = new CheckRecordDO();
        checkRecordDO.setId(id);
        counter.add(checkRecordDO);
    }
    public int count() {
        return counter.size();
    }
}

每當小B問有多少人已經來了的時候,小A只要瞅一眼本子上記錄的人數就能立馬回答了。get

過了幾天,小A發現,同窗們上班的時候不是都一個一個來的,有的時候一會兒同時來了好幾我的,就會有漏記下的,該怎麼解決這個問題呢?it

小A想了一個辦法,她讓來的同窗們一個一個等她記下來了,再到本身的位子上去。這麼作之後再也沒有出現過漏記的狀況了。<sup>[2]<sup>event

小A的這個辦法就是加了一個鎖,只能一個個串行的來:class

public class SimpleCounter2 {
    final List<CheckRecordDO> counter = new LinkedList<CheckRecordDO>();
    public void check(long id) {
        synchronized (counter) {
            CheckRecordDO checkRecordDO = new CheckRecordDO();
            checkRecordDO.setId(id);
            counter.add(checkRecordDO);
        }
    }
    public int count() {
        return counter.size();
    }
}

但是好景不長,開始幾天同窗們還能接受小A的作法,時間長了,不少同窗就有意見,同窗們都不想花時間在等記名字上面。date

小A只得改變一下方法,她在每一個入口處都放置了一個盒子,讓同窗來了後本身把名字寫在小紙片上,而後放到盒子裏,小A數一下盒子裏的小紙片數量就能知道來了多少人。<sup>[3]<sup>List

這種作法相似於在數據庫裏插入幾條記錄,統計的時候count一下:

public class SimpleCounter3 {
    private CheckRecordDAO checkRecordDAO;
    public void check(long id) {
        CheckRecordDO checkRecordDO = new CheckRecordDO();
        checkRecordDO.setId(id);
        do {
            if (checkRecordDAO.insert(checkRecordDO)) {
                break;
            }
        } while(true);
    }
    public int count() {
        return checkRecordDAO.count();
    }
}

雖然有時候一塊兒來的時候人不少,但只須要增長一下盒子的數量,也不會產生擁堵的狀況了,小B對小A的方案很滿意。

小A使用盒子的思路,就至關於創建分庫分表機制,增大並行數量,解決擁堵。

因爲小A的計數工做完成的很是出色,因而,其餘團隊的計數工做也都移交到小A這邊了。呆萌的小A本來只須要統計二十幾號人,如今一會兒增長到了幾百號人。小A每次數盒子裏的小紙片數量都須要花費比較長的時間,頓時,呆萌的妹紙又陷入了淡淡的憂傷當中。

這時候旁邊的小C站了出來,對小A說,其實小B並不關心到底來了哪些人,只須要知道來了多少人就能夠了。

小A一會兒明白過來,立馬改進了方法,在每一個入口設置了一個號碼本,每個同窗來的時候撕下一個號碼,小A只須要把幾個入口的號碼本上待撕的數字加一下就能獲得總數了。<sup>[4]<sup>

public class ParallelCounter1 {
    final int ENTRY_COUNT = 5;
    Counter[] counter;
    {
        counter = new Counter[ENTRY_COUNT];
        for (int i = 0; i < ENTRY_COUNT; i++) {
            Counter c = new Counter();
            c.value = 0;
            counter[i] = c;
        }
    }
    public void check(int id, int entry) {
        synchronized (counter[entry]) {
            counter[entry].value++;
        }
    }
    public int count() {
        int total = 0;
        for (int i = 0; i < ENTRY_COUNT; i++) {
            total += counter[i].value;
        }
        return total;
    }
    public class Counter {
        public int value;
    }
}

不幸的是,問題仍是來了,因爲每一個入口進來的人數不一致,有些入口的號碼本很容易早早用完,另一些入口卻還剩下很多。

小C是一個熱心的man,這時候又站出來了,他說,既然各個入口的人數不同,那麼按照人數比例設置號碼本數量不就能夠了麼。因而小A在各個入口處設置了不一樣數量的號碼本,果真問題解決了。<sup>[5]<sup>

如今小A的作法和下面的實現同樣,每個entry有不一樣數量的counter,每一個員工check的時候隨機選擇一個counter:

public class ParallelCounter2 {
    final int COUNTS = 16;
    final int ENTRY_COUNT = 5;
    Counter[] counter;
    Integer[][] entryCounter = {
            {0,0},
            {1,1},
            {2,3},
            {4,7},
            {8,15}
    };
    {
        counter = new Counter[COUNTS];
        for (int i = 0; i < COUNTS; i++) {
            Counter c = new Counter();
            c.value = 0;
            counter[i] = c;
        }
    }
    public void check(int id, int entry) {
        int idx = choose(entry);
        synchronized (counter[idx]) {
            counter[idx].value++;
        }
    }
    private int choose(int entry) { // 隨機選擇入口處的一個號碼本
        int low = entryCounter[entry][0];
        int high = entryCounter[entry][1];
        return low + (int)Math.floor(Math.random() * (high - low + 1));
    }
    public int count() {
        int total = 0;
        for (int i = 0; i < COUNTS; i++) {
            total += counter[i].value;
        }
        return total;
    }
    public class Counter {
        public int value;
    }
}

按代碼所示: 總共有5個入口,使用了16個號碼本。

通過這樣調整以後,即便有時候有入口由於施工或其餘緣由臨時關閉,也只須要調整一下每一個入口的號碼本數量就能夠了。


前20人送咖啡券

通過一段時間的統計,小B發現,大多數時候九點半前到公司的人數都不超過20我的。怎麼才能讓你們早點來公司呢?小B想了一個辦法,天天前20個來公司的人送咖啡券。

小A想,要給前20我的發咖啡券,那隻要記下每一個人來的時間,給最先的前20我的發就能夠了。能夠用以前放置盒子的方法,讓每一個來的人寫下本身的名字和來的時間(價值觀保證寫的時間是真實的,-_-),最後按時間統計出前20名發咖啡券就能夠了。<sup>[6]<sup>

代碼描述相比以前也只是有很小的改動:

public class SimpleCounter4 {
    private CheckRecordDAO checkRecordDAO = new CheckRecordDAO();
    public void check(long id) {
        CheckRecordDO checkRecordDO = new CheckRecordDO();
        checkRecordDO.setId(id);
        checkRecordDO.setTime(new Date());
        do {
            if (checkRecordDAO.insert(checkRecordDO)) {
                break;
            }
        } while(true);
    }
    public int count() {
        return checkRecordDAO.count();
    }
    public void give() {
        checkRecordDAO.updateStatusWithLimit(1,20);
    }
}

小B以爲,過後發放咖啡券不如即時發放效果好,讓小A在同窗們來的時候就發。小A一會兒又陷入了淡淡憂傷當中。若是隻有一個入口的話,能夠把咖啡券和號碼本放在一塊兒,讓同窗們來的時候本身拿一張,而如今有好幾個入口,每一個入口來的人數都不固定,無論怎麼分,均可能會形成一個入口已經沒得發了,另外的入口還有。

想來想去,小A仍是沒有想到什麼好辦法,難道要回到最初,一個一個來登記而後發券?

小A從新梳理了一下發咖啡券的需求,發券的方式要麼一個一個發,要麼不一個一個發。確定不要用以前串行的辦法,仍是得往同時發的方面考慮。按照以前的思路,在幾個入口同時都放,將20張咖啡券分配到每一個號碼本,撕下一個號碼的時候拿一張咖啡券。若是一個號碼本對應的咖啡券已經被領完了,就從別的地方調咖啡券過來。若是全部的咖啡券已經發完了,那麼就設置一個標誌,後來的人都沒有咖啡券能夠領了。<sup>[7]<sup>

public class ParallelCounterWithCallback3 {
    final int TOTAL_COFFEE_COUPON = 20;
    final int COUNTS = 16;
    final int ENTRY_COUNT = 5;
    Counter[] counter;
    boolean noMore = false;
    final Integer[] coffeeCoupon = new Integer[COUNTS];
    final Integer[][] entryCounter = {
            {0,2},
            {3,8},
            {9,10},
            {11,12},
            {13,15}
    };
    {
        counter = new Counter[COUNTS];
        for (int i = 0; i < COUNTS; i++) {
            Counter c = new Counter();
            c.value = 0;
            counter[i] = c;
            coffeeCoupon[i] = (TOTAL_COFFEE_COUPON / COUNTS); // 平分
            if (i < TOTAL_COFFEE_COUPON % COUNTS) {
                coffeeCoupon[i] += 1;
            }
        }
    }
    public void check(int id, int entry, Callback cbk) {
        int idx = choose(entry), get = 0;
        synchronized (counter[idx]) {
            if (coffeeCoupon[idx] > 0) {
                get = 1;
                coffeeCoupon[idx]--;
                counter[idx].value++;
            } else {
                if (!noMore) { // 其餘地方還有咖啡券
                    for (int i = 0; i < COUNTS && get == 0; i++) {
                        if (idx != i && coffeeCoupon[i] > 0) { // 找到有券的地方
                            synchronized (counter[i]) {
                                if (coffeeCoupon[i] > 0) {
                                    get = 1;
                                    coffeeCoupon[i]--;
                                    counter[idx].value++;
                                }
                            }
                        }
                    }
                    if (get == 0) noMore = true;
                }
                if (noMore) counter[idx].value++;
            }
        }
        cbk.event(id, get);
    }
    private int choose(int entry) { // 隨機選擇入口處的一個號碼本
        int low = entryCounter[entry][0];
        int high = entryCounter[entry][1];
        return low + (int)Math.floor(Math.random() * (high - low + 1));
    }
    public int count() {
        int total = 0;
        for (int i = 0; i < COUNTS; i++) {
            total += counter[i].value;
        }
        return total;
    }
    public class Counter {
        public int value;
    }
    public interface Callback {
        int event(int id, int get);
    }
}

發放咖啡券必須得是先到先得,若是用P<sub>im</sub>表示取第i個號碼本上號碼m的人撕下號碼的時間,C<sub>im</sub>表示其是否取得咖啡券(1表明得到,0表明未得到),那麼先到先得能夠這麼來表述:

∀m > n → P<sub>im</sub> > P<sub>in</sub>,

∃ m > n, C<sub>im</sub> = 1 → C<sub>in</sub> = 1

上面的代碼服從這兩條約束。

咖啡券發了一段時間後,同窗們來公司的時間都比之前早了,各個地方的咖啡券基本上都在同一時間發完,根本就不存在從別的地方調咖啡券的狀況。<sup>[8]<sup>

在各個號碼本號碼消耗速率保持一致的狀況下,小A所須要作的事情也獲得了簡化,只要平分咖啡券到每一個號碼本就好了,甚至各個號碼本分到的咖啡券數量都不須要預先分配,對應的代碼以下:

public class ParallelCounterWithCallback4 {
    final int TOTAL_COFFEE_COUPON = 20;
    final int COUNTS = 16;
    final int ENTRY_COUNT = 5;
    Counter[] counter;
    final Integer[][] entryCounter = {
            {0,2},
            {3,8},
            {9,10},
            {11,12},
            {13,15}
    };
    {
        counter = new Counter[COUNTS];
        for (int i = 0; i < COUNTS; i++) {
            Counter c = new Counter();
            c.value = 0;
            counter[i] = c;
        }
    }
    public void check(int id, int entry, Callback cbk) {
        int idx = choose(entry), get = 0;
        synchronized (counter[idx]) {
            if (counter[idx].value < coupon(idx)) {
                get = 1;
            }
            counter[idx].value++;
        }
        cbk.event(id, get);
    }
    private int coupon(int idx) {
        int c = (TOTAL_COFFEE_COUPON / COUNTS); // 平分
        return idx < TOTAL_COFFEE_COUPON % COUNTS ? c + 1 : c;
    }
    private int choose(int entry) { // 隨機選擇入口處的一個號碼本
        int low = entryCounter[entry][0];
        int high = entryCounter[entry][1];
        return low + (int)Math.floor(Math.random() * (high - low + 1));
    }
    public int count() {
        int total = 0;
        for (int i = 0; i < COUNTS; i++) {
            total += counter[i].value;
        }
        return total;
    }
    public class Counter {
        public int value;
    }
    public interface Callback {
        int event(int id, int get);
    }
}

好吧,小A的工做總算告一段落。


小Z

任何事都須要按實際狀況來分析處理,很差照搬。小A的最後一種方案是在項目中實際使用的,業務場景是限量開通超級粉絲卡。既然是限量, 便須要計數,便須要檢查能不能開卡 。在這個方案裏將計數和限量分紅了兩步來作,計數這一步經過分多個桶來保證併發容量,只要每一個桶的請求量差異不大,總的限量就能夠直接平分到每個桶的限量。這裏面,最關鍵的地方在於分桶的均勻。因爲是按用戶分桶,通用作法即是按id取模分桶,因爲用戶id是均勻的,分桶也就是均勻的。

相關文章
相關標籤/搜索