需求描述:复现尚硅谷电信客服项目,想在一个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方法进去,显得十分的杀鸡用牛刀,就先这么地吧,有大佬看到了知道这个怎么解决的可以说一下解决思路,在下不胜感激。
以上就是针对这个问题的解决办法,在后面的学习过程中也会接着回来进行优化。