妹纸小A的计数工作

文中所述事情均是YY。

小A是一个呆萌的妹纸,最近刚刚加入小B的团队,这几天小B交给她一个任务,让她每天统计一下团队里九点半之前到公司的人数。


九点半之前到公司人数

于是,每天早上小A都早早来到公司,然后拿一个本子来记,来一个人就记一下。 [1]

这里,其实小A的做法和下面的代码一样:

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只要瞅一眼本子上记录的人数就能立马回答了。

过了几天,小A发现,同学们上班的时候不是都一个一个来的,有的时候一下子同时来了好几个人,就会有漏记下的,该怎么解决这个问题呢?

小A想了一个办法,她让来的同学们一个一个等她记下来了,再到自己的位子上去。这么做以后再也没有出现过漏记的情况了。[2]

小A的这个办法就是加了一个锁,只能一个个串行的来:

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的做法,时间长了,很多同学就有意见,同学们都不想花时间在等记名字上面。

小A只得改变一下方法,她在每个入口处都放置了一个盒子,让同学来了后自己把名字写在小纸片上,然后放到盒子里,小A数一下盒子里的小纸片数量就能知道来了多少人。[3]

这种做法类似于在数据库里插入几条记录,统计的时候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只需要把几个入口的号码本上待撕的数字加一下就能得到总数了。[4]

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在各个入口处设置了不同数量的号码本,果然问题解决了。[5]

现在小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名发咖啡券就可以了。[6]

代码描述相比之前也只是有很小的改动:

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张咖啡券分配到每个号码本,撕下一个号码的时候拿一张咖啡券。如果一个号码本对应的咖啡券已经被领完了,就从别的地方调咖啡券过来。如果所有的咖啡券已经发完了,那么就设置一个标志,后来的人都没有咖啡券可以领了。[7]

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);
    }
}

发放咖啡券必须得是先到先得,如果用Pim表示取第i个号码本上号码m的人撕下号码的时间,Cim表示其是否取得咖啡券(1代表获得,0代表未获得),那么先到先得可以这么来表述:

?m > n → Pim > Pin,

? m > n, Cim = 1 → Cin = 1

上面的代码服从这两条约束。

咖啡券发了一段时间后,同学们来公司的时间都比以前早了,各个地方的咖啡券基本上都在同一时间发完,根本就不存在从别的地方调咖啡券的情况。[8]

在各个号码本号码消耗速率保持一致的情况下,小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是均匀的,分桶也就是均匀的。

时间: 2024-10-13 16:19:28

妹纸小A的计数工作的相关文章

一个特别适合新手练习的Android小项目——每日一妹纸

介绍 每天更新一张精选妹纸图片,第一版目前已完成,本项目会持续更新,遇到任何问题欢迎与我联系^_^ 为什么说这是一个特别适合新手练习的小项目? 服务器API接口功能丰富且无访问次数限制 包含了常见的网络通信,数据缓存等功能 使用了流行的Realm,Retrofit,Glide,Butterknife等开源项目,方便新手学习他们的使用 遵循Material Design规则 示例 项目当然是开源的啦,源码请戳下面的链接 https://github.com/SparkYuan/Meizi ----

关于快速开发:网红妹纸,给了我一个idea!

我是一名软件公司员工,从事撸码事业也有一段时间了.码代码已经成了我生活的一部分,我的撸码生活几乎一成不变.直到有一天,写字楼搬来了一家网红公司! 网红公司,什么是网红公司,在座的肯定都懂啦~就是那种漂亮妹妹很多的公司了!可别说我们公司那群屌丝多开心了,连我们公司只知道码代码的撸码狂人-逼哥(绰号),都开始频繁地上厕所了!吃完饭都要去走廊走几次,还一本正经的说,这叫散步,时不时还要讨论下人家的三围啦.长相啦.像我这种正经人,就不会像逼哥这么低俗了!我也就看过那么几次而已了,不像他们那么猴急,早就连

新来的长腿程序猿妹纸

新来的刚毕业的妹纸有一双大长腿,于是她在公司年会出尽风采,公司的一些男生老员工没事献献殷勤,但很奇怪,没有很久,都又消失了- 新来的妹纸喜欢穿着破洞的牛仔裤,在办公室插着耳麦,一副自以为很酷的样子,老员工都按部就班穿着上班装,突然一个标新立异的出现,觉得特别青春,好像回到了自己刚毕业的时候- 新来的妹纸特别有活力,天天在加班,工作积极,一片朝气,而老员工多了一些暮色晨晨的气息,工作干完了就走,尽量给自己手头的工作不犯错,然后争取年中末考核有个好的绩效,奖金拿多点- 新来的刚毕业的妹纸特别喜欢学习

利用WiFi钓鱼法追邻居漂亮妹纸

假设,你的邻居是一个妹纸.漂亮单身,你,技术狗,家穷人丑,集体户口.像借酱油这种老套搭讪方式的成功率对你来说实在很低. 你要做的是了解她,然后接近她.通过搜集更多的情报,为创造机会提供帮助. 初级情报搜集 这个没技术含量.人人可用. 一个人只要活在世上,就会留下痕迹.痕迹当中蕴含着情报,专业的情报人员都有着无比的好奇心和洞察力. 她会把垃圾袋放在门口,等出门的时候扔掉. 夜深人静,你悄悄的把垃圾袋偷走,找到了里边的快递包裹.于是你得到了她的电话号码和名字. 是假名怎么办?通过手机充值软件,可以通

程序猿,你们这么拼是找不到妹纸的!

原文:http://www.ido321.com/378.html 所有健康bug都被程序猿们承包了 据说新时代的攻城狮和程序猿要具备以下素质:去得了公司,回得了厨房:不惊动腾讯,不激怒同行:写得了代码,查得出异常:做得出产品,看得准市场:接触过VC,见识过投行.怎么样,是不是有点上天入地的赶脚? 在这个时代,互联网和数码产品发展更新得有多快,攻城狮和程序猿的生活节奏就有多快.他们在每个夜深人静的晚上熬夜奋战,炯炯有神地与代码和bug做斗争.电脑三天三夜不关机?连续上班72小时?在突击任务的时候

拥抱ARM妹纸第二季 之 第一次 点亮太阳

上次做鱼缸LED灯时还有很多材料正好拿来用.穆等等哥- 俺去找材料. 材料列表     3W LED   x  3     散热片     x  1     恒流IC     x  1     其他零件  ... ... 注意哦,大功率LED那叫个热啊.一定不要忘记把这个东东绑在散热片上,否则这小家伙会把自己的脑袋给烧掉. "CPU散热器行吗" "当然,只要能散热的都行." 小穆,帮忙摆摆整齐.笑一笑,茄子---! 再来一张 哦--- 一堆乱七八糟的东西! 自己动手

音乐出身的妹纸,零基础学习JAVA靠谱么

问:表示音乐出身的妹纸一枚  某一天突然觉得身边认识的是一群程序员   突然想 要不要也去试试... 众好友都觉得我该去做个老师,可是我怕我会误人子弟,祸害祖国下一代..... 要不要 要不要 学Java去..这是不是一条不归路 ... 答:初级应用编程没什么难的 , 只要数学方面不是特别渣, 思维逻辑不混乱,基本上是可行的, 至于其他的 ,没什么可考虑的 , 听得再多,不如向写一波程序 . 你的担忧不无道理,人最怕的就是失去热情.老师的发展空间小且多数属于事业单位 ,在选择和个人成长性上可能较

拥抱ARM妹纸第二季 之 第二次 约会需要浪漫,这么大灯泡怎么弄?

终于轮到俺的小穆出场啦.有请能让太阳也为之暗淡的小穆闪亮登场-,鼓掌吧,欢呼吧!-- ?? We can burn brighter Than the sun ~~~ ?? "谢谢---" 唱的太棒啦,再来首--  再来首-- "谢谢大家,为大家表演<遮天蔽日>魔术" !@%&--¥%!@!@#--¥@#¥%@#¥!@!!%%--¥%--&¥(咒语) ... .... 场内一片漆黑,只看到3个省略号--- 番茄或鸡蛋可以丢,硬东西可乐罐之

Python学习笔记(五十)爬虫的自我修养(三)爬取漂亮妹纸图

import random import urllib from urllib import request import os ######################################################### # 参数设置 wsp = 'DouziOOXX' # 打开连接 def url_open(url): req = urllib.request.Request(url) req.add_header('User-Agent', 'Mozilla/5.0