目录

  • 一、实时灵活分析需求
  • 1.需求分析的结果
  • 2.实时场景的关键
  • 二、实现过程
  • 1.使用canal实时采集数据
  • 2.Join的过程
  • 3.双流Join
  • 4.redis的相关建模
  • 三、代码开发


一、实时灵活分析需求

1.需求分析的结果

涉及全文检索,需要使用ES存储数据!

搜索的是商品明细,需要将商品的明细导入到ES!

商品明细:商品明细,男女比例,年龄比例

从Mysql的业务数据中取数据!

数据源: Mysql的业务数据!为了得到明细数据,需要三张表!

select 

	order_detail中的详情, age, gender

from

 order_detail  od  

left join   order_info oi  on  od.order_id = oi.id

left join  user_info u on oi.user_id=u.id

2.实时场景的关键

问题:

由于网络的延迟,可能会造成两个流要Join的数据分别位于两个批次,完美错过!

分别位于两个批次,要么批次早到,要么批次迟到!

二、实现过程

1.使用canal实时采集数据

order_info: 新增和变化

canal采集时,只是为了获取order_info中的user_id,user_id在订单创建时生成,无法被修改!

canal监控时,可以只监控 新增的数据!

order_detail : 新增

user_info : 新增和变化

2.Join的过程

order_info 和order_detail 几乎是同时产生,可以直接进行Join!

order_info 下单的用户,可能是之前产生的,当前canal监控不到,kafka中可能没有当前的用户信息!因此join用户时,需要Join,全量的用户表!

可以从mysql中读取全量的用户表!

可以为了性能,使用canal将所有新增的用户都写入到redis中,也需要保证redis中有全量的用户!从redis中读取用户信息,再和之前的order_info和order_detail join!

3.双流Join

问题:

由于网络的延迟,可能会造成两个流要Join的数据分别位于两个批次,完美错过!

分别位于两个批次,要么批次早到,要么批次迟到!

解决:将由于 早来 或 晚来,导致无法在当前批次 Join上的的数据,进行持久化!例如保证性能,将这部分数据,持久化到redis缓存!

实现过程: ①两个流当前批次的数据先进行Join,能匹配上就匹配上

②当前批次无法Join 成功的 order_detail ,到redis中读取 早到的 order_info 进行 Join

如果还Join 不成功, 说明 order_detail 早来了!

此时,将order_detail 写入redis,供后续的order_info 进行 Join

③每个批次在Join时 , order_info 都需要从redis中查询,是否有早到的 order_detail,如果有就关联

④由于order_info 和 order-detail 是 1对N的关系,所以每个批次的order_info 都需要无条件地写入redis, 等候后续迟到的order_detail

因为order_info 和 order_detail都需要缓存到redis,可以根据公司集群的最大延迟,配置key的过期时间!即便不配置也可以,redis可以根据默认LRU的过期策略,清理冷门的key!

⑤使用full outer join 来判断哪些join不上,join不上的会为None

4.redis的相关建模

order_info 都需要无条件地写入redis:

单值: string ,hash

集合:  list, set ,zset

K: 不能是集合类型,因为已经组合成功的 order_info 有自己的过期时间!而不是所有的order_info放入一个集合中,使用同一个过期时间!

从集合中取出后,需要遍历集合才能和order_detail匹配,效率低!

原则: 每个order_info 都 自己唯一的key


order_info:orderid

V:  String


早到的order_detail 写入redis:

一个order_id 对应 多个 order_detail

K: order_id标识

order_detail:orderid

V: Set


用户的维度信息:

一个用户保存一个k-v对

K: user:id

V: 用户的信息以json格式写入

string

三、代码开发

双流join处理

object SaleDetailApp {
  
  def main(args: Array[String]): Unit = {
    
    val streamingContext: StreamingContext = new StreamingContext("local[*]","SaleDetailApp",Seconds(10))
    
    //从kafka获取两个DS
    val ds1: InputDStream[ConsumerRecord[String, String]] = MykafkaUtil.getKafkaStream(GmallConstants.KAFKA_TOPIC_NEW_ORDER, streamingContext)
    val ds2: InputDStream[ConsumerRecord[String, String]] = MykafkaUtil.getKafkaStream(GmallConstants.KAFKA_TOPIC_ORDER_DETAIL,streamingContext)
    
    //从kafka的record中取出数据封装为样例类
    val orderInfoDs: DStream[(String, OrderInfo)] = ds1.map(recoord => {
      val orderInfo: OrderInfo = JSON.parseObject(recoord.value(), classOf[OrderInfo])
      val format1: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
      val format2: SimpleDateFormat = new SimpleDateFormat("HH")
      val format3: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd")
    
      val date: Date = format1.parse(orderInfo.create_time)
      orderInfo.create_hour = format2.format(date)
      orderInfo.create_date = format3.format(date)
    
      //对敏感数据脱敏
      orderInfo.consignee_tel = orderInfo.consignee_tel.replaceAll("(\\w{3})\\w*(\\w{4})", "$1****$2")
    
      (orderInfo.id, orderInfo)
    })
  
    val orderDetailDs: DStream[(String, OrderDetail)] = ds2.map(record => {
      val orderDetail: OrderDetail = JSON.parseObject(record.value(), classOf[OrderDetail])
      (orderDetail.order_id, orderDetail)
    })
    
    
    //① 两个流 full join
    val joinedDS: DStream[(String, (Option[OrderInfo], Option[OrderDetail]))] = orderInfoDs.fullOuterJoin(orderDetailDs)
    
    //以分区为单位操作,有返回值
    val noUserInfoDS: DStream[SaleDetail] = joinedDS.mapPartitions(iter => {
      //必须放入算子中,因为无法序列化
      implicit var format = org.json4s.DefaultFormats
    
      //一个分区只创建一个redis连接
      val jedisClient: Jedis = RedisUtil.getJedisClient
    
      val saleDetails = ListBuffer[SaleDetail]()
    
      iter.foreach {
        case (orderId, (orderInfoOption, orderDetailOption)) => {
        
          val order_infoKey = "order_info:" + orderId
          val order_detailkey = "order_detail:" + orderId
        
          //①full join 要么order_info不为none,要么order_info为none,先处理join后的order_info
          //a)判断每个order_info join 的order_deail是否为none,不为none,直接组装成SaleDetail对象
          if (orderInfoOption != None) {
            val orderInfo: OrderInfo = orderInfoOption.get
          
            if (orderDetailOption != None) {
              val orderDetail: OrderDetail = orderDetailOption.get
              //封装组合数据
              val saleDetail1: SaleDetail = new SaleDetail(orderInfo, orderDetail)
            
              saleDetails.append(saleDetail1)
            }
          
            //b) orderInfo无条件写入redis
            val orderInfoJson: String = Serialization.write(orderInfo)
          
            jedisClient.setex(order_infoKey, 500, orderInfoJson)
          
            //c)从redis中查询,是否有早到的order_detail,如果有,就查出来,组装为SaleDetail
            val orderDetails: util.Set[String] = jedisClient.smembers(order_detailkey)
          
            //将java集合转换为scala集合
            orderDetails.asScala.foreach(orderDetailsStr => {
              val orderDetail: OrderDetail = JSON.parseObject(orderDetailsStr, classOf[OrderDetail])
            
              val saleDetail: SaleDetail = new SaleDetail(orderInfo, orderDetail)
            
              saleDetails.append(saleDetail)
            })
          
          } else {
          
            //③再处理  join后的order_detail
            //order_detail 一定不是none
            val orderDetail: OrderDetail = orderDetailOption.get
          
            //e)取出order_detail
            //在redis中查看是否有早到的order_info,匹配上就组装为SaleDetail
          
            val orderInfoStr: String = jedisClient.get(order_infoKey)
          
            if (orderInfoStr != null) {
            
              val orderInfo: OrderInfo = JSON.parseObject(orderInfoStr, classOf[OrderInfo])
            
              val saleDetail: SaleDetail = new SaleDetail(orderInfo, orderDetail)
            
              saleDetails.append(saleDetail)
            } else {
              //没找到,意味着order_detail早到了,存入redis
              val orderDetailJson: String = Serialization.write(orderDetail)
            
              jedisClient.sadd(order_detailkey, orderDetailJson)
              jedisClient.expire(order_detailkey, 500)
            }
          }
        }
      }
      //归还连接
      jedisClient.close()
    
      saleDetails.iterator
    
    })
    
    
    //读取redis中用户信息,进行关联
    val result: DStream[SaleDetail] = noUserInfoDS.mapPartitions(iter => {
    
      //创建redis客户端
      val jedisClient: Jedis = RedisUtil.getJedisClient
    
      val saleDetails: Iterator[SaleDetail] = iter.map(saleDetail => {
        //生成key
        val userKey = "user:" + saleDetail.user_id
      
        val userInfoStr: String = jedisClient.get(userKey)
  
        // userInfo数据 Json串在解析还原为 Object时,可以会由于Json串少 { 报错
        //Caused by: com.alibaba.fastjson.JSONException: syntax error, expect {, actual error, pos 0, fastjson-version
       // val userInfo: UserInfo = JSON.parseObject(userInfoStr, classOf[UserInfo])
      
               val gson: Gson = new Gson()
                val userInfo: UserInfo = gson.fromJson(userInfoStr, classOf[UserInfo])
      
        saleDetail.mergeUserInfo(userInfo)
      
        saleDetail
      
      })
      saleDetails
    
      jedisClient.close()
    
      saleDetails
    
    })
    
    result.print()
    
    //将最终结果写入ES
    result.foreachRDD(rdd=>{

      rdd.foreachPartition(iter=>{

        //每天的明细数据保存在一个index中
        val indexName: String = GmallConstants.ES_INDEX_SALE_DETAIL + LocalDate.now()

        //每条数据的id设计:order_detail 的 id
        val datas: List[(String, SaleDetail)] = iter.toList.map(saleDetail => {
          (saleDetail.order_detail_id, saleDetail)
        })

        MyESUtil.insertBulk(indexName, datas)

      })

    })
  
  
    streamingContext.start()
  
    streamingContext.awaitTermination()
  
  
  }
  
  
}

用户业务数据写入redis

package com.gmall.app

import com.alibaba.fastjson.JSON
import com.gmall.common.constansts.GmallConstants
import com.gmall.util.{MykafkaUtil, RedisUtil}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.json4s.jackson.Json
import redis.clients.jedis.Jedis

object  UserApp {
  
  def main(args: Array[String]): Unit = {
    
    val streamingContext: StreamingContext = new StreamingContext("local[*]","UserApp",Seconds(10))
    
    val ds: InputDStream[ConsumerRecord[String, String]] = MykafkaUtil.getKafkaStream(GmallConstants.KAFKA_TOPIC_USER_INFO, streamingContext)
  
    //无需转样例类,直接将DS中record的value值写入redis
  
    val ds1: DStream[String] = ds.map(record => record.value())
  
    //写入redis
    ds1.foreachRDD(rdd => {
    
      rdd.foreachPartition(iter => {
      
        //创建连接
        val jedisClient: Jedis = RedisUtil.getJedisClient
      
        iter.foreach(userInfoString => {
        
          val userInfoKey = "user:" + JSON.parseObject(userInfoString).getString("id")
        
          jedisClient.set(userInfoKey , userInfoString)
        
        })
      
        jedisClient.close()
      
      })
    
    })
  
  
    streamingContext.start()
  
    streamingContext.awaitTermination()
  
  
  }
  
}