SparkStreaming实时数仓——ads层 [精准一次性消费之手动维护+事务]

一、将dws数据存入kafka

 /**
   * 把数据写入kafka,dws层
   */
          rdd.foreachPartition(it => {
            val producer: KafkaProducer[String, String] = MyKafkaUtil_1.getProducer
            it.foreach(orderWide => {
              implicit val f = org.json4s.DefaultFormats
                //调用一个生产者,传入一个对象,对象内传入的参数是topic和value,value需要将orderwide对象转成一个字符串
              producer.send(new ProducerRecord[String,String]("dws_order_wide",Serialization.write(orderWide)))
            })
            producer.close()
          })

二、ads层数据

2.1 需求

Ø 热门品牌统计

Ø 热门品类统计

Ø 热门商品统计

Ø 交易用户性别对比

Ø 交易用户年龄段对比

Ø 交易额省市分布

2.2 思路

  • 存入mysql的数据应该是这样的
聚合时间品牌名品牌(所有商品)分摊总额
2020-10-01 10:10:10华为888888.88
2020-10-01 10:10:15华为999999.99
  • 表的主键:聚合时间+品牌名

2.3 数据的精准一次性消费分析

前面我们精准一次消费是利用了 手动保存偏移量+输出系统的幂等 来实现的

	如何我们把数据写入到mysql, 幂等这块很难保证. 聚合时间+品牌名 当碰到数据重复保存的时候, 这个联合主键是不会重复的, 所以无法满足幂等.
	
	所以精准一次消费需要使用 手动保存偏移量+事务 来实现

	如果使用事务保证精准一次消费, 则数据库必须支持事务, 这就是为什么选择mysql的原因.  而且要把偏移量的保存和数据的保存在同一数据库中来保证他们处于同一事务中.

在drvier还是executor上使用事务?
		如果在executor上使用事务, 则executor之间不能保证同时成功或失败, 需要另外考虑分布式事务, 这个一般不使用
	
		我们采取在driver端开启事务, 所有的数据都通过driver写到mysql. 原因有2点:
1.实现方便, 不容易有bug 
2.数据量小(聚合后的数据), 单独一个driver写入是可以完成的

三、消费dws层数据:dws_order_wide

思路:
1.先读取初始偏移量
2.将偏移量存到mysql中
3.定义一个jdbc工具类,用来从mysql中读取偏移量
4.在offsetmanager方法中加一个从mysql中读取偏移量的方法
5.接收数据,将数据解析
6.将数据写入到mysql,同时将offset写入mysql

3.1 修改BaseAppV4

    //定义一个Map来接收初始偏移量
    val offsets = OffsetManager.readOffsetsFromMysql(groupId, topic)

3.2 定义一个jdbc的工具类

package com.atguigu.realtime.util

import java.sql._

object JDBCUtil {
    //select 。。。 from  t where id =? and name = ?
  def query(url:String, //用户直接写在url里
            //传递的sql语句
            sql:String, 
            //传的是一些条件,用于替换占位
            args:List[Object])
    //得到的类型是一个list集合里有多个map,即
    /*
    多行
    List[Map[..], Map[...], ...]
	每行多列
    Map[列->值, 列-> 值, ...]
    */
    :List[Map[String,Object]]={
    //1.获取到数据库的连接
    val conn: Connection = DriverManager.getConnection(url)
    //2.使用连接得到一个PrepareStatement,预处理语句
    val ps: PreparedStatement = conn.prepareStatement(sql)
    //3.替换占位符
    /*
	(1 to args.length).foreach(i => {
            ps.setObject(i, args(i - 1))
        })
 	*/
    args.zipWithIndex.foreach{
        //这里的i初始值是从0开始,占位符是从1开始,所以这里i要加1
      case (arg,i) =>
        ps.setObject(i + 1 ,arg)
    }
    //4.执行查询
    val resultSet: ResultSet = ps.executeQuery()
    //5.解析查询结果
    var result: List[Map[String, Object]] = List[Map[String, Object]]()
        //获取元数据,就能知道数据中有多少列
    val metaData: ResultSetMetaData = resultSet.getMetaData
    while (resultSet.next()){
      var map: Map[String, Object] = Map[String, Object]()
        //metaData.getColumnCount 得到数据中有多少列
      for(i <- 1 to metaData.getColumnCount){
          //获取每列的列名和列值,取出来作为map的key和value
          //列名只有元数据中才有
        val key: String = metaData.getColumnName(i)
          //值,要在值中拿
        val value:Object = resultSet.getObject(i)
          //将得到的map集合的键值对,加入map集合
        map += key -> value
      }
        //for循环结束后,一行就遍历完了,将最终得到的每个Map集合加入到一个List
      result :+= map
    }
    //关闭连接
    ps.close()
    conn.close()
    //将值返回
    result
  }
}

3.3 在OffsetManager中定义一个方法,从mysql读取偏移量

读的时候没必要用sparkSQl读,因为数据量比较小,所有使用自己封装的jdbc工具类来读写
/**
   *从mysql读取偏移量
   */
  def readOffsetsFromMysql(groupId: String, topic: String): Map[TopicPartition, Long] = {
    val url = "jdbc:mysql://hadoop162:3306/gmall_result?characterEncoding=utf-8&useSSL=false&user=root&password=aaaaaa"
    val sql =
      """
        |select
        | *
        |from ads_offset
        |where group_id=? and topic=?
        |""".stripMargin
    JDBCUtil
            .query(url, sql, List(groupId, topic)) 
      //得到的是一个Map集合,最终的是Map[TopicPartition, Long],现在的是List里面套了一个Map集合,所以需要将List里的map拿出来,做map操作
      //每行一个分区一个offset,
            .map((row:Map[String,Object]) => {
              //object不能直接toInt所以先toString
              //  row(key值)得到分区
              val partition = row("partition_id").toString.toInt
                 //  row(key值)得到偏移量
              val offset = row ("partition_offset").toString.toLong
              new TopicPartition(topic,partition) -> offset
            })
      //query出来的是list,map完之后还是liat,所以要toMap一下,
            .toMap
    }
"最终从mysql得到了偏移量
3.1val offsets = OffsetManager.readOffsetsFromMysql(groupId, topic)就得到了初始偏移量
"

3.4 AdsOrderWideApp

"注意:"
1.在事务里的任务同时执行,同时失败
2.注意SQL方法、apply方法
	--SQL这个方法专门用来执行sql语句
3.batch方法的传参
	调用batch方法,用于替换占位符,数据有很多行,每一行有四个问号,一个Seq就是对四个问号替换的值 
	
		'方法源码:'
		def batch(parameters: scala.collection.Seq[Any]*): SQLBatch = {
    		new SQLBatch(statement, parameters, tags)
  		}
  
batch传入的参数是一个可变参数,我们传的是一个集合,那么就需要将这个集合编程可变参数传过去
 		-- 集合名: _* 
object AdsOrderWideApp extends BaseAppV4{
  override val master: String = "local[2]"
  override val appName: String = "AdsOrderWideApp"
  override val groupId: String = "AdsOrderWideApp"
  override val topic: String = "dws_order_wide"
  override val bachTime: Int = 5

  override def run(ssc: StreamingContext,
                   offsetRanges: ListBuffer[OffsetRange],
                   sourceStream: DStream[String]): Unit = {
    DBs.setup()
    sourceStream
        .map(str => {
          implicit  val f: DefaultFormats.type =org.json4s.DefaultFormats
          val orderWide: OrderWide = JsonMethods.parse(str).extract[OrderWide]
          ((orderWide.tm_id -> orderWide.tm_name),orderWide.final_detail_amount)
        })
        //将相同key的值相加
        .reduceByKey(_ + _)
        .foreachRDD(rdd => {
          rdd.collect().foreach(println)    
            
//时间,日期
val now: LocalDateTime = LocalDateTime.now()
val dateTime = s"${now.toLocalDate} ${now.toLocalTime.toString.substring(0, 8)}"

          val tmAndAmount: Array[Seq[Any]] = rdd
          //将数据拉到driver端
                  .collect()
                  .map{
                    case ((tm_id,tm_name),amount) =>
                      //拼一个Seq
                      Seq(dateTime,tm_id,tm_name,amount)
                  }
            
          val adsOffset: ListBuffer[Seq[Any]] = offsetRanges
            //将偏移量改造
            .map(offsetRange => {
            //untilOffset下一次偏移量
            Seq(groupId,topic,offsetRange.partition,offsetRange.untilOffset)
          })
  
/*********************************************************/
            //这里加implicit的原因就是下面调用apply方法要用
            //代码在driver端执行
          DB.localTx(implicit session => {
            // 这里的代码都会在一个事务中执行
            // 写数据到mysql
            val dataSql =
            """
              |insert into tm_amount values(?, ?, ?, ?)
              |""".stripMargin
            SQL(dataSql).batch(tmAndAmount: _*).apply()
            //throw new UnsupportedOperationException
    //这个抛异常后,检验数据有没写进mysql,
    //如果数据就写进去了,偏移量没写进去,这两就不在事务里
    //这里抛完异常后两个都没写进去,所以在一个事务里
            // 写offset到Mysql
            val offsetSql =
           //i.loli.net/2020/11/24/kR7IuY3Q2sPDCxb.png)
            """
              |replace into ads_offset values(?, ?, ?, ?)
              |""".stripMargin
            SQL(offsetSql).batch(adsOffset: _*).apply()
            println("xxxxxx")
          })
        })
  /*********************************************************/
  }
}
/*     
1.SQL这个方法专门用来执行sql语句
2.调用batch方法,用于替换占位符,数据有很多行,每一行有四个问号,一个Seq就是对四个问号替换的值 
def batch(parameters: scala.collection.Seq[Any]*): SQLBatch = {
    new SQLBatch(statement, parameters, tags)
  }
  batch传入的参数是一个可变参数,我们传的是一个集合,那么就需要将这个集合编程可变参数传过去如下:
 集合名: _*

*/
  • 注意:如何给可变参数传一个集合

image-20201124232700906

  • 主键冲突解决:

    ​ 正常,主键冲突

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CbiYo4oJ-1606233087167)(https://i.loli.net/2020/11/24/NSsLhoj6mDc3kzt.png)]

image-20201124233312347

​ 解决1:

image-20201124233400943

​ 解决2:
image-20201124233502044

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值