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希望可以帮到各位,不当之处,请多指教~?