文中所述事情均是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是均匀的,分桶也就是均匀的。