1.SparkStreamming介绍
SparkStreaming是spark技术栈中做实时处理的工具,是一个微批次准实时的流式处理引擎。
我们在做实时处理的时候,通过kafka采集数,将数据读入sparkstreamming中进行处理。
2.整合kafka
方式 1 Receiver方式
这个方式已经过时,不建议使用
缺点:
- Receiver方式为了保证数据安全,将数据写入磁盘记录日志【write Ahead Log】,这种写入效率十分低下。
- 同时使用老版本的kafka消费API,先连接zookeeper,并将偏移量记录在Zookeeper中,效率低下。
方式 2 直连方式
推荐版本:kafka 0.10版本以上
优点:
- 直连方式使用kafka底层高效的API,不再经过zookeeper,效率高。
- 在直连方式中,kafka topic的一个分区对应一个RDD分区(这里的分区指的是leader分区),即RDD分区的数量和kafka的分区数量一一对应,生成的Task直接连到Kafka topic的Leader分区拉取数据,一个消费者Task对应一个Kafka分区。
- 直连方式中可以自己编程保存偏移量
3.Exactly-Once
在实时处理数据时,要保证数据的一次性语义,防止出现有的成功了,有的失败了,导致数据错误,比如银行转账,当小明转给小王100元钱的时候,银行系统出现了故障,导致小明少了100元,小王缺没有多100元,这种事情谁都不乐意见到是不,所以在实时处理中我们有俩种Exactly-Once的保存方式:事务和幂等性。
虽然kafka会自动提交偏移量,但是kafka保证不了Exactly-Once,因此我们公司一般都自动提交偏移量,利用Mysql、Redis或者Hbase保存。
1. 事务
利用事务做Exactly-Once首先要保证是聚合过的数据,只有聚合过的数据的数据量才不会很大,降低了内存溢出的几率。
事务保证Exactly-Once的过程
- 数据经过聚合以后,数据量变的很小,可以收集到Driver端
- 在Driver端利用第一个kafkaRDD获取到kafka的偏移量,然后将计算好的结果和偏移量,使用支持事务的数据库,在同一个事务中将偏移量和计算好的数据写入到数据库,保证同时成功。
- 如果失败,让任务重启,接着上一次成功的偏移量继续读取。
代码如下:
1.Mysql
//利用第一个kafkardd获取到偏移量
kafkaSparkStreaming.foreachRDD(rdd => {
//第一个kafkardd才能获得到偏移量信息
val ranges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
//取出kafka中的数据
val lines: RDD[String] = rdd.map(_.value())
//数据处理逻辑
val reduced: RDD[(String, Int)] = lines.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
//收集到driver端
val rs: Array[(String, Int)] = reduced.collect()
var connection: Connection = null
var statement: PreparedStatement = null
var statement1: PreparedStatement = null
try {
//建立mysql数据库连接,使用连接池更好
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/wanqi", "root", "root")
//开启事务
connection.setAutoCommit(false)
//插入数据,如果存在就更新
statement = connection.prepareStatement("INSERT into t_wordcount(word,count) values (?,?) on DUPLICATE KEY UPDATE count = count + ?")
for (elem <- rs) {
statement.setString(1, elem._1)
statement.setInt(2, elem._2)
statement.setInt(3, elem._2)
statement.executeUpdate()
}
//插入偏移量,如果存在就更新
statement1 = connection.prepareStatement("INSERT into t_kafka_offset(app_gid,topic_partition,offset) values (?,?,?) on DUPLICATE KEY UPDATE offset = ?")
for (elem <- ranges) {
val topic: String = elem.topic
val partition: Int = elem.partition
val offset: Long = elem.untilOffset
statement1.setString(1, appname + "_" + groupId)
statement1.setString(2, topic + "_" + partition)
statement1.setInt(3, offset.toInt)
statement1.setInt(4, offset.toInt)
statement1.executeUpdate()
}
//提交事务
connection.commit()
} catch {
case e: Exception => {
connection.rollback()
sc.stop()
}
} finally {
if (statement1 != null) {
statement1.close()
}
if (statement != null) {
statement.close()
}
if (connection != null) {
connection.close()
}
}
})
2.redis
kafkaDstream.foreachRDD(rdd => {
//通过第一个rdd获取到偏移量信息
val ranges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
//获取数据并写处理逻辑
val rs: RDD[(String, Int)] = rdd.map(_.value()).flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
//收集到driver端
val rsArr: Array[(String, Int)] = rs.collect()
var jedis: Jedis = null
var pipeline: Pipeline = null
try {
jedis = JedisConnectionPool.getJedisConnection()
//开启事务
pipeline = jedis.pipelined()
//支持多个
pipeline.multi()
for (elem <- rsArr) {
pipeline.hincrBy("WORD_COUNT", elem._1, elem._2.toLong)
println("=========================")
}
//将偏移量跟新进redis中
for (elem <- ranges) {
val topic: String = elem.topic
val partition: Int = elem.partition
val offset: Long = elem.untilOffset
pipeline.hset(appName + "_" + groupId, topic + "_" + partition, offset.toString)
println("++++++++++++++++++++++++++")
}
//提交事务
pipeline.exec()
pipeline.sync()
} catch {
case e: Exception => {
if (pipeline != null) {
pipeline.discard()
}
JedisConnectionPool.stop()
ssc.stop(true, false)
}
} finally {
if (pipeline != null) {
pipeline.close()
}
if (jedis != null) {
jedis.close()
}
}
})
注意:redis中的事务使用的是pipelined
2. 幂等性
使用幂等性的情况一般都是数据量很大,内存存不下,同时没有聚合操作,才使用幂等性。
事务保证Exactly-Once的过程
- 将偏移量和计算好的结果同时写入hbase中的一行(es等数据库也可以)
- 如果数据写入成功,但是偏移量没有更新成功,可以接着上一次成功的偏移量消费,覆盖原来的数据
- 数据本身必须携带一个唯一标识,作为Hbase的rowkey
hbase
//第一手的DSteam中的KafkaRDD才有偏移量
kafkaDStream.foreachRDD(rdd => {
if (!rdd.isEmpty()) {
//将RDD强转成HasOffsetRanges目的就是为了获取偏移量
//在Driver端获取没一个批次KafkaRDD的偏移量
val offsetRanges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
val lines: RDD[String] = rdd.map(_.value())
//不能shuffle
val beanRDD: RDD[OrderBean] = lines.map(line => {
var bean: OrderBean = null
try {
bean = JSON.parseObject(line, classOf[OrderBean])
} catch {
case e: JSONException => {
//记录日志
e.printStackTrace()
}
}
bean
})
//过滤数据
val filtered = beanRDD.filter(bean => bean != null)
//将数据写入Hbase
filtered.foreachPartition(it => {
val i = TaskContext.get().partitionId()
val offsetRange: OffsetRange = offsetRanges(i)
val connection = HBaseUtil.getConnection("node-1.51doit.cn,node-2.51doit.cn,node-3.51doit.cn", 2181)
val table = connection.getTable(TableName.valueOf("orderdoit"))
//设置rowkey
val puts = new util.ArrayList[Put](5)
it.foreach(bean => {
//构建一个put,并设置rowkey
val put = new Put(Bytes.toBytes(bean.oid))
//将数据放入列族中
put.addColumn(Bytes.toBytes("data"), Bytes.toBytes("total_money"), Bytes.toBytes(bean.totalMoney))
if(!it.hasNext) {
//将偏移量放入列族中
put.addColumn(Bytes.toBytes("offset"), Bytes.toBytes("groupid"), Bytes.toBytes(groupId))
put.addColumn(Bytes.toBytes("offset"), Bytes.toBytes("topic_partition"), Bytes.toBytes(offsetRange.topic + "_" + offsetRange.partition))
put.addColumn(Bytes.toBytes("offset"), Bytes.toBytes("offset"), Bytes.toBytes(offsetRange.untilOffset))
}
puts.add(put)
if(puts.size() == 5) {
table.put(puts)
puts.clear()
}
})
table.put(puts)
table.close()
connection.close()
})
}
})
由于hbase的特性,在hbase中如果一次写入的数据有一行失败了,就将重新重写,因此不会出现数据重复。