Spark實時項目第三天-精準一次消費|手動提交偏移

精確一次消費

在這裏插入圖片描述

問題產生

在這裏插入圖片描述

解決方案

在這裏插入圖片描述

手動提交偏移量

在這裏插入圖片描述

用Redis保存偏移量原因

在這裏插入圖片描述

編寫OffsetManagerUtil

在scala\com\atguigu\gmall\realtime\utils\OffsetManagerUtil

import java.util

import org.apache.kafka.common.TopicPartition
import org.apache.spark.streaming.kafka010.OffsetRange
import redis.clients.jedis.Jedis
import  scala.collection.JavaConversions._
object OffsetManagerUtil {


  /**
   * 從Redis中讀取偏移量
   * @param groupId
   * @param topic
   * @return
   */
  def getOffset(groupId:String,topic:String):Map[TopicPartition,Long]={
    var offsetMap=Map[TopicPartition,Long]()

    val jedisClient: Jedis = RedisUtil.getJedisClient

    val redisOffsetMap: util.Map[String, String] = jedisClient.hgetAll("offset:"+groupId+":"+topic)
    jedisClient.close()
    if(redisOffsetMap!=null&&redisOffsetMap.isEmpty){
      null
    }else {

      val redisOffsetList: List[(String, String)] = redisOffsetMap.toList

      val kafkaOffsetList: List[(TopicPartition, Long)] = redisOffsetList.map { case ( partition, offset) =>
        (new TopicPartition(topic, partition.toInt), offset.toLong)
      }
      kafkaOffsetList.toMap
    }
  }

  /**
   * 偏移量寫入到Redis中
   * @param groupId
   * @param topic
   * @param offsetArray
   */
  def saveOffset(groupId:String,topic:String ,offsetArray:Array[OffsetRange]):Unit= {
    if (offsetArray != null && offsetArray.size > 0) {
      val offsetMap: Map[String, String] = offsetArray.map { offsetRange =>
        val partition: Int = offsetRange.partition
        val untilOffset: Long = offsetRange.untilOffset
        (partition.toString, untilOffset.toString)
      }.toMap

      val jedisClient: Jedis = RedisUtil.getJedisClient

      jedisClient.hmset("offset:" + groupId + ":" + topic, offsetMap)
      jedisClient.close()
    }
  }
}

在這裏插入圖片描述

DauApp完整代碼

import com.atguigu.gmall.realtime.utils.{MyEsUtil, MyKafkaUtil, OffsetManagerUtil, RedisUtil}
import java.lang
import java.text.SimpleDateFormat
import java.util.Date

import com.alibaba.fastjson.{JSON, JSONObject}
import com.atguigu.gmall.realtime.bean.DauInfo
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.TopicPartition
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.{HasOffsetRanges, OffsetRange}
import org.apache.spark.streaming.{Seconds, StreamingContext}

import scala.collection.mutable.ListBuffer

object DauApp {

  // 兩個問題 1 數組下標i 和 分區數 不對應   保存的時候應該使用 offsetRange.partition 來存分區編號
  // 2   redis的Key的日期( 發送日誌的業務) 和 es存儲的索引後綴日期(當前系統日期) 沒有對應 取同一天
  def main(args: Array[String]): Unit = {
    val sparkConf: SparkConf = new SparkConf().setAppName("dau_app").setMaster("local[*]")
    val ssc = new StreamingContext(sparkConf,Seconds(5))
    // 自定義消費topic 和 groupId
    val topic="GMALL_START"
    val groupId="GMALL_DAU_CONSUMER"

    // 用OffsetManagerUtil.getOffset()方法,獲取偏移量 -> 爲達到手動提交偏移量做鋪墊
    val startOffset: Map[TopicPartition, Long] = OffsetManagerUtil.getOffset(groupId,topic)

    // Kafka消費數據
    var startInputDstream: InputDStream[ConsumerRecord[String, String]]=null
    // 判斷如果從redis中讀取當前最新偏移量 則用該偏移量加載kafka中的數據  否則直接用kafka讀出默認最新的數據
    if(startOffset!=null&&startOffset.size>0){
      startInputDstream = MyKafkaUtil.getKafkaStream(topic,ssc,startOffset,groupId)
      //startInputDstream.map(_.value).print(1000)
    }else{
      startInputDstream  = MyKafkaUtil.getKafkaStream(topic,ssc,groupId)
    }

    //獲得本批次偏移量的移動後的新位置
    var startupOffsetRanges: Array[OffsetRange] =null
    val startupInputGetOffsetDstream: DStream[ConsumerRecord[String, String]] = startInputDstream.transform { rdd =>
      startupOffsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
      rdd
    }
    //startInputDstream.map(_.value).print(1000)



    // 將數據轉換爲標準JSON格式
    val startJsonObjDstream: DStream[JSONObject] = startInputDstream.map { record =>
      val jsonString: String = record.value()
      val jSONObject: JSONObject = JSON.parseObject(jsonString)
      jSONObject
    }
    //寫入去重清單  日活 每天一個清單 key: 每天一個key   //不夠優化,連接次數較多
    //    startJsonObjDstream.map{jsonObj=>
    //
    //      //Redis  寫入    type?  set        key?    dau:2020-05-12      value?  mid
    //      val dateStr: String = new SimpleDateFormat("yyyyMMdd").format(new Date(jsonObj.getLong("ts")))
    //
    //      val dauKey="dau:"+dateStr
    //      val jedis = new Jedis("hadoop102",6379)
    //      val mid: String = jsonObj.getJSONObject("common").getString("mid")
    //      jedis.sadd(dauKey,mid)
    //      jedis.close()
    //    }

    // Redis去重
    val startJsonObjWithDauDstream: DStream[JSONObject] = startJsonObjDstream.mapPartitions { jsonObjItr =>
      // 獲取連接池
      val jedis = RedisUtil.getJedisClient
      // 轉換成一個JSONObject List
      val jsonObjList: List[JSONObject] = jsonObjItr.toList
      println("過濾前:"+jsonObjList.size)

      // 存儲過濾後的BufferList
      val jsonObjFilteredList = new ListBuffer[JSONObject]()
      // 遍歷
      for (jsonObj <- jsonObjList) {
        // 獲取日誌中的ts字段的時間戳進行格式化日期
        val dateStr: String = new SimpleDateFormat("yyyyMMdd").format(new Date(jsonObj.getLong("ts")))
        // 定義每日的key,不是單純的日期。 -> dau:2020-05-12
        val dauKey = "dau:" + dateStr
        // 獲取日誌中的mid
        val mid: String = jsonObj.getJSONObject("common").getString("mid")
        // 用Redis的插入寫操作,返回一個 0 或 1的值
        val isFirstFlag: lang.Long = jedis.sadd(dauKey, mid)
        // 數值爲1 表示首次插入成功, 數值爲0 表示插入失敗,以此達到去重的目的
        if(isFirstFlag==1L){
          jsonObjFilteredList+=jsonObj
        }

      }
      jedis.close()
      println("過濾後:"+jsonObjFilteredList.size)
      // 返回一個去重的List集合
      jsonObjFilteredList.toIterator
    }
    // startJsonObjWithDauDstream.print(1000)


    // 變換結構
    val dauInfoDstream: DStream[DauInfo] = startJsonObjWithDauDstream.map { jsonObj =>
      val commonJsonObj: JSONObject = jsonObj.getJSONObject("common")
      val dateTimeStr: String = new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date(jsonObj.getLong("ts")))
      // 對日期數據按照" "進行切分
      val dateTimeArr: Array[String] = dateTimeStr.split(" ")
      // 獲取 yyyy-MM-dd
      val dt: String = dateTimeArr(0)
      // 將HH:mm 按照 ":" 進行切分
      val timeArr: Array[String] = dateTimeArr(1).split(":")
      // 提取時分數據
      val hr = timeArr(0)
      val mi = timeArr(1)

      // 對數據進行封裝
      DauInfo(commonJsonObj.getString("mid"),
        commonJsonObj.getString("uid"),
        commonJsonObj.getString("ar"),
        commonJsonObj.getString("ch"),
        commonJsonObj.getString("vc"),
        dt, hr, mi, jsonObj.getLong("ts")
      )

    }

    //要插入gmall1122_dau_info_2020xxxxxx  索引中
    dauInfoDstream.foreachRDD {rdd=>
      // 又區內遍歷
      rdd.foreachPartition { dauInfoItr =>
        // 封裝程一個以mid和DauInfo的對偶元組的List
        val dataList: List[(String, DauInfo)] = dauInfoItr.toList.map { dauInfo => (dauInfo.mid, dauInfo) }
        // 獲取時間
        val dt = new SimpleDateFormat("yyyyMMdd").format(new Date())
        // 建立index gmall1122_dau_info_20200513
        // ES查找 : GET gmall1122_dau_info_20200513/_search
        val indexName = "gmall1122_dau_info_" + dt
        MyEsUtil.saveBulk(dataList, indexName)
      }

      // 偏移量的提交
      OffsetManagerUtil.saveOffset(groupId,topic,startupOffsetRanges)

    }
    ssc.start()
    ssc.awaitTermination()

  }
}

在這裏插入圖片描述

結果測試

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章