Data_Algorithm》读书笔记四 — 使用MapReduce实现左外连接

1.左外连接

左外连接即left join,这个在 mysql 中是很常见的操作,如下表:

1.1 创建表
create table users
(user_id varchar(10),
location_id varchar(10)
);
create table transactions(
transaction_id varchar(10),
product_id varchar(10),
user_id varchar(10),
quantity int,
amount int
);
1.2 插入数据
insert into users values('u1','UT');
insert into users values('u2','GA');
insert into users values('u3','CA');
insert into users values('u4','CA');
insert into users values('u5','GA');
insert into transactions values('t1','p3','u1',3,330  );
insert into transactions values('t2','p1','u2',1,400  );
insert into transactions values('t3','p1','u1',3,600  );
insert into transactions values('t4','p2','u2',10,1000);
insert into transactions values('t5','p4','u4',9,90   );
insert into transactions values('t6','p1','u1',4,120  );
insert into transactions values('t7','p4','u1',8,160  );
insert into transactions values('t8','p4','u5',2,40   );
1.3 查询数据
select 
t.product_id
,count(distinct u.location_id) as frequency
from transactions t
left join  users u
	on u.user_id = t.user_id
group by t.product_id;
+------------+-----------+
| product_id | frequency |
+------------+-----------+
| p1         |         2 |
| p2         |         1 |
| p3         |         1 |
| p4         |         3 |
+------------+-----------+
4 rows in set (0.00 sec)

这样,以transactions作为左表,然后得到每个 product_id 热销到不同 location 的数目。例如:这里的
| p1 | 2 | 表示的意思就是:p1这个商品,销往了2个不同的地区。其它同理
那么现在的问题是:如果这个表很大,打到无法使用mysql等关系型数据库使用join语句,那么该怎么办呢?别急,这时候有MapReduce来帮助我们解决这个问题。

2. MapReduce 实现思路

这里主要讲解一下我的思路。
step 1:针对user表我们可以得到的信息是:<user_id,location_id>; 针对transactions表,我们可以的得到的信息是<user_id,product_id>。那么我们可以使用两个mapper分别从不同的文件中得到这些数据。这个功能可以由MultipleInputs类实现
在从不同的Mapper中得到这个<user_id,location_id><user_id,product_id> 这种键值对之后,需要将其交由 reducer 处理,但是我们在 reducer 之后得到的数据可能是无序的,如下:

[root@server4 hadoop]# hdfs dfs -cat hdfs://server4:9000/output/leftJoin/part-r-00000
u1	UT,p4,p1,p1,p3,
u2	p2,p1,GA,
u3	CA,
u4	CA,p4,
u5	p4,GA,

出现这种问题的原因是:我们未控制到达 reduce 中键值对的value的顺序,所以会出现这种 location_idproduct_id 没有顺序的结果。但是MapReduce提供了一种叫做Second Sort的功能,可以帮助我们对 value 进行一个排序,所以就能够保证到达reduce的数据是有顺序的。即想得到如下这样的数据:

[root@server4 hadoop]# hdfs dfs -cat /output/leftJoin/part-r-00000
u1	UT,p4,p1,p1,p3,
u2	GA,p2,p1,
u3	CA,
u4	CA,p4,
u5	GA,p4,

step 2:使用一个类 User封装user_id,location_id,product_id,level等信息。其中user_id,location_id,product_id都是业务属性,而level是我们构造出来,用于对两种不同的 mapper 进行排序的字段。
step 3:在使用mapper产生了key-value对之后,进行一个二次排序,然后使得相同的key的数据能够有一种排序规则。
step 4:得到如下的这种数据之后,仍然不是我们想要的结果。因为我们想要的是一个每个prodcut_id销往的不同的产地,还需要再使用一次MapReduce job才能得到最终的结果。

[root@server4 hadoop]# hdfs dfs -cat /output/leftJoin/part-r-00000
u1	UT,p4,p1,p1,p3,
u2	GA,p2,p1,
u3	CA,
u4	CA,p4,
u5	GA,p4,

第二次使用MapReduce job 时,只需要将上述输出文件形成如下这种键值对:

p4,UT
p4,CA,
p4,GA,
p1,UT
p1,UT
p1,GA
p3,UT
p2,GA

得到这种键值对之后,我们就可以在reduce中使用一个set简单去重一下即可。
step 4:最后即可得到如下这种数据

[root@server4 hadoop]# hdfs dfs -cat /output/leftJoin/final/part-r-00000
p1	2
p2	1
p3	1
p4	3

3. 实现代码

因为实现代码较多,不宜在这里全部展示,只展示部分重要的类代码。如有兴趣,可以去我的github中获取,或者是留言获取即可。

3.1 主要实现类

主要的实现类如下:
《Data_Algorithm》读书笔记四 — 使用MapReduce实现左外连接_MapReduce

3.2 SecondarySortGroupComparator
package data_algorithm.chapter_4.step_1_leftJoin;

import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;

public class SecondarySortGroupComparator extends WritableComparator {

    //这个构造参数一定要实现,否则会报错
    public SecondarySortGroupComparator() {
        super(User.class,true);
    }

 
    @Override
    public int compare(WritableComparable wc1, WritableComparable wc2) {
        User user1 = (User)wc1;
        User user2 = (User)wc2;

        return user1.getUser_id().compareTo(user2.getUser_id());
    }
}

3.2 User
package data_algorithm.chapter_4.step_1_leftJoin;

import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

public class User implements Writable, WritableComparable<User>{
    private String user_id;
    private int level;//the level of 1 or 2
    private String location;//user location
    private String product_name;//the product name of user buy

    //为什么需要添加无参的构造函数
    public User() {
    }

    public User(String user_id, int level, String location, String product_name) {
        this.user_id = user_id;
        this.level = level;
        this.location = location;
        this.product_name = product_name;
    }

    public String getUser_id() {
        return user_id;
    }

    public void setUser_id(String user_id) {
        this.user_id = user_id;
    }

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }

    public String getProduct_name() {
        return product_name;
    }

    public void setProduct_name(String product_name) {
        this.product_name = product_name;
    }

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;
    }

    public int compareTo(User user) {
        int compareValue = this.getUser_id().compareTo(user.getUser_id());
        if (compareValue == 0) {
            return this.getLevel() - user.getLevel();  //先出现 location, 再出现 product
            //return user.getLevel() - this.getLevel(); //先出现product , 再出现 location
        }
        return compareValue;
    }

    public void write(DataOutput out) throws IOException {
        out.writeUTF(this.user_id);
        out.writeInt(this.level);
        out.writeUTF(this.location);
        out.writeUTF(this.product_name);
    }

    public void readFields(DataInput in) throws IOException {
        this.user_id = in.readUTF();
        this.level = in.readInt();
        this.location = in.readUTF();
        this.product_name = in.readUTF();
    }

    @Override
    public int hashCode() {
        int result = this.getUser_id() != null ? this.getUser_id().hashCode() : 0;
        //result = 31 * result + level ;
        //不能使用这个+ level 值的,否则会将相同user_id 的分到不同的区?
        if (this.getUser_id().equals("u1")) {
            System.out.println("u1");
            return 1;
        }
        if (this.getUser_id().equals("u2")) {
            System.out.println("u2");
            return 2;
        }
//        if (this.getUser_id().equals("u3")) return 3;
//        if (this.getUser_id().equals("u4")) return 4;
//        if (this.getUser_id().equals("u5")) return 5;

        return result;
    }

    @Override
    public String toString() {
        return this.getUser_id();
    }
}