文中所述事情均是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個號碼本。
通過這樣調整以後,即便有時候有入口由於施工或其餘緣由臨時關閉,也只須要調整一下每一個入口的號碼本數量就能夠了。
通過一段時間的統計,小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的工做總算告一段落。
任何事都須要按實際狀況來分析處理,很差照搬。小A的最後一種方案是在項目中實際使用的,業務場景是限量開通超級粉絲卡。既然是限量, 便須要計數,便須要檢查能不能開卡 。在這個方案裏將計數和限量分紅了兩步來作,計數這一步經過分多個桶來保證併發容量,只要每一個桶的請求量差異不大,總的限量就能夠直接平分到每個桶的限量。這裏面,最關鍵的地方在於分桶的均勻。因爲是按用戶分桶,通用作法即是按id取模分桶,因爲用戶id是均勻的,分桶也就是均勻的。