201901工作复盘

前言

过去的一个月公司事情比较多,笔者负责一个版本的更新,这两天大致完成了.这次是真正意义上的自己负责一个版本(当然,有大佬review代码),压力蛮大的,整个过程犯了不少错误,也尝试将学过的新知识应用在项目上,总体来说收获颇丰.

出现了哪些问题?

数据一致性很重要

统计用户每个周期的数据,并且前端也有展示每天/周期的数据的需求

最初的想法

笔者的最初想法,首先肯定需要一张周期记录表,由于前端有展示需求,那就需要一张每日记录表,两者表的关键字段相同,笔者选择了下面这个方案

  1. 每当有数据更新时,只更新每日记录表
  2. 设置一个定时任务,每天0点把前一天的每日记录表统计数据增量到周期记录表上

选择这个方案有下面几点好处

  1. 避免了频繁更新,虽然每日记录表每当有数据就要更新一次,但是周期记录表每天只需要更新一次
  2. 减少了mysql锁竞争问题,因为可能有大量数据同时到达,对周期表的更新操作会导致大量等待
  3. 数据更新部分的代码会简单很多(这会导致其他部分的代码更复杂,比如需要结合周期表和每日表计算后获取当期的实时数据)
有哪些问题

不得不说大佬看问题就是一针见血,review之后指出了下面的这几个问题

  1. 如果服务器出现异常,定时任务不一定会执行(这很常见),所以不能依赖于定时任务,如果定时任务未执行,周期表的数据就会出现异常,也就是数据不一致了,致命缺陷
  2. 周期表的语义有问题,从字面理解,周期表统计的就是从周期开始到周期结束(如果还没到周期结束就是到现在)的数据,按照我的实现,语义是从周期开始到昨天的,致命缺陷

两个问题都很严重,也是我之前没有考虑过的问题,结合大佬的建议,最终的实现使用了下面的方案
每当有数据更新时,同时更新每日和周期记录表,这样更新操作会比较频繁,mysql锁竞争也会很多,但是可以确保数据一致且表的语义正确

简化问题=抽象+一点点技巧

有两种类型的用户,一种类型的用户可能转换为另一种类型,需要存储用户列表并区分他们(存储在redis中,zset)

最直观的做法,类型区分

笔者的想法时,添加一个类型字段,用户存储时根据类型存储到不同的列表,这个思路很简单,前期做起来也简单.一个很严重的问题是:

对于一种类型的用户可能转换为另一种类型这个需求需要将用户从一个列表中移除并添加到另一个列表中,这需要也必须是一个原子操作,考虑到现有业务的复杂性以及对列表操作的频繁性,实现起来会很复杂,这种实现很很容易发生冲突,导致用户从一个列表中被移除但没有被加入另一个列表中

抽象,抽象,抽象

回到需求,两种类型的用户只是产品提出的,那么程序中实现就必须要分为两个类型吗,可能通过其他方案解决吗? 当然可以了,前面说过用户列表是存储在redis中的zset的,我这里正好可以通过score值来区分用户类型,这样用户类型切换时只需要改下score即可,不会有并发问题,也不需要存储两个列表,问题大大简化了

尽可能的精准

实时人数统计

毫无疑问,用redis来存储.由于程序中肯定会有些异常情况,导致人数统计出现偏差,那么就需要定时重置(选择在线人数最少的时候)一下人数.请教大佬之后,意识到这种方案有缺陷,定时重置是需要一定时间的,这段时间人数还是在实时变动的,这会导致一定的偏差,上面说了是在在线人数最少时重置,原想着这点误差无所谓,大佬一句既然可以做的更精准,为什么不做呢,是啊,既然可以做到,为什么不做呢.于是就有了下面这个方案

  1. 同时维护两个key,一个记录总人数,另外一个记录单位(比如一个分区,房间)总人数,两个key同时更新
  2. 定时任务时先重置记录单位总人数,再使用记录单位总人数去统计总人数(为了这个过程尽可能快,我们用了lua脚本).

记录单位总人数这个key只在定时任务执行的那一小段时间有意义,使用它去避免这一小段时间的误差

这么多问题,真让人头疼

这些问题都很严重,不得不改,修复这些问题导致完成时间比预期晚了三四天,修改过程也很痛苦,毕竟要摒弃自己原有的思路改用另一种思路,整个过程中也发现即使下定决心使用另外一种思路,还是会受到原有思路的影响,比如有一张表写到最后才意识到在现有设计中有一个字段根本没有用途,而这个字段是在原有思路下才有用的.这也提醒了自己开始编码前要尽可能的回顾自己的设计思路,确保设计上没有偏差

做了哪些尝试呢?

这次做之前正好看了《Java 8 in Action》,读过之后收益良多,这次便尝试用modern java来写代码,也小小的试了下设计模式.

lambda的魔力

模板方法

在java8之前,使用模板方法很繁琐,你需要定义一个父类封装通用逻辑,再定义子类实现自定义的代码,在这种情况下,模板方法太鸡肋了,用了它代码可能更复杂并且更多了,但是在lambda的加持下,模板方法焕发出新的活力,下面以加锁获取一个资源T,在对T进行一些操作后,释放锁为例


public void lockMethodTemplate(args, Consumer<T> consumer){
try{
    //加锁
    lock.lock();
    //获取需要的资源        
    T t=...       
    consumer.accept(t) 
}finally {
//解锁
lock.unlock();
    }
}

这样不需要再去定义父类子类什么的,只需要传递一个lambda,里面包含你要执行的逻辑即可,很简洁有没有

行为参数化

这个有点抽象,举个例子,有一个列表,你需要根据不同的条件进行过滤,这里明显是可以封装的,但是要用到继承体系,麻烦,有了lambda,你可以这样做

public List<Integer> filterElements(List<Integer> sources,Predicate<Integer> predicate){
    return sources.stream().filter(e->predicate.test(e));
}

可以这样调用

//过滤得到>3的列表
filterElements(sources,(e)->e>3);
//过滤得到<1的列表
filterElements(sources,(e)->e<1);

这也是行为参数化的含义,将你要做的行为当做参数

Stream的高效使用

用Stream来对列表进行操作平时就用的很多,这次尝试了一些对我来说新的,高级的api

分割列表

根据给定条件将列表分割为两部分

举个例子,以>3为界限,将列表分成两部分,以前,对于这种需求我是这么做的

List<Integer> biggers=sources.stream().filter(e->e>3).collect(toList());
List<Integer> smallers=sources.stream().filter(e->e<=3).collect(toList());

可以看到做了两次过滤,效率很低.有了partitioningBy,可以这样做

Map<Boolean,List<Integer>> map=sources.stream().collect(partitioningBy(e->e>3));
List<Integer> biggers=map.get(true);
List<Integer> smallers=map.get(false);

只需要一次即可完成分割,当然Map有点不够直观,可以利用上面提到的参数行为化再封装一下


public static <V> TwoTuple<List<V>, List<V>> partitionBy(Collection<V> collection, Predicate<V> predicate{       
Map<Boolean, List<V>> partitionMap=collection.stream().collect(Collectors.partitioningBy(predicate));
return new TwoTuple<>(partitionMap.get(true), partitionMap.get(false));
}
分组并执行自定义统计操作

根据给定条件分组并统计每组的数量

举个例子,统计每种型号手机的数量

//型号,数量
Map<Type,Long> countMap=phones.stream().collect(groupBy(Phone::getType),counting()));

这里的counting可以修改成任何你需要的行为,simple and powerful.

Optional yes!

Optional取代null

null判断很啰嗦,对于一部分操作,使用Optional来取代null判断很有用,举个例子,获取一个列表,如果为空,返回一个空列表,如果不为空进行包装
如果使用null判断会是这样

if( sources==null||sources.isEmpty() ){
    return new ArrayList();
}else{
    return sources.stream.map(...).collect(toList());
}

如果用了Optional


// Collections.emptyList()是不可修改的,是一个静态对象,算是一个小小的优化.对于上面的例子,由于不确定上层会不会对返回的列表进行操作,只能使用 new ArrayList();
Optional.ofNullable(sources).orElse(Collections.emptyList())
.stream().map(...).collect(toList());

可以看到使用了Optional,不用再if else判断了,阅读起来也更流畅,也可以说是从命令式编程声明式编程的转变

Optional 作为返回值

如果一个方法可能返回null,那就可以用Optional进行一次封装,考虑一下这个场景,如果方法返回不为空,就执行一些操作
按照以前的写法会是这样

public Long getLong(){
...
}
//使用时
Long val=getLong();
if(val!=null){
...
}

如果我们使用Optional,可以这样做

public Optional<Long> getLong(){
...
}
//使用时
Optional<Long> optVal=getLong();
optVal.ifPresent(val->{...});

这里并没有简化代码,但是这个方法的语义更清楚了,返回值不一定存在,更难被误用

总结

这次任务,其实没有达到自己的预期效果,犯了太多设计上的错误,自己也反思过,这些错误提炼一下可以总结为

  1. 对需求研究不够透彻,有些隐藏含义没有get到,导致设计上就有偏差
  2. 缺少总结,有些问题以前是遇到过的,但是没有总结,时间一长就忘记了,好记性不如烂笔头啊
  3. 每天开始编码前,没有再去理一遍思路(说实话,东西太多,全理一遍不太现实,但是能部分理一下也会好很多),只是照着现有逻辑继续写,也就出现写到最后写了一些不需要的逻辑
  4. 表达能力不太够,code review搞得大家不知所云,这点任重而道远了
  5. 不要因为怕麻烦而选择简单的方案,该做的总是要做的,等做完之后你才会意识到当初那个麻烦的方案才是最简单的.

自我批判完了,这次也是有很多意外之喜的,比如

  1. 尝试了这么多java8的新特性 ,Stream,lambda,Optional,真香
  2. 小小的尝试了一下设计模式,这样的代码写出来才有意义

与诸君共勉


以后会尝试每个月写一篇总结,希望大家能给点反馈Ψ( ̄∀ ̄)Ψ

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容

  • 2018年8月1日 星期三 晴 “抱团成长,第422篇。” 先生的最后一个八一建军节。说出来的时候眼泪也会跟着一起...
    161d968e601f阅读 132评论 0 0
  • 2018年1月19日我在清晨中屋檐下的雨滴声里醒来,武汉的冬雨依旧那么潮湿阴冷。昨日一个人去看了被同事们安利了很...
    乐活江夏阅读 223评论 0 0
  • 20180902 开学之季,下面这段话被刷屏了。 开学第一课,共勉[微笑][微笑][微笑] 在教育孩子的时候...
    新愉阅读 253评论 0 1