1.updateStateByKey
代码如下:
//消费者配置,及读取日志过程省略..........
//输出数据格式,例如(20200328224742,(1,858,1))
li=(time,(flag.toInt,flag.toInt*fee.toInt,1))
// 这里是以时间为K,将K一样的V聚集成一个列表seq,当前K对应的状态V为state,然后只对V做运算,输出也只输出V。这里k会将当前阶段流中的K对应的v作为一个的列表,统计每分钟的销售额,。
val funupdate=(v:Seq[(Int,Int,Int)],state:Option[(Int,Int,Int)])=>{
var i1:Int=0
var i2:Int=0
var i3:Int=0
for(i<-v){
i1+=i._1
i2+=i._2
i3+=i._3
}
val currentc=(i1,i2,i3)
val prec=state.getOrElse((0,0,0))// 上一个批次的该状态的值
//输出值只是V,没有K的信息
Some(currentc._1+prec._1,currentc._2+prec._2,currentc._3+prec._3)//返回值结果
}
li.map(i=>(i._1.substring(10,12),(i._2)))
.updateStateByKey(funupdate)
.map(i=> ("时间:"+i._1,"订单总额: "+i._2._2+")",
"\n(时间:"+i._1,"成功订单总数: "+i._2._1,"订单总数: "+i._2._3,"成功订单占比: "+(i._2._1.toDouble/i._2._3.toDouble*100)+"%"))
.print
计算结果:
小结:
1、updateStateByKey共有6中方法重载方法。
2、更新函数两个参数Seq[V], Option[S],前者是每个key新增的值Value的集合,后者是当前保存的状态。
3、updateStateByKey会统计全局的key的状态,但是就算没有数据输入,他也会在每一个批次的时候返回之前的key的状态。这样的缺点:如果数据量太大的话,我们需要checkpoint数据会占用较大的存储,同时会产生很多小文件,对HDFS不太友好。而且效率也不高,数据很多的时候不建议使用updateStateByKey。
2.mapWithState
代码如下:
//消费者配置,及读取日志过程省略..........
//输出数据格式,例如(20200328224742,(1,858,1))
li=(time,(flag.toInt,flag.toInt*fee.toInt,1))
//每个(K,V)都会传入一次,和updateStateByKey不一样,不会提前自动reduce,必须自己reduce.
def mappfunc(key: String, value: Option[(Int, Int,Int)], state: State[(Int, Int, Int)]): (String,(Int, Int, Int)) = {
// 当前值加上上一个批次的该状态的值
val v1: Int = value.getOrElse((0, 0, 0))._1 + state.getOption.getOrElse((0, 0, 0))._1
val v2: Int = value.getOrElse((0, 0, 0))._2 + state.getOption.getOrElse((0, 0, 0))._2
val v3: Int = value.getOrElse((0, 0, 0))._3 + state.getOption.getOrElse((0, 0, 0))._3
state.update((v1,v2,v3)) // 更新当前的状态
(key, (v1,v2,v3))//返回值结果
}
//mappfunc _:这里的下划线是将方法转化为函数,即将mappfunc方法转化为一个函数
li.map(i=>(i._1.substring(10,12),(i._2))).reduceByKey((i,j)=>(i._1+j._1,i._2+j._2,i._3+j._3)).mapWithState(StateSpec.function(mappfunc _))
.map(i=> ("时间:"+i._1,"订单总额: "+i._2._2+")",
"\n(时间:"+i._1,"成功订单总数: "+i._2._1,"订单总数: "+i._2._3,"成功订单占比: "+(i._2._1.toDouble/i._2._3.toDouble*100)+"%"))
.print
计算结果:
小结:
1、mapWithState是1.6版本之后推出的
2、必须设置checkpoint来储存历史数据(会保存状态及offset)
3、mapWithState和updateStateByKey的区别 : 他们类似,都是有状态DStream操作, 区别在于,updateStateByKey是输出增量数据,随着时间的增加, 输出的数据越来越多,这样会影响计算的效率, 对CPU和内存压力较大.而mapWithState则输出本批次数据,但是也含有状态更新.
4、mapWithstate底层是创建了一个MapWithStateRDD,存的数据是MapWithStateRDDRecord对象,一个Partition对应一个MapWithStateRDDRecord对象,该对象记录了对应Partition所有的状态,每次只会对当前batch有的数据进行跟新,而不会像updateStateByKey一样对所有数据计算。
3.offset存储
3.1、checkpoint:
1、原理:checkpoint是对sparkstreaming运行过程中的元数据和 每次rdds的数据状态保存到一个持久化系统中,当然这里面也包含了offset,如果程序挂了,或者集群挂了,下次启动仍然能够从checkpoint中恢复,从而做到生产环境的7*24高可用。
2、配置:自动提交,设置enable.auto.commit=true,更新的频率根据参数【auto.commit.interval.ms】来定。这种方式也被称为【at most once】,fetch到消息后就可以更新offset,无论是否消费成功。可以通过ssc.checkpoint(地址)配置存的地址。
3、弊端:一旦你的流式程序代码或配置改变了,或者更新迭代新功能了,这个时候,你先停旧的sparkstreaming程序,然后新的程序打包编译后执行运行,会发现两种情况: (1)启动报错,反序列化异常 (2)启动正常,但是运行的代码仍然是上一次的程序的代码。
为什么会出现上面的两种情况,这是因为checkpoint第一次持久化的时候会把整个相关的jar给序列化成一个二进制文件,每次重启都会从里面恢复,但是当你新的 程序打包之后序列化加载的仍然是旧的序列化文件,这就会导致报错或者依旧执行旧代码。如果直接把上次的checkpoint删除了,但是一旦你删除了旧的checkpoint,新启动的程序,只能从kafka的smallest或者largest的偏移量消费,默认是从最新的,如果是最新的,而不是上一次程序停止的那个偏移量 就会导致有数据丢失,如果是老的,那么就会导致数据重复。不管怎么样搞,都有问题。
3.2、自己维护:
1、方式:可以用zookeeper、mysql、checkpoint、Hbase等等。
2、手动提交,设置enable.auto.commit=false,这种方式称为【at least once】。fetch到消息后,等消费完成再调用方法【consumer.commitSync()】,手动更新offset;如果消费失败,则offset也不会更新,此条消息会被重复消费一次。
3、使用zk维护offset也是比较不错的选择,如果将checkpoint存储在HDFS上,每隔几秒都会向HDFS上进行一次写入操作而且大部分都是小文件,且不说写入性能怎么样,就小文件过多,对整个Hadoop集群都不太友好。因为只记录偏移量信息,所以数据量非常小,zk作为一个分布式高可靠的的内存文件系统,非常适合这种场景。详情参照博客
4、存入checkpoint、kafka(1.0.1)、redis可参照博客。
实例:交由mysql 维护:
代码如下:
package streaming.read
import java.io.File
import java.sql.{DriverManager, ResultSet}
import com.alibaba.fastjson.JSON
import com.typesafe.config.ConfigFactory
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, HasOffsetRanges, KafkaUtils, OffsetRange}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.immutable.Map
import scala.collection.mutable
object test {
def main(args: Array[String]): Unit = {
//配置统一化管理
val config = ConfigFactory.parseFile(new File("F:\\Bigdata\\scalawork\\spark-maven\\src\\main\\scala\\streaming\\stream.conf"))
StreamingExamples.setStreamingLogLevels()
val conf = new SparkConf()
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer").setAppName("Kafkajson").setMaster("local[2]")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")
//Streaming以2s为间隔将数据进行分批
val ssc = new StreamingContext(sc, Seconds(5))
//设置检查点,如果存放在HDFS上面,则写成类似ssc.checkpoint("/user/hadoop/checkpoint")这种形式,但是,要启动hadoop
ssc.checkpoint(config.getString("kafka.checkpointdir"))
//Zookeeper服务器地址
val zkQuorum = config.getString("kafka.broker.list")
//topic所在的group,可以设置为自己想要的名称,
val group = config.getString("kafka.group.id")
//topics的名称
val topics = List(config.getString("kafka.topic"))
val numThreads = 1 //每个topic的分区数
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> zkQuorum,
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> group,
//(1)earliest :会从该分区当前最开始的offset消息开始消费(即从头消费),如果最开始的消息offset是0,那么消费者的offset就会被更新为0.
//(2)latest:只消费当前消费者启动完成后生产者新生产的数据。旧数据不会再消费。offset被重置为分区的HW。
//(3)none:....
"auto.offset.reset" -> "latest", //earliest
"enable.auto.commit" -> (false: java.lang.Boolean) //true:定期帮自动提交位移的。自己来管理位移提交,则设置flase
)
//2.使用KafkaUtil连接Kafak获取数据
//注意:
//如果MySQL中没有记录offset,则直接连接,从latest开始消费
//如果MySQL中有记录offset,则应该从该offset处开始消费
val offsetMap: mutable.Map[TopicPartition, Long] = OffsetUtil.getOffsetMap(group, topics(0))
for (i <- offsetMap) {
println(i)
}
val messages: InputDStream[ConsumerRecord[String, String]] = if (offsetMap.size > 0) { //有记录offset
println("MySQL中记录了offset,则从该offset处开始消费")
KafkaUtils.createDirectStream[String, String](ssc,
PreferConsistent, //位置策略,源码强烈推荐使用LocationStrategies或者PreferConsistent,会让Spark的Executor和Kafka的Broker均匀对应
ConsumerStrategies.Subscribe[String, String](topics, kafkaParams, offsetMap)) //消费策略,源码强烈推荐使用该策略
} else { //没有记录offset
println("没有记录offset,则直接连接,从latest开始消费")
// /opt/soft/kafka/bin/kafka-console-producer.sh --broker-list node-01:9092 --topic spark_kafka
KafkaUtils.createDirectStream[String, String](ssc,
PreferConsistent, //位置策略,源码强烈推荐使用LocationStrategies或者PreferConsistent,会让Spark的Executor和Kafka的Broker均匀对应
ConsumerStrategies.Subscribe[String, String](topics, kafkaParams)) //消费策略,源码强烈推荐使用该策略
}
// val messages = KafkaUtils.createDirectStream[String, String](
// ssc, PreferConsistent, Subscribe[String, String](topics, kafkaParams))
//就是将里面的内容读取出来。
// messages.print()
val li = messages.map(x => {
val js = JSON.parseObject(x.value)
val time = js.getString("time")
val userid = js.getString("userid")
val courseid = js.getString("courseid")
val fee = js.getString("fee")
val flag = js.getString("flag")
(time, (flag.toInt, flag.toInt * fee.toInt, 1))
}).cache()
val funupdate = (v: Seq[(Int, Int, Int)], state: Option[(Int, Int, Int)]) => {
var i1: Int = 0
var i2: Int = 0
var i3: Int = 0
for (i <- v) {
i1 += i._1
i2 += i._2
i3 += i._3
}
val currentc = (i1, i2, i3)
val prec = state.getOrElse((0, 0, 0))
Some(currentc._1 + prec._1, currentc._2 + prec._2, currentc._3 + prec._3) //输出值只是V,没有K的信息
}
val lii = li.map(i => (i._1.substring(10, 12), (i._2)))
.updateStateByKey(funupdate)
.map(i => ("时间:" + i._1, "订单总额: " + i._2._2 + ")",
"\n(时间:" + i._1, "成功订单总数: " + i._2._1, "订单总数: " + i._2._3, "成功订单占比: " + (i._2._1.toDouble / i._2._3.toDouble * 100) + "%")).cache
lii.print
messages.foreachRDD(rdd => {
if (rdd.count() > 0) { //当前这一时间批次有数据
//接收到的Kafk发送过来的数据为:ConsumerRecord(topic = spark_kafka, partition = 1,
// offset = 6, CreateTime = 1565400670211, checksum = 1551891492, serialized key size = -1,
// serialized value size = 43, key = null, value = hadoop spark ...)
//rdd.foreach(record => println("接收到的Kafk发送过来的数据为:" + record))
//spark提供了一个类,帮我们封装offset的数据
val offsetRanges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
for (o <- offsetRanges) {
println(s"topic=${o.topic},partition=${o.partition},fromOffset=${o.fromOffset},untilOffset=${o.untilOffset}")
}
//将偏移量存入mysql中
OffsetUtil.saveOffsetRanges(group, offsetRanges)
}
})
ssc.start()
ssc.awaitTermination
}
}
object OffsetUtil {
/**
* 从数据库读取偏移量
*/
def getOffsetMap(groupid: String, topic: String) = {
//获取mysql连接
Class.forName("com.mysql.jdbc.Driver")
val connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?tensquare_article?characterEncoding=utf8&useSSL=false", "root", "root")
//没有就创建新表
val sql = "create table if not exists offset(" +
"topic varchar(50) primary key, partition_1 int, groupid varchar(50), offset_1 BIGINT)"
//num int primary key AUTO_INCREMENT,
connection.prepareStatement(sql).executeUpdate()
print(groupid, topic)
val pstmt = connection.prepareStatement("select * from offset where groupid=? and topic=?")
pstmt.setString(1, groupid)
pstmt.setString(2, topic)
//方法executeQuery:返回查询结果;
val rs: ResultSet = pstmt.executeQuery()
val offsetMap = mutable.Map[TopicPartition, Long]()
while (rs.next()) {
offsetMap += new TopicPartition(rs.getString("topic"), rs.getInt("partition_1")) -> rs.getLong("offset_1")
}
rs.close()
pstmt.close()
connection.close()
offsetMap
}
/**
* 将偏移量保存到数据库
*/
def saveOffsetRanges(groupid: String, offsetRange: Array[OffsetRange]) = {
//获取mysql连接
Class.forName("com.mysql.jdbc.Driver")
val connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?tensquare_article?characterEncoding=utf8&useSSL=false", "root", "root")
//replace into表示之前有就替换,没有就插入
val pstmt = connection.prepareStatement("replace into offset (`topic`, `partition_1`, `groupid`, `offset_1`) values(?,?,?,?)")
for (o <- offsetRange) {
pstmt.setString(1, o.topic)
pstmt.setInt(2, o.partition)
pstmt.setString(3, groupid)
pstmt.setLong(4, o.untilOffset)
//方法executeUpdate:适用于数据定义语言;
pstmt.executeUpdate()
}
pstmt.close()
connection.close()
}
}
结果:
KafkaProducer的数据:
读取offset结果:
参考博客
https://blog.csdn.net/lmb09122508/article/details/80537881