基于Flume+Kafka+HBase+Mapreduce的电信客服项目(下)

文章目录

    • 1、数据消费阶段
    • 2、数据分析阶段

1、数据消费阶段

  • 此阶段是将Kafka集群中的数据写入HBase,其中,Kafka和HBase里的数据格式前面已经介绍过了。
  • HBase中关于Put方法的编写。
    /**
     * ori数据样式: 18576581848,17269452013,2017-08-14 13:38:31,1761
     * rowkey样式:01_18576581848_20170814133831_17269452013_1_1761
     * HBase表的列:call1  call2   build_time   build_time_ts   flag   duration
     * @param ori
     */
    public void put(String ori) {
        try {
            if (cacheList.size() == 0) {
                connection = HConnectionManager.createConnection(conf);
                hTable = connection.getTable(TableName.valueOf(tableName));
                // setAutoFlush(autoFlush, clearBufferOnFail)
                // autoFlush 为false, 则当put填满客户端写缓存时,才向HBase服务端发起请求,
                // 而不是有一条put就执行一次更新, autoFlush默认为true
                // clearBufferOnFail 为true,则不会重复提交响应错误的数据。clearBufferOnFail默认是true的。
                hTable.setAutoFlush(false, false);
                hTable.setWriteBufferSize(2 * 1024 * 1024);
            }
            String[] splitOri = ori.split(",");
            String caller = splitOri[0];  // 主叫
            String callee = splitOri[1];  // 被叫
            String buildTime = splitOri[2]; // 开始通话时间
            String duration = splitOri[3];  // 通话时长
            String regionCode = HBaseUtil.genRegionCode(caller, buildTime, regions);  // 分区号
            String buildTimeReplace = sdf2.format(sdf1.parse(buildTime));  // yyyyMMddHHmmss
            String buildTimeTs = String.valueOf(sdf1.parse(buildTime).getTime()); //时间戳字段
            // 生成rowkey    "1": 主叫
            String rowkey = HBaseUtil.genRowKey(regionCode, caller, buildTimeReplace, callee, "1", duration);
    
            // 向表中插入该条数据
            Put put = new Put(Bytes.toBytes(rowkey));
            // HBase表的列:call1  call2   build_time   build_time_ts   flag   duration
            put.add(Bytes.toBytes("f1"), Bytes.toBytes("call1"), Bytes.toBytes(caller));
            put.add(Bytes.toBytes("f1"), Bytes.toBytes("call2"), Bytes.toBytes(callee));
            put.add(Bytes.toBytes("f1"), Bytes.toBytes("build_time"), Bytes.toBytes(buildTime));
            put.add(Bytes.toBytes("f1"), Bytes.toBytes("build_time_ts"), Bytes.toBytes(buildTimeTs));
            put.add(Bytes.toBytes("f1"), Bytes.toBytes("flag"), Bytes.toBytes("1"));
            put.add(Bytes.toBytes("f1"), Bytes.toBytes("duration"), Bytes.toBytes(duration));
            System.out.println("caller: " + caller + ", callee: " + callee + ", buildTime: " +
            buildTime + ", buildTimeTs: " + buildTimeTs + ", duration: " + duration);
            cacheList.add(put);
            if (cacheList.size() >= 30) {
                hTable.put(cacheList);
                hTable.flushCommits();
                hTable.close();
                cacheList.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
    
  • 接着,我们在HBaseConsumer类的main方法编写消费数据的逻辑。
    public static void main(String[] args) {
        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(PropertiesUtil.properties);        kafkaConsumer.subscribe(Arrays.asList(PropertiesUtil.getProperty(kafkaTopics)));
        HBaseDAO hd = new HBaseDAO();
        int i = 1;
        while (true) {
            ConsumerRecords<String, String> records = kafkaConsumer.poll(100);
            for (ConsumerRecord<String, String> cr : records) {
                String oriValue = cr.value();
                System.out.println(i + "   " + oriValue);
                i++;
                hd.put(oriValue);
            }
        }
    }
    
  • 然后,我们再谈谈rowkey中关于hashregion的设计:
  • 分区键怎么设计呢?我们生成的分区键必须是有序的,这里我们使用了TreeSet数据结构。TreeSet是去重的,且默认是按升序排列的。为了防止数据倾斜(包括现有的数据倾斜,也有新进来的数据倾斜),我们可以截取手机号的某几位,然后进行hash,最后再对分区数求余即可。有人会想,直接用Random类随机生成00到05的分区键,不就好了嘛。这样存在两个问题:
    1)Random实际上是伪随机数,当数据量达到万亿级别时,会发现Random产生的数据符合一定的规律,而不是真正离散的。毕竟,计算机是一种可确定,可预测的的设备,想通过一行一行的确定的代码自身产生真随机数,显然不可能。而且,有限状态机是不可能产生真正的随机数的,所以在现在的计算机中没有一个真正的随机数生成算法,估计要产生真随机数只能依靠量子力学了。
    2)不能按时间范围归属数据,比如用户2月份的电话信息会分散在多个分区中,从而无法设置扫描范围,只能通过过滤器去访问数据。这对海量数据的查询,效率是极低的。
    这里,我们利用手机号的后4位与时间的年月进行异或操作,得到的结果便是手机号与年月的联系,则用户在某月份的通话信息就可以保存在一个分区中。也许,读者认为,若极端情况下,如果用户在某一个月都不打电话或者频繁打电话(过年),会产生数据倾斜?这发生的概率很低,理由是:我们用手机号的后4位与时间进行了多次离散和hash操作。生成分区键代码如下:
    /**
     * 手机号:15837312345
     * 通话建立时间:2017-01-10 11:20:30 -> 20170110112030
     * @param call1
     * @param buildTime
     * @param regions
     * @return
     */
    public static String genRegionCode(String call1, String buildTime, int regions){
        int len = call1.length();
        //取出后4位号码
        String lastPhone = call1.substring(len - 4);
        //取出年月
        String ym = buildTime
                .replaceAll("-", "")
                .replaceAll(":", "")
                .replaceAll(" ", "")
                .substring(0, 6);
        //离散操作1
        Integer x = Integer.valueOf(lastPhone) ^ Integer.valueOf(ym);
        //离散操作2
        int y = x.hashCode();
        //生成分区号
        int regionCode = y % regions;
        //格式化分区号
        DecimalFormat df = new DecimalFormat("00");
        return df.format(regionCode);
    }
    
  • 协处理器(以后补充~)

2、数据分析阶段

  • 统计指标如下:

  • 用户每天主叫通话个数统计,通话时间统计。

  • 用户每月通话记录统计,通话时间统计。

  • 用户之间亲密关系统计。(通话次数与通话时间体现用户亲密关系)

  • map函数如下:

    @Override
    protected void map(ImmutableBytesWritable key, Result value, Context context) throws IOException, InterruptedException {
        //05_19902496992_20170312154840_15542823911_1_1288
        String rowKey = Bytes.toString(key.get());
        String[] splits = rowKey.split("_");
        if (splits[4].equals("0")) return;
    
        //以下数据全部是主叫数据,但是也包含了被叫电话的数据
        String caller = splits[1];
        String callee = splits[3];
        String buildTime = splits[2];
        String duration = splits[5];
        durationText.set(duration);
    
        String year = buildTime.substring(0, 4);
        String month = buildTime.substring(4, 6);
        String day = buildTime.substring(6, 8);
    
        //组装ComDimension
        //组装DateDimension
        ////05_19902496992_20170312154840_15542823911_1_1288
        DateDimension yearDimension = new DateDimension(year, "-1", "-1");
        DateDimension monthDimension = new DateDimension(year, month, "-1");
        DateDimension dayDimension = new DateDimension(year, month, day);
    
        //组装ContactDimension
        ContactDimension callerContactDimension = new ContactDimension(caller, phoneNameMap.get(caller));
    
        //开始聚合主叫数据
        comDimension.setContactDimension(callerContactDimension);
        //年
        comDimension.setDateDimension(yearDimension);
        context.write(comDimension, durationText);
        //月
        comDimension.setDateDimension(monthDimension);
        context.write(comDimension, durationText);
        //日
        comDimension.setDateDimension(dayDimension);
        context.write(comDimension, durationText);
    
        //开始聚合被叫数据
        ContactDimension calleeContactDimension = new ContactDimension(callee, phoneNameMap.get(callee));
        comDimension.setContactDimension(calleeContactDimension);
        //年
        comDimension.setDateDimension(yearDimension);
        context.write(comDimension, durationText);
        //月
        comDimension.setDateDimension(monthDimension);
        context.write(comDimension, durationText);
        //日
        comDimension.setDateDimension(dayDimension);
        context.write(comDimension, durationText);
    }
    
  • reduce函数如下:

    @Override
    protected void reduce(ComDimension key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
        int callSum = 0;
        int callDurationSum = 0;
        for(Text t : values){
            callSum++;
            callDurationSum += Integer.valueOf(t.toString());
        }
        countDurationValue.setCallSum(String.valueOf(callSum));
        countDurationValue.setCallDurationSum(String.valueOf(callDurationSum));
    
        context.write(key, countDurationValue);
    }
    
  • 源码和数据
    https://github.com/fengqijie001/ct

    希望可以帮到各位,不当之处,请多指教~?

你可能感兴趣的:(我的项目)