大数据学习三:sparkstreaming中关于mapWithState、updateStateByKey、offset的学习

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/u010454030/article/details/54985740?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

https://blog.csdn.net/lmb09122508/article/details/80537881

https://blog.csdn.net/qq_38483094/article/details/99118140?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

https://blog.csdn.net/jklcl/article/details/85217660

https://blog.csdn.net/weixin_42261227/article/details/100095244?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值