Spark從外部數據集中讀取數據

本文首發於我的個人博客QIMING.INFO,轉載請帶上鍊接及署名。

本文將介紹幾種從Spark中讀取數據存入RDD的方式,分別是
- 從HDFS中讀數據
- 從MySQL數據庫中讀數據
- 從HBase數據庫中讀數據

本文中涉及到的工具版本如下:
- Hadoop:2.7.4
- Spark:2.1.1
- HBase:1.2.6
- MySQL:5.7.22
- JDK:1.8.0_171
- Scala:2.11.8

從HDFS中讀數據

準備數據

首先啓動Hadoop(使用start-dfs.sh),在HDFS上創建一個目錄:

$ hadoop fs -mkdir -p /user/hadoop/input

新建一個文件input.txt,內容如下:

15 78 89 22
777 32 4 50

input.txt上傳到HDFS上:

$ hadoop fs -put input.txt /user/hadoop/input

用ls命令查看是否上傳成功:
ls

讀取數據

Spark將讀取到的數據會保存在RDD中,關於RDD的介紹可以參考本站的這篇文章Spark-RDD的簡單使用
在Spark中從HDFS讀取文本文件可以使用sc.textFile方法,將此方法的參數設爲hdfs://master:port/path即可。
所以本例中的讀取步驟如下:
進入spark的安裝目錄,使用bin/spark-shell來啓動spark命令行編程(語言爲scala)。
輸入以下代碼:

val rdd = sc.textFile("hdfs://localhost:9000/user/hadoop/input/input.txt")
rdd.count()           // 輸出行數
rdd.foreach(println)  // 將所有內容打印出來

hdfsresult

從MySQL數據庫中讀數據

數據來源

將db_score數據庫中的tb_course表作爲數據來源,表中內容如下圖:
mysql

讀取數據

Spark可以用JDBC來連接關係型數據庫,包括MySQL、Oracle、Postgre等系統。
在執行spark-shell或者spark-submit命令的時候,需在--driver-class-path配置對應數據庫的JDBC驅動的路徑。
本例中,使用以下命令啓動spark-shell:

$ bin/spark-shell --driver-class-path /home/hadoop/mysql-connector-java-5.1.21-bin.jar

方法一:使用org.apache.spark.rdd.JdbcRDD

代碼及說明如下:

import java.sql.DriverManager
import java.sql.ResultSet
import org.apache.spark.rdd.JdbcRDD

def createConnection() = {              //創建連接
  Class.forName("com.mysql.jdbc.Driver").newInstance()
DriverManager.getConnection("jdbc:mysql://localhost:3306/db_score","root","passwd")
}
def extractValues(r:ResultSet) = {      //從數據庫中取得數據後轉換格式
  (r.getInt(1),r.getString(2))
}
val courseRdd = new JdbcRDD(            // 調用JdbcRDD類
  sc,                                   // SparkContext對象
  createConnection,                     // 與數據庫的連接
  "select * from tb_course where ? <= courseid and courseid <= ?", // SQL語句
  1,                                    // 查詢的下界
  7,                                    // 查詢的上界
  2,                                    // partition的個數(即分爲幾部分查詢)
  extractValues                         // 將數據轉換成需要的格式
)
courseRdd.collect.foreach(println)           // 打印輸出

結果如下圖:
courseRDD

注:從上例中可以看出,使用JdbcRDD時,SQL查詢語句必須有類似ID >= ? AND ID <= ?這樣的where語句(經測試,直接去掉會報錯),而且上界和下界的類型必須是Long,這樣使得JdbcRDD的使用場景比較侷限。不過參照JdbcRDD的源代碼,用戶可以修改源代碼以寫出符合自己需求的JdbcRDD。

方法二:使用Spark SQL來返回一個DataFrame

代碼及說明如下:

import org.apache.spark.sql.SQLContext

val sqlContext = new SQLContext(sc)               // 生成SQLContext對象
val sql = "select * from tb_course"               // SQL查詢語句

val courseDF = sqlContext.read.format("jdbc").options(
  Map("url"->"jdbc:mysql://localhost:3306/db_score",
    "dbtable"->s"(${sql}) as table01",            // SQL查詢並對結果起別名
    "driver"->"com.mysql.jdbc.Driver",            // 驅動
    "user"-> "root",                              // 用戶名
    "password"->"passwd")                         // 密碼
).load()

courseDF.collect().foreach(println)               // 打印輸出

結果如下圖:
courseDF

從HBase數據庫中讀數據

準備數據

首先啓動HDFS(start-dfs.sh)和HBase(start-hbase.sh
輸入hbase shell進入HBase的命令行模式
使用create命令創建一張有f1、f2兩個列族的表:

hbase(main) > create 'test1',{NAME => 'f1'},{NAME => 'f2'}

使用put命令給表test1添加一些測試數據:

hbase(main) > put 'test1','row01','f1:data','10001' 
hbase(main) > put 'test1','row01','f2:data','10002'
hbase(main) > put 'test1','row02','f2:data','10003'

查看添加的數據:
scantest1

讀取數據

Spark連接HBase時需要一些必要的jar包,可在HBase安裝目錄下的lib文件夾中找到,將它們複製到一個自定義文件夾中(本例中在Spark安裝目錄下新建了名爲hbase-lib的文件夾),這些jar包清單如下:
sparkhbasejars
即metrics-core-2.2.0.jar、protobuf-java-2.5.0.jar、htrace-core-3.1.0-incubating.jar、guava-12.0.1.jar這四個jar包加上所有hbase-開頭的所有jar包。(注:spark的環境中有metrics的jar包,但是可能是版本不匹配的問題,如果不加入此2.2.0版本的,程序會報錯)
然後在Spark安裝目錄下的conf文件夾中找到spark-env.sh,在其中添加:

export SPARK_CLASSPATH=/opt/software/spark/hbase-lib/*

方法一:調用newAPIHadoopRDD

代碼及相關說明如下:

import org.apache.hadoop.hbase.HBaseConfiguration
import org.apache.hadoop.hbase.client.Result
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.hadoop.hbase.util.Bytes

val conf = HBaseConfiguration.create()
conf.set(TableInputFormat.INPUT_TABLE,"test1")   //設置需要掃描的表(test1)

val rdd = sc.newAPIHadoopRDD(conf,
classOf[TableInputFormat],classOf[ImmutableBytesWritable],classOf[Result])

由於TableInputFormat類的實現,Spark可以用Hadoop輸入格式訪問HBase,即調用sc.newAPIHadoopRDD,此方法返回一個鍵值對類型的RDD,其中鍵的類型爲ImmutableBytesWritable,值的類型爲Result(分別是此方法的後兩個參數)。
因此,遍歷此鍵值對RDD中的值即可取得想要的數據,代碼如下:

rdd.foreach{case (_,result) =>{              //逐行遍歷
  val row = Bytes.toString(result.getRow)    //獲取當前行的Row key
  val value = Bytes.toString(result.getValue("f2".getBytes,"data".getBytes))
                            //根據列族名(f2)和列名(data)取當前行的數據
  println("Row:"+row+" f2, data:"+value)     //打印輸出
}}

運行結果如下:

方法二:用org.apache.hadoop.hbase中提供的方法

以下代碼改編自《Hadoop+Spark生態系統操作與實戰指南》,利用此代碼可以實現對HBase的CRUD操作,代碼如下:

import org.apache.hadoop.hbase.client._
import org.apache.hadoop.hbase.util.Bytes
import org.apache.hadoop.hbase.{HBaseConfiguration, HColumnDescriptor,
HTableDescriptor, TableName}

//創建表
def createHTable(connection: Connection,tablename: String): Unit=
{
  //Hbase表模式管理器
  val admin = connection.getAdmin
  //本例將操作的表名
  val tableName = TableName.valueOf(tablename)
  //如果需要創建表
  if (!admin.tableExists(tableName)) {
    //創建Hbase表模式
    val tableDescriptor = new HTableDescriptor(tableName)
    //創建列簇1    artitle
    tableDescriptor.addFamily(new HColumnDescriptor("artitle".getBytes()))
    //創建列簇2    author
    tableDescriptor.addFamily(new HColumnDescriptor("author".getBytes()))
    //創建表
    admin.createTable(tableDescriptor)
    println("create done.")
  }
}
//刪除表
def deleteHTable(connection:Connection,tablename:String):Unit={
  //本例將操作的表名
  val tableName = TableName.valueOf(tablename)
  //Hbase表模式管理器
  val admin = connection.getAdmin
  if (admin.tableExists(tableName)){
    admin.disableTable(tableName)
    admin.deleteTable(tableName)
  }
}

//插入記錄
def insertHTable(connection:Connection,tablename:String,family:String,column:String,
key:String,value:String):Unit={
  try{
    val userTable = TableName.valueOf(tablename)
    val table=connection.getTable(userTable)
    //準備key 的數據
    val p=new Put(key.getBytes)
    //爲put操作指定 column 和 value
    p.addColumn(family.getBytes,column.getBytes,value.getBytes())
    //提交一行
    table.put(p)
  }
}

//基於KEY查詢某條數據
def getAResult(connection:Connection,tablename:String,family:String,column:String,
key:String):Unit={
  var table:Table=null
  try{
    val userTable = TableName.valueOf(tablename)
    table=connection.getTable(userTable)
    val g=new Get(key.getBytes())
    val result=table.get(g)
    val value=Bytes.toString(result.getValue(family.getBytes(),column.getBytes()))
    println("value:"+value)
  }finally{
    if(table!=null)table.close()
  }

}

//刪除某條記錄
def deleteRecord(connection:Connection,tablename:String,family:String,column:String,
key:String): Unit ={
  var table:Table=null
  try{
    val userTable=TableName.valueOf(tablename)
    table=connection.getTable(userTable)
    val d=new Delete(key.getBytes())
    d.addColumn(family.getBytes(),column.getBytes())
    table.delete(d)
    println("delete record done.")
  }finally{
    if(table!=null)table.close()
  }
}

//掃描記錄
def scanRecord(connection:Connection,tablename:String,family:String,column:String): Unit ={
  var table:Table=null
  var scanner:ResultScanner=null
  try{
    val userTable=TableName.valueOf(tablename)
    table=connection.getTable(userTable)
    val s=new Scan()
    s.addColumn(family.getBytes(),column.getBytes())
    scanner=table.getScanner(s)
    println("scan...for...")
    var result:Result=scanner.next()
    while(result!=null){
      println("Found row:" + result)
      println("Found value: "+
Bytes.toString(result.getValue(family.getBytes(),column.getBytes())))
      result=scanner.next()
    }
  }finally{
    if(table!=null)
      table.close()
    scanner.close()
  }
}

(注:以上代碼中的Key均代表Row Key
以上代碼將在HBase中創建表、刪除表、插入記錄、根據行號查詢數據、刪除記錄、掃描記錄等操作都寫成了函數,將以上代碼在spark-shell中運行後,對HBase的操作直接調用相關函數即可,如下:

//創建一個配置,採用的是工廠方法
val conf = HBaseConfiguration.create
//Connection 的創建是個重量級的工作,線程安全,是操作hbase的入口
val connection= ConnectionFactory.createConnection(conf)

//創建表測試
createHTable(connection, "HadoopAndSpark")

//插入數據,重複執行爲覆蓋
insertHTable(connection,"HadoopAndSpark","artitle","Hadoop","002","Hadoop for me")
insertHTable(connection,"HadoopAndSpark","artitle","Hadoop","003","Java for me")
insertHTable(connection,"HadoopAndSpark","artitle","Spark","002","Scala for me")

//刪除記錄
deleteRecord(connection,"HadoopAndSpark","artitle","Spark","002")

//掃描整個表
scanRecord(connection,"HadoopAndSpark","artitle","Hadoop")

//根據表名、行鍵、列族、列名取當前Cell的數據
getAResult(connection,"HadoopAndSpark","artitle","Hadoop","002")

//刪除表測試
deleteHTable(connection, "HadoopAndSpark")

後記

Spark可以通過所有Hadoop支持的外部數據源(包括本地文件系統、HDFS、Cassandra、關係型數據庫、HBase、亞馬遜S3等)建立RDD,本文沒有講到的,後續視情況補充。Spark支持文本文件、序列文件及其他任何Hadoop輸入格式文件。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章