201901工作复盘

作者: alonwang | 来源:发表于2019-01-13 16:34 被阅读0次

    前言

    过去的一个月公司事情比较多,笔者负责一个版本的更新,这两天大致完成了.这次是真正意义上的自己负责一个版本(当然,有大佬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. 小小的尝试了一下设计模式,这样的代码写出来才有意义

    与诸君共勉


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

    相关文章

      网友评论

        本文标题:201901工作复盘

        本文链接:https://www.haomeiwen.com/subject/irhldqtx.html