精確一次消費
問題產生
解決方案
手動提交偏移量
用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()
}
}