需求描述:复现尚硅谷电信客服项目,想在一个map中统计单个用户的通话信息以及主被叫用户之间的通话亲密度信息。

思路1:

当前统计单个用户通话信息已经封装了一个自定义key,AnalysisKey(属性值为tel和date),自然想到再封装一个新的自定义key,AnalysisIntimacyKey(属性值caller,callee,date)。但map方法或者reduce方法中都是只能返回一种key和value,因此如果直接在一个map()中封装另一个自定义key的话,map的返回值key会不知道指定哪个。

思路2:

既然无法指定两个key,那么就想办法让这两个key产生关联,比如继承关系等。因此让新封装的AnalysisIntimacyKey extends AnalysisKey,map()和reduce()中仍使用AnalysisKey。然而在extends过程中发现,由于父类AnalysisKey已经实现了WritableCompareable接口,子类在继承时无法override其中的compareTo方法,只能使用父类的compareTo方法,不符合业务逻辑;并且父类实现接口时传递的泛型时父类,子类继承后无法进行修改。

思路3:

最后没有办法,就重新定义了一个新的AnalysisBaseKey,这个key为了可以满足上述两种功能的需求,设计了两个属性。第一个为String类型的ArrayList,专门用来存储电话号码,可以存放单个用户的号码或者主被叫用户两个号码;第二个保持AnalysisKey中的date属性。重写WritableCompareable接口的compareTo()、write()和readFiled()方法时需要分情况处理:

//用户电话号码数组,String类型,可以存放至多两个电话号码,按照主叫->被叫的顺序存放
    private ArrayList<String> telArrays;
    private String date;
@Override
    public int compareTo(AnalysisBaseKey key) {
//        这里稍微繁琐一下,因为基类可以代表两种分析情况,单独统计一个用户的通话时长信息和两个用户的通话亲密度信息
        int result;
        int len1 = telArrays.size();
        int len2 = key.getTelArrays().size();
        if (len1 == 1 && len2 == 1){
            result =  compareToOneUser(key);
        }else if (len1 == 1 && len2 == 2 || len1 ==2 && len2 == 1){
            result = compareToChaos(key);
        }
        else{
            //        两个用户通话亲密度
            result =  compareToIntimacy(key);
        }

        return result;
    }
/**
     * 两条记录是不一样的,一个是单用户的,另一个是用户亲密度的。这种混乱的情况下没有办法保证排序的合理性,就凑活排吧
     * @param key,len1,len2
     * len1为当前key的telArrays长度,len2为key的telArrays长度
     * @return
     */
    private int compareToChaos(AnalysisBaseKey key) {
        /**
         * 两种情况:
         * 情况1:当前key的telArrays只有主叫或被叫,len2拥有主叫和被叫两个号码,则默认当前key中的是主叫号码,与传入key的主叫字段比较
         * 情况2:当前key的telArrays包括主叫被叫,len2只有主叫或被叫一个号码,则默认传入key中的是主叫号码,与当前key的主叫字段比较
         * 最终策略:都先只比较两个key的telArrays[0],相同时比较日期。
         */
        String keyCaller = key.getTelArrays().get(0);
        int result = telArrays.get(0).compareTo(keyCaller);
        if (result == 0){
//                主叫号码相同的情况下直接比较日期
            String keyDate = key.getDate();
            result = date.compareTo(keyDate);
        }
        return result;
    }

这里很无奈,等于说这样排序又回到最初子类无法重写父类compareTo方法的问题,兜了一大圈啥都没解决,直接脑裂。

/**
     * 单用户信息统计compareTo方法
     * @param key
     * @return
     */
    private int compareToOneUser(AnalysisBaseKey key){
//        获取待比较的key中单用户的电话号码
        String keyTel = key.getTelArrays().get(0);
        int result = telArrays.get(0).compareTo(keyTel);
        if (result == 0){
            String keyDate = key.getDate();
            result = date.compareTo(keyDate);
        }
        return result;
    }
/**
     * 主被叫用户通话亲密度分析compareTo方法
     * @param key
     * @return
     */
    private int compareToIntimacy(AnalysisBaseKey key){
        String keyCaller = key.getTelArrays().get(0);

        int result = telArrays.get(0).compareTo(keyCaller);
        if (result == 0){
            String keyCallee = key.getTelArrays().get(1);
            result = telArrays.get(1).compareTo(keyCallee);
            if (result == 0){
                String keyDate = key.getDate();
                result = date.compareTo(keyDate);
            }
        }
        return result;
    }
//序列化反序列化也需要修改
@Override
    public void write(DataOutput out) throws IOException {
        if (telArrays.size() == 1){
            writeOneUser(out);
        }else{
            writeIntimacy(out);
        }
    }


    /**
     * 单用户序列化方法
     * @param out
     * @throws IOException
     */
    public void writeOneUser(DataOutput out) throws IOException {
        out.writeUTF(telArrays.get(0));
        out.writeUTF(date);

//        添加一个标志对象避免序列化异常
        out.writeUTF("null");
    }

    /**
     * 主被叫用户亲密度分析序列化方法
     * @param out
     * @throws IOException
     */
    public void writeIntimacy(DataOutput out) throws IOException{
        out.writeUTF(telArrays.get(0));//主叫
        out.writeUTF(telArrays.get(1));//被叫
        out.writeUTF(date);

        out.writeUTF("null");
    }


    @Override
    public void readFields(DataInput in) throws IOException {
        List<String> inputList = new ArrayList<String>(3);

//        将反序列化读入的数据保存到list中,最多可写入三次(caller,callee,tel)
        for (int i = 0; i < 3; i++){
            String inputData = in.readUTF();
            if (!"null".equals(inputData)){
                inputList.add(inputData);
            }else{
                break;
            }
        }

//        根据list的大小初始化
        if (inputList.size() == 2){
            readFieldsOneUser(inputList);
        }else if (inputList.size() == 3){
            readFieldsIntimacy(inputList);
        }
    }

    /**
     * 单用户通话时长统计反序列化方法
     * @param inputLists
     * @throws IOException
     */
    public void readFieldsOneUser(List<String> inputLists) throws IOException {
        ArrayList<String> tmpArrayList = new ArrayList<>(1);
        tmpArrayList.add(inputLists.get(0));
        telArrays = tmpArrayList;
        date = inputLists.get(1);
    }


    /**
     * 主被叫用户亲密度分析反序列化方法
     * @param inputLists
     * @throws IOException
     */
    public void readFieldsIntimacy(List<String> inputLists) throws IOException {
        ArrayList<String> tmpArrayList = new ArrayList<>(2);
        tmpArrayList.add(inputLists.get(0));
        tmpArrayList.add(inputLists.get(1));
        telArrays = tmpArrayList;
        date = inputLists.get(2);
    }

这样这个AnalysisBaseKey就大功告成了,可以说是没解决什么问题还给自己徒增了一堆烦恼……。

最后在mapper中和reducer中更改一下对应的key就行,至于在context.write()方法中如何封装对应字段信息到key和value中就是各有各的办法了,我写的很丑。

@Override
    protected void map(ImmutableBytesWritable key, Result value, Mapper<ImmutableBytesWritable, Result, 
            AnalysisBaseKey, Text>.Context context) throws IOException, InterruptedException {
//        主叫用户和被叫用户分别根据hbase中的记录生成三条记录,包含年、月、日三种时间粒度级别
//        参数中key为hbase的rowkey,根据rowkey切分出mapper每条记录所需数值
//        regionNum_call1_calldate_call2_duration_flag
//        5_19993024340_2018091205000005_17721353718_0661_1
        String rowKey = Bytes.toString(key.get());//这里切记不要直接写个key.toString(),不然直接G

        String[] fields = rowKey.split("_");

        String caller = fields[1];//主叫用户
        String callee = fields[3];//被叫用户
        String callTime = fields[2];//通话日期
        String duration = fields[4];//通话时长

//        切分通话日期为年月日
        String year = callTime.substring(0,4);
        String month = callTime.substring(4,6);
        String day = callTime.substring(6,8);

        /**
         * 由于每条记录的条数就代表了通话次数,因此只需要在reducer计算每个reducer中参与聚合的记录数就可以,mapper中只需要传递duration即可
         * 主叫用户:19993024340-不同粒度时间,duration
         * 切分粒度的时候按修改key格式:
         *
         * 仅统计2018年:year + "0000",月和日缺省
         * 仅统计2018年某一月:year + month + "00",日缺省
         * 仅统计2018年某一月某一日:year + month + day,无缺省值
         * 这样的好处是可以避免数据库查询时空指针异常,同时优化数据库设计时设计字段为not null时的额外存储策略,
         * 代价是用增加了若干字节(0)的存储开销
         */
//        主叫用户
        ArrayList<String> telArrays = new ArrayList<>();
        telArrays.add(caller);
        context.write(new AnalysisBaseKey(telArrays,year + "0000"), new Text(duration));//仅统计年
        context.write(new AnalysisBaseKey(telArrays , year + month + "00"), new Text(duration));//仅统计月
        context.write(new AnalysisBaseKey(telArrays , year + month + day), new Text(duration));//仅统计日

//        被叫用户:19993024340-不同粒度时间,duration
        telArrays.set(0,callee);
        context.write(new AnalysisBaseKey(telArrays , year + "0000"), new Text(duration));
        context.write(new AnalysisBaseKey(telArrays , year + month + "00"), new Text(duration));
        context.write(new AnalysisBaseKey(telArrays , year + month + day), new Text(duration));

        telArrays.set(0,caller);
        telArrays.add(callee);
//        统计用户亲密度
        context.write(new AnalysisBaseKey(telArrays,year + "0000"),new Text(duration));
        context.write(new AnalysisBaseKey(telArrays,year + month + "00"),new Text(duration));
        context.write(new AnalysisBaseKey(telArrays,year + month + day),new Text(duration));

    }

运行结果是成功的,但是mapper阶段特别的慢,当然跟用的虚拟机有一定关系,但比之前运行过程直接慢了一倍,等得我头又秃了那么一点。原因想了想,首先map()方法首先没有优化,十分的臃肿,一次写了这么多条记录简直犹太人见了都流泪;另一个就是还是觉得这个设计想法有问题,key本身就很冗长,序列化反序列化以及compare过程添加了大量的条件语句进行控制,区分key之间的情况,感觉很不合理。但我又不想为了增加这一个分析指标就又加一个map方法进去,显得十分的杀鸡用牛刀,就先这么地吧,有大佬看到了知道这个怎么解决的可以说一下解决思路,在下不胜感激。

以上就是针对这个问题的解决办法,在后面的学习过程中也会接着回来进行优化。