hadoop技术推出一度曾遭到关系数据库研究者的挑衅和批评,认为MapReduce不具有关系数据库中的结构化数据存储和处理能力。为此,hadoop社区和研究人员做了多的努力,在hadoop0.19版支持
MapReduce访问关系数据库,如:MySQL、Mongodb、PostgreSQL、Oracle 等几个数据库系统。Hadoop 访问关系数据库主要通过DBInputFormat类实现的,包的位置在 org.apache.hadoop.mapred.lib.db。
本课程我们以 Mysql为例来学习 MapReduce读写数据。
[读数据]
DBInputFormat 在 Hadoop 应用程序中通过数据库供应商提供的 JDBC接口来与数据库进行交互,并且可以使用标准的 SQL 来读取数据库中的记录。学习DBInputFormat首先必须知道二个条件。
第一、在使用 DBInputFormat 之前,必须将要使用的 JDBC 驱动拷贝到分布式系统各个节点的$HADOOP_HOME/lib/目录下。
第二、MapReduce访问关系数据库时,大量频繁的从MapReduce程序中查询和读取数据,这大大的增加了数据库的访问负载,因此,DBInputFormat接口仅仅适合读取小数据量的数据,而不适合处理数据仓库。
提示:处理数据仓库的方法有:利用数据库的 Dump 工具将大量待分析的数据输出为文本,并上传到 HDFS 中进行理。
下面我们来看看 DBInputFormat类的内部结构,DBInputFormat 类中包含以下三个内置类。
1、protected class DBRecordReader implementsRecordReader< LongWritable, T>:用来从一张数据库表中读取一条条元组记录。
2、public static class NullDBWritable implements DBWritable,Writable:主要用来实现 DBWritable 接口。DBWritable接口要实现二个函数,第一是write,第二是readFileds,这二个函数都不难理解,一个是写,一个是读出所有字段。原型如下:
public void write(PreparedStatement statement) throwsSQLException;
public void readFields(ResultSet result);
3、protected static class DBInputSplit implements InputSplit:主要用来描述输入元组集合的范围,包括 start 和 end 两个属性,start 用来表示第一条记录的索引号,end 表示最后一条记录的索引号。
下面对怎样使用 DBInputFormat 读取数据库记录进行详细的介绍,具体步骤如下:
步骤一、配置 JDBC 驱动、数据源和数据库访问的用户名和密码。代码如下。
DBConfiguration.configureDB (Job job, StringdriverClass, String dbUrl, String userName, String passwd)
MySQL 数据库的 JDBC 的驱动为“com.mysql.jdbc.Driver”,数据源为“jdbc:mysql://localhost/testDB”,其中testDB为访问的数据库。useName一般为“root”,passwd是你数据库的密码。
步骤二、使用 setInput 方法操作 MySQL 中的表,setInput 方法的参数如下。
DBInputFormat.setInput(Job job, Class< extends DBWritable> inputClass, String tableName, String conditions,String orderBy, String... fieldNames)
这个方法的参数很容易看懂,inputClass实现DBWritable接口。string tableName表名, conditions表示查询的条件,orderby表示排序的条件,fieldNames是字段,这相当与把sql语句拆分的结果。当然也可以用sql语句进行重载,代码如下。
setInput(Job job, Class< extends DBWritable> inputClass, String inputQuery, StringinputCountQuery)。
步骤三、编写MapReduce函数,包括Mapper 类、Reducer 类、输入输出文件格式等,然后调用job.waitForCompletion(true)。
我们通过示例程序来看看 MapReduce 是如何读数据的,假设 MySQL 数据库中有数据库 user,假设数据库中的字段有“uid”,“email”,“name"。
第一步要实现DBwrite和Writable数据接口。代码如下:
package com.dajiangtai.hadoop.advance;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.mapred.lib.db.DBWritable;
public class UserRecord implements Writable, DBWritable {
int uid;
String email;
String name;
/*
*从数据库读取所需要的字段
*
*/
@Override
public void readFields(ResultSet resultSet) throws SQLException {
// TODO Auto-generated method stub
this.uid = resultSet.getInt(1);
this.email = resultSet.getString(2);
this.name = resultSet.getString(3);
}
/*
*向数据库写入数据
*
*/
@Override
public void write(PreparedStatement statement) throws SQLException {
// TODO Auto-generated method stub
statement.setInt(1, this.uid);
statement.setString(2, this.email);
statement.setString(3, this.name);
}
/*
*读取序列化数据
*
*/
@Override
public void readFields(DataInput in) throws IOException {
// TODO Auto-generated method stub
this.uid = in.readInt();
this.email = in.readUTF();
this.name = in.readUTF();
}
/*
*将数据序列化
*
*/
@Override
public void write(DataOutput out) throws IOException {
// TODO Auto-generated method stub
out.writeInt(uid);
out.writeUTF(email);
out.writeUTF(name);
}
public String toString() {
return new String(this.uid + " " + this.email + " " +this.name);
}
}
第二步,实现Map和Reduce类
public static class ConnMysqlMapper extends Mapper< LongWritable,UserRecord,Text,Text> {
public void map(LongWritable key,UserRecord values,Context context)
throws IOException,InterruptedException {
//从 mysql 数据库读取需要的数据字段
context.write(new Text(values.uid+""), new Text(values.name +" "+values.email));
}
}
public static class ConnMysqlReducer extends Reducer< Text,Text,Text,Text> {
public void reduce(Text key,Iterable< Text> values,Context context)
throws IOException,InterruptedException {
//将数据输出到HDFS中
for(Iterator< Text> itr = values.iterator();itr.hasNext();) {
context.write(key, itr.next());
}
}
}
第三步:主函数的实现
/**
* @function MapReduce 连接mysql数据库 读取数据
* @author 小讲
*
*/
public class ConnMysql {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
//输出路径
Path output = new Path("hdfs://single.hadoop.dajiangtai.com:9000/advance/mysql/out");
FileSystem fs = FileSystem.get(URI.create(output.toString()), conf);
if (fs.exists(output)) {
fs.delete(output);
}
//mysql的jdbc驱动
DistributedCache.addFileToClassPath(new Path("hdfs://single.hadoop.dajiangtai.com:9000/advance/jar/mysql-connector-java-5.1.14.jar"), conf);
//设置mysql配置信息 4个参数分别为: Configuration对象、mysql数据库地址、用户名、密码
DBConfiguration.configureDB(conf, "com.mysql.jdbc.Driver", "jdbc:mysql://hadoop.dajiangtai.com:3306/djtdb_www", "username", "password");
Job job = new Job(conf,"test mysql connection");//新建一个任务
job.setJarByClass(ConnMysql.class);//主类
job.setMapperClass(ConnMysqlMapper.class);//Mapper
job.setReducerClass(ConnMysqlReducer.class);//Reducer
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
job.setInputFormatClass(DBInputFormat.class);//从数据库中读取数据
FileOutputFormat.setOutputPath(job, output);
//列名
String[] fields = { "uid", "email","name" };
//六个参数分别为:
//1.Job;2.Class< extends DBWritable> 3.表名;4.where条件 5.order by语句;6.列名
DBInputFormat.setInput(job, UserRecord.class,"user", null, null, fields);
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
第四步:运行命令如下。
[hadoop@single-hadoop-dajiangtai-com djt]$ hadoop jar ConnMysql.jar com.dajiangtai.hadoop.advance.ConnMysql
第五步:查看结果如下所示。
[hadoop@single-hadoop-dajiangtai-com djt]$ hadoop fs -text /advance/mysql/out/
54 无情 [email protected]
55 冷血 [email protected]
提示 MapReduce 操作 MySQL 数据库在实际工作中比较常用,例如把 MySQL 中的数据迁移到 HDFS 中, 当然还有个很好的方法把 MySQL 或 Oracle 中的数据迁移到 HDFS 中,这个工具是 Pig,如果有这
方面的需求建议使用 Pig。
[写数据]
数据处理结果的数据量一般不会太大,可能适合hadoop直接写入数据库中。hadoop提供了数据库接口,把 MapReduce 的结果直接输出到 MySQL、Oracle 等数据库。 主要的类如下所示。
1、DBOutFormat: 提供数据库写入接口。
2、DBRecordWriter:提供向数据库中写入的数据记录的接口。
3、DBConfiguration:提供数据库配置和创建链接的接口。
下面我们通过示例来看看 MapReduce 如何向数据库写数据,假设 MySQL 数据库中有数据库 test,假设数据库中的字段有“uid”,“email”,“name"。
第一步同上定义UserRecord实现DBwrite和Writable数据接口。代码不再赘叙。
第二步,实现Map和Reduce类,代码如下所示。
public static class ConnMysqlMapper extends Mapper< LongWritable,Text,Text,Text>
{
public void map(LongWritable key,Text value,Context context)throws IOException,InterruptedException
{
//读取 hdfs 中的数据
String email = value.toString().split("\\s")[0];
String name = value.toString().split("\\s")[1];
context.write(new Text(email),new Text(name));
}
}
public static class ConnMysqlReducer extends Reducer< Text,Text,UserRecord,UserRecord>
{
public void reduce(Text key,Iterable< Text> values,Context context)throws IOException,InterruptedException
{
//接收到的key value对即为要输入数据库的字段,所以在reduce中:
//wirte的第一个参数,类型是自定义类型UserRecord,利用key和value将其组合成UserRecord,然后等待写入数据库
//wirte的第二个参数,wirte的第一个参数已经涵盖了要输出的类型,所以第二个类型没有用,设为null
for(Iterator< Text> itr = values.iterator();itr.hasNext();)
{
context.write(new UserRecord(key.toString(),itr.next().toString()),null);
}
}
}
第三步:主函数的实现,代码如下所示。
/**
* 将mapreduce的结果数据写入mysql中
* @author 小讲
*/
public class WriteDataToMysql {
public static void main(String args[]) throws IOException, InterruptedException, ClassNotFoundException
{
Configuration conf = new Configuration();
//配置 JDBC 驱动、数据源和数据库访问的用户名和密码
DBConfiguration.configureDB(conf, "com.mysql.jdbc.Driver","jdbc:mysql://hadoop.dajiangtai.com:3306/djtdb_www","username", "password");
Job job = new Job(conf,"test mysql connection");//新建一个任务
job.setJarByClass(WriteDataToMysql.class);//主类
job.setMapperClass(ConnMysqlMapper.class); //Mapper
job.setReducerClass(ConnMysqlReducer.class); //Reducer
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
job.setInputFormatClass(TextInputFormat.class);
job.setOutputFormatClass(DBOutputFormat.class);//向数据库写数据
//输入路径
FileInputFormat.addInputPath(job, new Path("hdfs://single.hadoop.dajiangtai.com:9000/advance/mysql/data/data.txt"));
//设置输出到数据库 表名:test 字段:uid、email、name
DBOutputFormat.setOutput(job, "test", "uid","email","name");
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
第四步:运行命令如下。
[hadoop@single-hadoop-dajiangtai-com djt]$ hadoop jar WriteDataToMysql.jar com.dajiangtai.hadoop.advance.WriteDataToMysql
第五步:查看 MySQL 数据库记录。
[email protected] yangjun
[email protected] yangjun
[email protected] binquan