Spark-Streaming的ExactlyOnce

1.SparkStreamming介绍

SparkStreaming是spark技术栈中做实时处理的工具,是一个微批次准实时的流式处理引擎。
我们在做实时处理的时候,通过kafka采集数,将数据读入sparkstreamming中进行处理。

2.整合kafka

方式 1 Receiver方式

这个方式已经过时,不建议使用

缺点:

  1. Receiver方式为了保证数据安全,将数据写入磁盘记录日志【write Ahead Log】,这种写入效率十分低下。
  2. 同时使用老版本的kafka消费API,先连接zookeeper,并将偏移量记录在Zookeeper中,效率低下。

方式 2 直连方式

推荐版本:kafka 0.10版本以上

优点:

  1. 直连方式使用kafka底层高效的API,不再经过zookeeper,效率高。
  2. 在直连方式中,kafka topic的一个分区对应一个RDD分区(这里的分区指的是leader分区),即RDD分区的数量和kafka的分区数量一一对应,生成的Task直接连到Kafka topic的Leader分区拉取数据,一个消费者Task对应一个Kafka分区。
  3. 直连方式中可以自己编程保存偏移量

3.Exactly-Once

在实时处理数据时,要保证数据的一次性语义,防止出现有的成功了,有的失败了,导致数据错误,比如银行转账,当小明转给小王100元钱的时候,银行系统出现了故障,导致小明少了100元,小王缺没有多100元,这种事情谁都不乐意见到是不,所以在实时处理中我们有俩种Exactly-Once的保存方式:事务和幂等性。
虽然kafka会自动提交偏移量,但是kafka保证不了Exactly-Once,因此我们公司一般都自动提交偏移量,利用Mysql、Redis或者Hbase保存。

1. 事务

利用事务做Exactly-Once首先要保证是聚合过的数据,只有聚合过的数据的数据量才不会很大,降低了内存溢出的几率。

事务保证Exactly-Once的过程

  1. 数据经过聚合以后,数据量变的很小,可以收集到Driver端
  2. 在Driver端利用第一个kafkaRDD获取到kafka的偏移量,然后将计算好的结果和偏移量,使用支持事务的数据库,在同一个事务中将偏移量和计算好的数据写入到数据库,保证同时成功。
  3. 如果失败,让任务重启,接着上一次成功的偏移量继续读取。
    代码如下:
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的过程

  1. 将偏移量和计算好的结果同时写入hbase中的一行(es等数据库也可以)
  2. 如果数据写入成功,但是偏移量没有更新成功,可以接着上一次成功的偏移量消费,覆盖原来的数据
  3. 数据本身必须携带一个唯一标识,作为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中如果一次写入的数据有一行失败了,就将重新重写,因此不会出现数据重复。

完整代码

完整代码

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值