sparkstreaming的exactly once
spark的exactly once
1.利用mysql 的幂等性
注:spark整合kafka可以实现exactly once,一种是事物性,另一种是幂等性
绍幂: 幂等性就是未聚和的,在executor端获取偏移量,将偏移量和计算结果写入到ES或者Hbase,如果数据写入成功,但是偏移量未更新成功,覆盖原来的数据。
事物:数据经过聚合后,数据已经变得很少,可以将计算好的结果收集到driver端,在Driver端获取偏移量,然后将计算好的结果和偏移量使用支持事务的数据库,在同一个事务中将计算好的数据和偏移量写入到数据库,保证同时成功。如果失败,让任务重启,接着上一次成功的偏移量继续读取。
在 DRDS 上,可以将事务作以下的简单分类:
- 单机事务:所有的事务操作都落在同一个 RDS 数据库;
- 跨库事务:事务的操作涉及到多个 RDS 数据库。
hbase只支持单行单词操作的事务。
mysql数据库:
MySQL是否支持事务要看用的是什么存储引擎。
在缺省模式下,MYSQL是autocommit模式的,所有的数据库更新操作都会即时提交,所以在缺省情况下,mysql是不支持事务的。
但是如果你的MYSQL表类型是使用InnoDB Tables 或 BDB tables的话,你的MYSQL就可以使用事务处理,使用SET AUTOCOMMIT=0就可以使MYSQL允许在非autocommit模式,
在非autocommit模式下,你必须使用COMMIT来提交你的更改,或者用ROLLBACK来回滚你的更改。
2. 数据库的事务
数据库事务(Database Transaction)是指作为单个逻辑工作单元执行的一系列操作,要么完整地执行,要么完全地不执行。 正常的情况下,这些操作将顺利进行,最终交易成功,与交易相关的所有数据库信息也成功地更新。但是,如果在这一系列过程中任何一个环节出了差错,数据库中所有信息都必须保持交易前的状态不变,比如最后一步更新用户信息时失败而导致交易失败,那么必须保证这笔失败的交易不影响数据库的状态–库存信息没有被更新、用户也没有付款,订单也没有生成。否则,数据库的信息将会一片混乱而不可预测。
2.1事务的四个特性
原子性
事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。通常,与某个事务关联的操作具有共同的目标,并且是相互依赖的。如果系统只执行这些操作的一个子集,则可能会破坏事务的总体目标。原子性消除了系统处理操作子集的可能性。
一致性
事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如 B 树索引或双向链表)都必须是正确的。某些维护一致性的责任由应用程序开发人员承担,他们必须确保应用程序已强制所有已知的完整性约束。例如,当开发用于转帐的应用程序时,应避免在转帐过程中任意移动小数点。
隔离性
由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。这称为隔离性,因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同。当事务可序列化时将获得最高的隔离级别。在此级别上,从一组可并行执行的事务获得的结果与通过连续运行每个事务所获得的结果相同。由于高度隔离会限制可并行执行的事务数,所以一些应用程序降低隔离级别以换取更大的吞吐量。
持久性
事务完成之后,它对于系统的影响是永久性的。该修改即使出现致命的系统故障也将一直保持。
3. 代码实现大致过程
注:如需详细代码请联系博主
3.1ExactlyOnceWordCount
import java.sql.{DriverManager, PreparedStatement, SQLException}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, HasOffsetRanges, KafkaUtils, LocationStrategies, OffsetRange}
import org.apache.spark.streaming.{Milliseconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
object ExactlyOnceWordCount {
def main(args: Array[String]): Unit = {
val gid = "g008"
val appName = this.getClass.getSimpleName
val conf = new SparkConf().setAppName(appName).setMaster("local[*]")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc: StreamingContext = new StreamingContext(sc, Milliseconds(5000))
val topics = Array("doitwc")
//SparkSteaming跟kafka整合的参数
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "node-1.51.cn:9092,node-2.51.cn:9092,node-3.51.cn:9092",
"key.deserializer" -> classOf[StringDeserializer].getName,
"value.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer",
"group.id" -> gid,
"auto.offset.reset" -> "earliest", //如果没有记录偏移量,第一次从最开始读,有偏移量,接着偏移量读
"enable.auto.commit" -> (false: java.lang.Boolean) //消费者不自动提交偏移量
)
//查询历史偏移量【上一次成功写入到数据库的偏移量】
val historyOffsets: Map[TopicPartition, Long] = OffsetUtils.queryHistoyOffsetFromMySQL(appName, gid)
//跟Kafka进行整合,需要引入跟Kafka整合的依赖
//createDirectStream更加高效,使用的是Kafka底层的消费API,消费者直接连接到Kafka的Leader分区进行消费
//直连方式,RDD的分区数量和Kafka的分区数量是一一对应的【数目一样】
val kafkaDStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](
ssc,
LocationStrategies.PreferConsistent, //调度task到Kafka所在的节点
ConsumerStrategies.Subscribe[String, String](topics, kafkaParams, historyOffsets) //指定订阅Topic的规则, 从历史偏移量接着读取数据
)
//对数据进行处理,使用直连方式,一般都是将DStream中的RDD进行运算
//跟Kafka整合,获取到kafkaDStream,然后直接调用foreachRDD,获取DStream对应的RDD
kafkaDStream.foreachRDD(rdd => {
if (!rdd.isEmpty()) {
//获取KakfaRDD的偏移量
val ranges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
//获取KafkaRDD中的数据
val lines: RDD[String] = rdd.map(_.value())
val words = lines.flatMap(_.split(" "))
val wordAndOne = words.map((_, 1))
val reduced = wordAndOne.reduceByKey(_ + _)
//将计算好的结果收集到Driver【因为该程序时一个聚合类的运算】
//为什么将计算好的结果收集到Driver端,因为偏移量在Driver端获取的
//在Driver端获取一个数据库连接,将数据写入到数据库保证在同一个事物
val reslut: Array[(String, Int)] = reduced.collect()
//获取数据库连接【MySQL】
val connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/bigdata?characterEncoding=UTF-8", "root", "123456")
//开启数据库的事物,设置不自动提交事物
connection.setAutoCommit(false)
//将计算好的结果和偏移量信息都写入到数据库中【使用同一个连接,在同一个事物中】
var ps1: PreparedStatement = null
var ps2: PreparedStatement = null
try {
//先写计算好的结果
ps1 = connection.prepareStatement("INSERT INTO t_wordcount VALUES (?, ?) ON DUPLICATE KEY UPDATE counts = counts + ?")
reslut.foreach(t => {
ps1.setString(1, t._1) //单词
ps1.setInt(2, t._2) //次数
ps1.setInt(3, t._2)
ps1.executeUpdate()
})
//再将偏移量写入到数据库
ps2 = connection.prepareStatement("INSERT INTO t_kafka_offset (app_gid, topic_partition, offset) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE offset = ?")
//获取每一个分区的偏移量
for(range <- ranges) {
//获取topic名称
val topic = range.topic
//获取分区编号
val partition = range.partition
//获取偏移量
val untilOffset = range.untilOffset
ps2.setString(1, appName + "_" + gid)
ps2.setString(2, topic + "_" + partition)
ps2.setLong(3, untilOffset)
ps2.setLong(4, untilOffset)
ps2.executeUpdate()
}
//提交事物
connection.commit()
} catch {
case e: SQLException => {
e.printStackTrace()
connection.rollback()
//停掉该程序
ssc.stop()
}
} finally {
if(ps1 != null){
ps1.close()
}
if(ps2 != null) {
ps2.close()
}
if(connection != null) {
connection.close()
}
}
}
})
ssc.start()
ssc.awaitTermination()
}
}
offsetutils代码
import java.sql.{DriverManager, ResultSet}
import org.apache.kafka.common.TopicPartition
import scala.collection.mutable
object OffsetUtils {
//每一次启动该程序,都要从MySQL查询历史偏移量
def queryHistoyOffsetFromMySQL(appName: String, gid: String): Map[TopicPartition, Long] = {
val offsets = new mutable.HashMap[TopicPartition, Long]()
val connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/bigdata?characterEncoding=UTF-8", "root", "123456")
val ps = connection.prepareStatement("SELECT topic_partition, offset FROM t_kafka_offset WHERE app_gid = ?")
ps.setString(1, appName + "_" + gid)
//查询返回结果
val rs: ResultSet = ps.executeQuery()
while(rs.next()) {
val topicAndParition = rs.getString(1)
val offset = rs.getLong(2)
val fields = topicAndParition.split("_")
val topic = fields(0)
val parition = fields(1).toInt
offsets.put(new TopicPartition(topic, parition), offset)
}
offsets.toMap
}
}
3.2更新Kafka的偏移量到Kafka的特殊分区中【__consumer_offset】
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010._
import org.apache.spark.streaming.{Milliseconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
/**
* 更新Kafka的偏移量到Kafka的特殊分区中【__consumer_offset】
**/
object UpdateOffsetToKafka {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("KafkaSourceWordCount").setMaster("local[*]")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc: StreamingContext = new StreamingContext(sc, Milliseconds(5000))
val topics = Array("wc123")
//SparkSteaming跟kafka整合的参数
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "node-1.51doit.cn:9092,node-2.51doit.cn:9092,node-3.51doit.cn:9092",
"key.deserializer" -> classOf[StringDeserializer].getName,
"value.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer",
"group.id" -> "g003",
"auto.offset.reset" -> "earliest", //如果没有记录偏移量,第一次从最开始读,有偏移量,接着偏移量读
"enable.auto.commit" -> (false: java.lang.Boolean) //消费者不自动提交偏移量
)
//跟Kafka进行整合,需要引入跟Kafka整合的依赖
//createDirectStream更加高效,使用的是Kafka底层的消费API,消费者直接连接到Kafka的Leader分区进行消费
//直连方式,RDD的分区数量和Kafka的分区数量是一一对应的【数目一样】
//返回的KafkaDStream是在哪一端?Driver端
val kafkaDStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](
ssc,
LocationStrategies.PreferConsistent, //调度task到Kafka所在的节点
ConsumerStrategies.Subscribe[String, String](topics, kafkaParams) //指定订阅Topic的规则
)
//对数据进行处理,使用直连方式,一般都是将DStream中的RDD进行运算
//跟Kafka整合,获取到kafkaDStream,然后直接调用foreachRDD,获取DStream对应的RDD
//foreachRDD方法是在Driver端调用的,传入到foreachRDD中的函数也是在Driver端调用的,这个函数会被周期性的执行
kafkaDStream.foreachRDD(rdd => {
//如果KafkaDStream调用foreachRDD,那么取出来的RDD就是KafkaRDD类型
//判断当前RDD是否为空
if (!rdd.isEmpty()) {
//获取当前RDD对应Kafka分区的偏移量,RDD每一个分区都会对应一个分区的偏移量信息
val offsetRanges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
val lines: RDD[String] = rdd.map(_.value())
val reduced = lines.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
//将计算好的结果收集到Driver【聚合类的运算】
val r = reduced.collect()
//将计算好的结果写入到数据库,写入到数据库可以写入成功,但是偏移量由可能写入失败
//为了保证Exactly-Once,Spark计算好的结果和偏移量信息要同时写入成功,要失败都要回滚
//为了保证在同一个事物中,我要将结果和偏移量都保存到MySQL中
//更新这个批次的偏移量
//可以将RDD每个分区的偏移量更新到Kafka特殊的Topic【__consumer_offsets】
kafkaDStream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
}
})
ssc.start()
ssc.awaitTermination()
}
}