大数据学习之路111-大数据项目(中国移动运营数据分析一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_37050372/article/details/83183752

业务二:

统计每个省份的充值失败数据量,并以地图的方式显示分布情况。

数据说明:

充值的整个过程是包括:

订单创建->支付请求->支付通知->充值请求->充值通知

而我们需要处理的就是充值通知部分的数据。而我们的数据中是包含上面这五种类型的数据的。

那么我们如何从那么多数据中确定哪条数据是充值通知的数据呢?

我们可以通过serviceName字段来确定,如果该字段是reChargeNotifyReq则代表该条数据是充值通知部分的数据。

为什么呢?

针对业务一:

充值订单量我们只需要通过有多少行数就可以确定有多少笔。

对于充值金额,我们首先需要确定到充值成功的订单数(字段bussinessRst如果为0000则代表成功)

找到充值成功的订单之后,我们可以将该数据的chargefee字段进行累加。就可以得到总金额。

充值成功率:我们只要知道总交易笔数和成功的笔数即可求。

充值平均时长:首先我们需要知道开始时间和结束时间,我们才能知道充值所花费的时间。

开始时间:对于开始时间,这里有一个RequestId字段,它是由时间戳+随机数生成的。

结束时间:即为接到充值通知的时间,为字段(receiveNotifyTime)

针对业务二:

对于业务失败量的分布,首先我们需要知道在哪个省份,哪个地区。

我们可以根据provinceCode字段来确定省份

对于失败的订单我们可以通过统计bussinessRst为不是0000的情况来确定。

接下来我们就开始写业务:

下面是我们的数据截图,该文件名叫cmcc.log

首先我们用flume采集数据到kafka:

我们先写配置文件:

# 定义这个agent中各组件的名字
a1.sources = r1
a1.sinks = k1
a1.channels = c1

# 描述和配置source组件:r1
a1.sources.r1.type = spooldir
a1.sources.r1.spoolDir = /root/flumedata/


# 描述和配置sink组件:k1
a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink
a1.sinks.k1.kafka.topic = cmccThree
a1.sinks.k1.kafka.bootstrap.servers = marshal:9092,marshal01:9092,marshal02:9092,marshal03:9092,marshal04:9092,marshal05:9092
a1.sinks.k1.kafka.flumeBatchSize = 20
a1.sinks.k1.kafka.producer.acks = 1
a1.sinks.k1.kafka.producer.linger.ms = 1
a1.sinks.k1.kafka.producer.compression.type = snappy


# 描述和配置channel组件,此处使用是内存缓存的方式
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100

# 描述和配置source  channel   sink之间的连接关系
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1

然后启动flume:

执行结果:

现在我们已经将数据导入kafka了,在kafka的data目录中,该数据的主题是cmccTwo

接下来我们可以写代码了,在写代码之前我们 可以再确认一下kafka中是否已经写进去数据了。

bin/kafka-console-consumer.sh --bootstrap-server marshal:9092 --from-beginning --topic cmccThree

我们可以看到运行结果如下:

一共有40883条记录。

到此为止我们就完成了将数据导入到kafka的工作。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 接下来我们需要从kafka中拉取数据:

我们在写代码的过程中应该有分离代码的思想,我们首先将一些kafka的配置信息写出去:

application.conf

#kafka相关参数
kafka.topic = "cmcc"
kafka.broker.list = "marshal:9092,marshal01:9092,marshal02:9092,marshal03:9092,marshal04:9092,marshal05:9092"
kafka.group.id = "20181016"

AppParams.scala

package com.sheep.utils

import com.typesafe.config.{Config, ConfigFactory}
import org.apache.kafka.common.serialization.StringDeserializer

object AppParams {
  /**
    * 解析application.conf的配置文件
    * 加载resource下面的配置,默认规则application.conf -> application.json -> application.properties
    */
    private lazy val config: Config = ConfigFactory.load()
  /**
    * 返回订阅的主题,这里用,分割是因为可能有多个主题
    */
  val topic = config.getString("kafka.topic").split(",")
  /**
    * kafka集群所在的主机和端口
    */
  val brokers = config.getString("kafka.broker.list")
  /**
    * 消费者的id
    */
  val groupId = config.getString("kafka.group.id")

  val kafkaParams = Map[String,Object](
    "bootstrap.servers" -> brokers,
    "key.deserializer" -> classOf[StringDeserializer],
    "value.deserializer" -> classOf[StringDeserializer],
    "group.id" -> groupId,
    //这个代表,任务启动之前产生的数据也要读
    "auto.offset.reset" -> "earliest",
    "enable.auto.commit" -> (false:java.lang.Boolean)
  )
}

配置文件写完之后就可以获取kafka中的数据了:

package com.sheep.app

import com.alibaba.fastjson.{JSON, JSONObject}
import com.sheep.utils.AppParams
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object BootStrapApp {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org.apache.spark").setLevel(Level.OFF)
    val conf: SparkConf = new SparkConf()
      .setAppName("中国移动运营实时监控平台-Monitor")
      //如果是在集群上运行的话需要去掉setMaster
      .setMaster("local[*]")
    //SparkStreaming传输的是离散流,离散流是由RDD组成的
    //数据传输的时候可以对RDD进行压缩,压缩的目的是减少内存的占用
    //默认采用org.apache.spark.serializer.JavaSerializer
    //这是最基本的优化
    conf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
    //rdd压缩
    conf.set("spark.rdd.compress","true")
    //设置每次拉取的数量,为了防止一下子拉取的数据过多,系统处理不过来
    //这里并不是拉取100条,是有公式的。
    //batchSize = partitionNum * 分区数量 * 采样时间
    conf.set("spark.streaming.kafka.maxRatePerPartition","100")
    //设置优雅的结束,这样可以避免数据的丢失
    conf.set("spark.streaming.stopGracefullyOnShutdown","true")
    val ssc: StreamingContext = new StreamingContext(conf,Seconds(2))
    //获取kafka的数据
    /**
      *   指定kafka数据源
      *   ssc:StreamingContext的实例
      *   LocationStrategies:位置策略,如果kafka的broker节点跟Executor在同一台机器上给一种策略,不在一台机器上给另外一种策略
      *       设定策略后会以最优的策略进行获取数据
      *       一般在企业中kafka节点跟Executor不会放到一台机器的,原因是kakfa是消息存储的,Executor用来做消息的计算,
      *       因此计算与存储分开,存储对磁盘要求高,计算对内存、CPU要求高
      *       如果Executor节点跟Broker节点在一起的话使用PreferBrokers策略,如果不在一起的话使用PreferConsistent策略
      *       使用PreferConsistent策略的话,将来在kafka中拉取了数据以后尽量将数据分散到所有的Executor上
      *   ConsumerStrategies:消费者策略(指定如何消费)
      *
      */
    val directStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(ssc,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe[String,String](AppParams.topic,AppParams.kafkaParams)
    )

写到这里我们就已经获取了kafka中的数据了。接下来就是对他进行处理:

我们首先做的是计算充值成功的笔数:

由于我们的数据是json的所以要想对数据进行分析的话,就要使用json解析工具:

我们导入json解析工具的依赖:

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.51</version>
        </dependency>

我们的思路是:过滤出serviceName字段为reChargeNotifyReq的数据,这些数据就是和充值通知有关的,我们需要处理的数据。

在过滤出来的这些数据中,有成功的,也有失败的。

过滤完数据之后我们对数据操作,将成功的数据设为1,失败的设为0,并且从requestId中截取出前8位和标志位组成一个元组返回。

然后使用reduceByKey就可以统计出当天交易成功的数量了。如果是将结果打印在控制台上结果是这样的:

所以要想统计最终的我们可以将数据写入redis 累加。

代码如下:

 directStream.foreachRDD(
      rdd =>{
        //rdd.map(_.value()).foreach(println)
        //取得所有充值通知日志
        val baseData: RDD[JSONObject] = rdd.map(cr =>JSON.parseObject(cr.value()))
        .filter(obj => obj.getString("serviceName").equalsIgnoreCase("reChargeNotifyReq")).cache()
        //bussinessRst是业务结果,如果是0000则为成功,其他返回错误编码
        val totalSucc = baseData.map(obj => {
          val reqId = obj.getString("requestId")
          val day = reqId.substring(0, 8)
          //取出该条充值是否成功的标志
          val result = obj.getString("bussinessRst")
          val flag = if (result.equals("0000")) 1 else 0
          (day, flag)
        }).reduceByKey(_+_)

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------现在我们需要做的是统计出当天的交易成功的总金额,我们只要对上面的程序进行修改一下就好了。

之前的代码中如果交易成功返回的是1,而现在我们只要返回交易金额就好了。

代码如下:

//获取充值成功的订单金额
        val totalMoney = baseData.map(obj => {
          val reqId = obj.getString("requestId")
          val day = reqId.substring(0, 8)
          //取出该条充值是否成功的标志
          val result = obj.getString("bussinessRst")
          val fee = if (result.equals("0000")) obj.getString("chargefee").toDouble else 0
          (day, fee)
        }).reduceByKey(_+_)

输出结果如下:

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------到现在为止我们已经充值成功的订单量和充值金额写好了。接下来就要算充值成功率了。

充值成功率 = 成功订单数 / 总订单数

总订单量只要使用count就可以得出。而充值成功的订单我们前面已经算出来了。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

接下来要算的就是充值成功的充值时长:

如果是交易成功的则用结束时间(即通知充值成功的时间)- 开始时间(即requestId的前17位)

如果是交易失败的话,则返回0

代码如下:

 /**
          * 获取充值成功的充值时长
          */
        val totalTime: RDD[(String, Long)] = baseData.map(obj => {
          val reqId = obj.getString("requestId")
          //获取日期
          val day = reqId.substring(0, 8)
          //取出该条充值是否成功的标志
          val result = obj.getString("bussinessRst")
          //时间格式为:yyyyMMddHHmmssSSS(年月日时分秒毫秒)
          val endTime = obj.getString("receiveNotifyTime")
          val startTime: String = reqId.substring(0, 17)
          val format: SimpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS")
          val cost = if (result.equals("0000")) format.parse(endTime).getTime - format.parse(startTime).getTime else 0
          (day, cost)
        }).reduceByKey(_ + _)

输出结果如下:

这里返回的时间是毫秒数

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

接下来我们尝试将充值成功的订单数写入redis :

我们首先在application.conf中添加redis的一些配置:

# redis
redis.host = "marshal"
redis.db.index = 1

接下来在AppParams中添加,对redis参数的访问:


  /**
    * redis服务器地址
    */
  val redisHost = config.getString("redis.host")

  /**
    * 将数据写入到哪个库
    */
  val redisDbIndex = config.getString("redis.db.index").toInt

然后写一个从连接池获取连接的方法:

package com.sheep.cmcc.utils

import org.apache.commons.pool2.impl.GenericObjectPoolConfig
import redis.clients.jedis.{Jedis, JedisPool}

object Jpools {
  private val poolConfig = new GenericObjectPoolConfig()
  //连接池中最大的空闲连接数,默认是8
  poolConfig.setMaxIdle(5)
  //只支持最大的连接数,连接池中最大的连接数,默认是8
  poolConfig.setMaxTotal(2000)
  private lazy val jedisPool: JedisPool = new JedisPool(poolConfig,AppParams.redisHost)

  def getJedis = {
    val jedis: Jedis = jedisPool.getResource
    jedis.select(AppParams.redisDbIndex)
    jedis
  }
}

接下来写入数据库:

//将充值成功的订单数写入redis
        totalSucc.foreachPartition(it => {
          val jedis: Jedis = Jpools.getJedis
          it.foreach(
            tp => {
              jedis.incrBy("CMCC-"+tp._1,tp._2)
            })
          jedis.close()
        })

执行结果如下:

两次刷新之后的结果不一样,是因为他不停的在读数据,处理数据,然后做累加。

但是这样的写法很不好,而且存在很多问题:

比如频繁的使用reduceByKey,会不停的产生shuffle,这样对性能会有影响。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------

写到现在我们的程序还不够优化,我们的各项指标都是单独计算的,每次计算都会产生shuffle.这样的性能是非常低的。所以我们针对现有的程序进行一个改造。

以下为优化方案:

接下来我们就来优化一下:

首先我们将计算时间差的功能提取出来,比如说下面这样:

package com.sheep.cmcc.utils

import java.text.SimpleDateFormat

object CaculateTools {
  //非线程安全的
  private val format = new SimpleDateFormat("yyyyMMddHHmmssSSS")
     def caculateTime(startTime:String,endTime:String):Long = {
       val start = startTime.substring(0,17)
        format.parse(endTime).getTime - format.parse(start).getTime
     }
}

可是这样做是有问题的,因为他是非线程安全的。如果想要线程安全,那么我们最好每次调用的时候都new一下那个SimpleDateFormat

所以我们就使用另一个方法,他是线程安全的。

package com.sheep.cmcc.utils

import java.text.SimpleDateFormat

import org.apache.commons.lang3.time.FastDateFormat



object CaculateTools {
  //非线程安全的
  //private val format = new SimpleDateFormat("yyyyMMddHHmmssSSS")
  private val format: FastDateFormat = FastDateFormat.getInstance("yyyyMMddHHmmssSSS")
     def caculateTime(startTime:String,endTime:String):Long = {
       val start = startTime.substring(0,17)
        format.parse(endTime).getTime - format.parse(start).getTime
     }
}

实现代码如下:

package com.sheep.cmcc.app

import java.lang

import com.alibaba.fastjson.{JSON, JSONObject}
import com.sheep.cmcc.utils.{AppParams, CaculateTools, Jpools}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import redis.clients.jedis.Jedis

/**
  * 中国移动监控平台优化版
  */
object BootStrapAppV2 {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org.apache.spark").setLevel(Level.OFF)
    val conf: SparkConf = new SparkConf()
      .setAppName("中国移动运营实时监控平台-Monitor")
      //如果是在集群上运行的话需要去掉setMaster
      .setMaster("local[*]")
    //SparkStreaming传输的是离散流,离散流是由RDD组成的
    //数据传输的时候可以对RDD进行压缩,压缩的目的是减少内存的占用
    //默认采用org.apache.spark.serializer.JavaSerializer
    //这是最基本的优化
    conf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
    //rdd压缩
    conf.set("spark.rdd.compress","true")
    //设置每次拉取的数量,为了防止一下子拉取的数据过多,系统处理不过来
    //这里并不是拉取100条,是有公式的。
    //batchSize = partitionNum * 分区数量 * 采样时间
    conf.set("spark.streaming.kafka.maxRatePerPartition","100")
    //设置优雅的结束,这样可以避免数据的丢失
    conf.set("spark.streaming.stopGracefullyOnShutdown","true")
    val ssc: StreamingContext = new StreamingContext(conf,Seconds(2))
    //获取kafka的数据
    /**
      *   指定kafka数据源
      *   ssc:StreamingContext的实例
      *   LocationStrategies:位置策略,如果kafka的broker节点跟Executor在同一台机器上给一种策略,不在一台机器上给另外一种策略
      *       设定策略后会以最优的策略进行获取数据
      *       一般在企业中kafka节点跟Executor不会放到一台机器的,原因是kakfa是消息存储的,Executor用来做消息的计算,
      *       因此计算与存储分开,存储对磁盘要求高,计算对内存、CPU要求高
      *       如果Executor节点跟Broker节点在一起的话使用PreferBrokers策略,如果不在一起的话使用PreferConsistent策略
      *       使用PreferConsistent策略的话,将来在kafka中拉取了数据以后尽量将数据分散到所有的Executor上
      *   ConsumerStrategies:消费者策略(指定如何消费)
      *
      */
    val directStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(ssc,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe[String,String](AppParams.topic,AppParams.kafkaParams)
    )
    //serviceName为reChargeNotifyReq的才被认为是充值通知
    directStream.foreachRDD(rdd =>{
      //取得所有充值通知日志
      val baseData= rdd.map(cr =>JSON.parseObject(cr.value()))
        .filter(obj => obj.getString("serviceName").equalsIgnoreCase("reChargeNotifyReq"))
        .map(obj => {
          //判断这条日志是否是充值成功的日志
          val result = obj.getString("bussinessRst")
          //获取充值金额
         val fee: lang.Double = obj.getDouble("chargefee")
          //充值发起的时间和结束时间
          val requestId: String = obj.getString("requestId")
          //数据当前时间
          val day = requestId.substring(0,8)
          val receiveTime: String = obj.getString("receiveNotifyTime")
          //取得充值时长
          val costTime = CaculateTools.caculateTime(requestId,receiveTime)
          val succAndFeeAndTime: (Double, Double, Double) = if(result.equals("0000")) (1,fee,costTime) else(0,0,0)
          //(日期,List(订单数,成功订单,订单金额,充值时长))
          (day,List[Double](1,succAndFeeAndTime._1,succAndFeeAndTime._2,succAndFeeAndTime._3))
        }).cache()

      baseData.reduceByKey(_.zip(_).map(tp => {tp._1+tp._2}))
        .foreachPartition(partition =>{
          val jedis: Jedis = Jpools.getJedis
          partition.foreach(tp => {
            jedis.hincrBy("A-"+tp._1,"total",tp._2(0).toLong)
            jedis.hincrBy("A-"+tp._1,"succ",tp._2(1).toLong)
            jedis.hincrByFloat("A-"+tp._1,"money",tp._2(2))
            jedis.hincrBy("A-"+tp._1,"cost",tp._2(3).toLong)
            //设置key的过期时间
            jedis.expire("A-"+tp._1,60*60*48)
          })
          jedis.close()
        })
    })
      ssc.start()
    ssc.awaitTermination()

  }
}

运行结果如下:

到这里我们整体的业务就算做完了。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------接下来我们要做的就是实时充值业务的办理趋势

如下图:

上面的业务是按照天统计的,下面的业务是按照小时统计的。

这里我们需要的是两个计算维度:

按小时计算:

计算内容:充值成功订单量,成功率

其实我们只需要统计出来某时的总订单量和充值成功的订单量,而充值成功率可以根据这两个值推算出来。

我们之前的表维度都是按照日期进行划分的,现在用日期肯定不行了,所以我们就用日期加小时数做一个维度。

改了维度之后我们就需要在原来返回的元组之上再加一个小时维度。

而现在由于返回的元组已经不是对偶元组了,对于之前按天统计的业务,不能进行reduceByKey,所以需要重构一下。

而之后我们要写的按小时统计的业务,我们需要将key变为日期和小时的集合,因为如果不加上日期的话会出现日期混淆的情况。

package com.sheep.cmcc.app

import java.lang

import com.alibaba.fastjson.{JSON, JSONObject}
import com.sheep.cmcc.utils.{AppParams, CaculateTools, Jpools}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import redis.clients.jedis.Jedis

/**
  * 中国移动监控平台优化版
  */
object BootStrapAppV2 {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org.apache.spark").setLevel(Level.OFF)
    val conf: SparkConf = new SparkConf()
      .setAppName("中国移动运营实时监控平台-Monitor")
      //如果是在集群上运行的话需要去掉setMaster
      .setMaster("local[*]")
    //SparkStreaming传输的是离散流,离散流是由RDD组成的
    //数据传输的时候可以对RDD进行压缩,压缩的目的是减少内存的占用
    //默认采用org.apache.spark.serializer.JavaSerializer
    //这是最基本的优化
    conf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
    //rdd压缩
    conf.set("spark.rdd.compress","true")
    //设置每次拉取的数量,为了防止一下子拉取的数据过多,系统处理不过来
    //这里并不是拉取100条,是有公式的。
    //batchSize = partitionNum * 分区数量 * 采样时间
    conf.set("spark.streaming.kafka.maxRatePerPartition","100")
    //设置优雅的结束,这样可以避免数据的丢失
    conf.set("spark.streaming.stopGracefullyOnShutdown","true")
    val ssc: StreamingContext = new StreamingContext(conf,Seconds(2))
    //获取kafka的数据
    /**
      *   指定kafka数据源
      *   ssc:StreamingContext的实例
      *   LocationStrategies:位置策略,如果kafka的broker节点跟Executor在同一台机器上给一种策略,不在一台机器上给另外一种策略
      *       设定策略后会以最优的策略进行获取数据
      *       一般在企业中kafka节点跟Executor不会放到一台机器的,原因是kakfa是消息存储的,Executor用来做消息的计算,
      *       因此计算与存储分开,存储对磁盘要求高,计算对内存、CPU要求高
      *       如果Executor节点跟Broker节点在一起的话使用PreferBrokers策略,如果不在一起的话使用PreferConsistent策略
      *       使用PreferConsistent策略的话,将来在kafka中拉取了数据以后尽量将数据分散到所有的Executor上
      *   ConsumerStrategies:消费者策略(指定如何消费)
      *
      */
    val directStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(ssc,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe[String,String](AppParams.topic,AppParams.kafkaParams)
    )
    //serviceName为reChargeNotifyReq的才被认为是充值通知
    directStream.foreachRDD(rdd =>{
      //取得所有充值通知日志
      val baseData= rdd.map(cr =>JSON.parseObject(cr.value()))
        .filter(obj => obj.getString("serviceName").equalsIgnoreCase("reChargeNotifyReq"))
        .map(obj => {
          //判断这条日志是否是充值成功的日志
          val result = obj.getString("bussinessRst")
          //获取充值金额
         val fee: lang.Double = obj.getDouble("chargefee")
          //充值发起的时间和结束时间
          val requestId: String = obj.getString("requestId")
          //数据当前时间
          val day = requestId.substring(0,8)
          val hour = requestId.substring(8,10)
          val minute = requestId.substring(10,12)
          val receiveTime: String = obj.getString("receiveNotifyTime")
          //取得充值时长
          val costTime = CaculateTools.caculateTime(requestId,receiveTime)
          val succAndFeeAndTime: (Double, Double, Double) = if(result.equals("0000")) (1,fee,costTime) else(0,0,0)
          //(日期,List(订单数,成功订单,订单金额,充值时长))
          (day,hour,List[Double](1,succAndFeeAndTime._1,succAndFeeAndTime._2,succAndFeeAndTime._3))
        }).cache()

      baseData.map(tp => (tp._1,tp._3)).reduceByKey(_.zip(_).map(tp => {tp._1+tp._2}))
        .foreachPartition(partition =>{
          val jedis: Jedis = Jpools.getJedis
          partition.foreach(tp => {
            jedis.hincrBy("A-"+tp._1,"total",tp._2(0).toLong)
            jedis.hincrBy("A-"+tp._1,"succ",tp._2(1).toLong)
            jedis.hincrByFloat("A-"+tp._1,"money",tp._2(2))
            jedis.hincrBy("A-"+tp._1,"cost",tp._2(3).toLong)
            //设置key的过期时间
            jedis.expire("A-"+tp._1,60*60*48)
          })
          jedis.close()
        })

      /**
        * 业务概述-每小时的充值情况
        */
      baseData.map(tp => ((tp._1,tp._2),List(tp._3(0),tp._3(1)))).reduceByKey(_.zip(_).map(tp => {tp._1+tp._2}))
        .foreachPartition(partition =>{
          val jedis: Jedis = Jpools.getJedis
          partition.foreach(tp => {
            //总的充值成功和失败订单数量
            jedis.hincrBy("B-"+tp._1._1,"T:"+tp._1._2,tp._2(0).toLong)
            //充值成功订单数量
            jedis.hincrBy("B-"+tp._1._1,"S:"+tp._1._2,tp._2(1).toLong)

            //设置key的过期时间
            jedis.expire("B-"+tp._1._1,60*60*48)
          })
          jedis.close()
        })
    })
      ssc.start()
    ssc.awaitTermination()

  }
}

运行结果:

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

接下来我们还要进行优化,因为我们可以看到随着指标的增多,我们的代码很凌乱。

所以我们就可以将所有关于指标的函数和方法都封装起来

封装类:

package com.sheep.cmcc.utils

import java.lang

import com.alibaba.fastjson.JSON
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.spark.rdd.RDD
import redis.clients.jedis.Jedis

object KpiTools {
  /**
    * 业务概况(总订单量,成功订单量,充值成功总金额,时长
    * @param baseData
    */
   def kpi_general(baseData: RDD[(String, String, List[Double])]) = {
    baseData.map(tp => (tp._1, tp._3)).reduceByKey(_.zip(_).map(tp => {
      tp._1 + tp._2
    }))
      .foreachPartition(partition => {
        val jedis: Jedis = Jpools.getJedis
        partition.foreach(tp => {
          jedis.hincrBy("A-" + tp._1, "total", tp._2(0).toLong)
          jedis.hincrBy("A-" + tp._1, "succ", tp._2(1).toLong)
          jedis.hincrByFloat("A-" + tp._1, "money", tp._2(2))
          jedis.hincrBy("A-" + tp._1, "cost", tp._2(3).toLong)
          //设置key的过期时间
          jedis.expire("A-" + tp._1, 60 * 60 * 48)
        })
        jedis.close()
      })

  }

  /**
    * 业务概述-(每小时的充值总订单量,每小时的成功订单量)
    * @param baseData
    */
  def kpi_general_hour(baseData: RDD[(String, String, List[Double])]) = {
    baseData.map(tp => ((tp._1, tp._2), List(tp._3(0), tp._3(1)))).reduceByKey(_.zip(_).map(tp => {
      tp._1 + tp._2
    }))
      .foreachPartition(partition => {
        val jedis: Jedis = Jpools.getJedis
        partition.foreach(tp => {
          //总的充值成功和失败订单数量
          jedis.hincrBy("B-" + tp._1._1, "T:" + tp._1._2, tp._2(0).toLong)
          //充值成功订单数量
          jedis.hincrBy("B-" + tp._1._1, "S:" + tp._1._2, tp._2(1).toLong)

          //设置key的过期时间
          jedis.expire("B-" + tp._1._1, 60 * 60 * 48)
        })
        jedis.close()
      })
  }

  /**
    * 整理基础数据
    * @param rdd
    * @return
    */
  def baseDataRDD(rdd: RDD[ConsumerRecord[String, String]]) = {
    rdd.map(cr => JSON.parseObject(cr.value()))
      .filter(obj => obj.getString("serviceName").equalsIgnoreCase("reChargeNotifyReq"))
      .map(obj => {
        //判断这条日志是否是充值成功的日志
        val result = obj.getString("bussinessRst")
        //获取充值金额
        val fee: lang.Double = obj.getDouble("chargefee")
        //充值发起的时间和结束时间
        val requestId: String = obj.getString("requestId")
        //数据当前时间
        val day = requestId.substring(0, 8)
        val hour = requestId.substring(8, 10)
        val minute = requestId.substring(10, 12)
        val receiveTime: String = obj.getString("receiveNotifyTime")
        //取得充值时长
        val costTime = CaculateTools.caculateTime(requestId, receiveTime)
        val succAndFeeAndTime: (Double, Double, Double) = if (result.equals("0000")) (1, fee, costTime) else (0, 0, 0)
        //(日期,List(订单数,成功订单,订单金额,充值时长))
        (day, hour, List[Double](1, succAndFeeAndTime._1, succAndFeeAndTime._2, succAndFeeAndTime._3))
      }).cache()
  }
}

封装之后我们的主题代码就少很多了:

-------------------------------------------------------------------------------------------------------------------------------------------------------------------

接下来开始写我们的下一个业务:由于我们这里的数据基本上都是充值成功的,所以我们这里就不统计充值失败的了。我们这里统计充值成功的全国分布。

计算维度:日期+地区

计算内容:充值成功的订单量

这样我们就需要将之前的代码进行修改增加一个字段:

package com.sheep.cmcc.utils

import java.lang

import com.alibaba.fastjson.JSON
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.spark.rdd.RDD
import redis.clients.jedis.Jedis

object KpiTools {
  /**
    * 业务概况(总订单量,成功订单量,充值成功总金额,时长
    * @param baseData
    */
   def kpi_general(baseData: RDD[(String, String, List[Double],String)]) = {
    baseData.map(tp => (tp._1, tp._3)).reduceByKey(_.zip(_).map(tp => {
      tp._1 + tp._2
    }))
      .foreachPartition(partition => {
        val jedis: Jedis = Jpools.getJedis
        partition.foreach(tp => {
          jedis.hincrBy("A-" + tp._1, "total", tp._2(0).toLong)
          jedis.hincrBy("A-" + tp._1, "succ", tp._2(1).toLong)
          jedis.hincrByFloat("A-" + tp._1, "money", tp._2(2))
          jedis.hincrBy("A-" + tp._1, "cost", tp._2(3).toLong)
          //设置key的过期时间
          jedis.expire("A-" + tp._1, 60 * 60 * 48)
        })
        jedis.close()
      })

  }

  /**
    * 业务概述-(每小时的充值总订单量,每小时的成功订单量)
    * @param baseData
    */
  def kpi_general_hour(baseData: RDD[(String, String, List[Double],String)]) = {
    baseData.map(tp => ((tp._1, tp._2), List(tp._3(0), tp._3(1)))).reduceByKey(_.zip(_).map(tp => {
      tp._1 + tp._2
    }))
      .foreachPartition(partition => {
        val jedis: Jedis = Jpools.getJedis
        partition.foreach(tp => {
          //总的充值成功和失败订单数量
          jedis.hincrBy("B-" + tp._1._1, "T:" + tp._1._2, tp._2(0).toLong)
          //充值成功订单数量
          jedis.hincrBy("B-" + tp._1._1, "S:" + tp._1._2, tp._2(1).toLong)

          //设置key的过期时间
          jedis.expire("B-" + tp._1._1, 60 * 60 * 48)
        })
        jedis.close()
      })
  }

  /**
    * 业务质量
    * @param baseData
    */
  def kpi_general_quality(baseData: RDD[(String, String, List[Double],String)]): Unit ={
    baseData.map(tp => ((tp._1, tp._4), tp._3(1))).reduceByKey(_+_)
      .foreachPartition(partition => {
        val jedis: Jedis = Jpools.getJedis
        partition.foreach(tp => {
          //充值成功订单数量
          jedis.hincrBy("C-" + tp._1._1, tp._1._2, tp._2.toLong)

          //设置key的过期时间
          jedis.expire("C-" + tp._1._1, 60 * 60 * 48)
        })
        jedis.close()
      })
   }
  /**
    * 整理基础数据
    * @param rdd
    * @return
    */
  def baseDataRDD(rdd: RDD[ConsumerRecord[String, String]]) = {
    rdd.map(cr => JSON.parseObject(cr.value()))
      .filter(obj => obj.getString("serviceName").equalsIgnoreCase("reChargeNotifyReq"))
      .map(obj => {
        //判断这条日志是否是充值成功的日志
        val result = obj.getString("bussinessRst")
        //获取充值金额
        val fee: lang.Double = obj.getDouble("chargefee")
        //充值发起的时间和结束时间
        val requestId: String = obj.getString("requestId")
        //数据当前时间
        val day = requestId.substring(0, 8)
        val hour = requestId.substring(8, 10)
        val minute = requestId.substring(10, 12)
        val receiveTime: String = obj.getString("receiveNotifyTime")
        //省份code
        val provinceCode: String = obj.getString("provinceCode")
        //取得充值时长
        val costTime = CaculateTools.caculateTime(requestId, receiveTime)
        val succAndFeeAndTime: (Double, Double, Double) = if (result.equals("0000")) (1, fee, costTime) else (0, 0, 0)
        //(日期,List(订单数,成功订单,订单金额,充值时长))
        (day, hour, List[Double](1, succAndFeeAndTime._1, succAndFeeAndTime._2, succAndFeeAndTime._3),provinceCode)
      }).cache()
  }
}

运行结果:

--------------------------------------------------------------------------------------------------------------------------------------------------------------

接下来我们要写的就是每分钟实时充值情况的分布

计算维度:日期+小时+分钟

计算内容:充值笔数,充值金额

这样我们又要增加一个分钟字段:

/**
    *实时统计每分钟的充值金额和订单量
    */
  def kpi_realtime_minute(baseData: RDD[(String, String, List[Double],String,String)]): Unit ={
    baseData.map(tp => ((tp._1,tp._2 ,tp._5), List(tp._3(1),tp._3(2)))).reduceByKey(_.zip(_).map(tp=>tp._1+tp._2))
      .foreachPartition(partition => {
        val jedis: Jedis = Jpools.getJedis
        partition.foreach(tp => {
          //每分钟充值成功订单数量和金额
          jedis.hincrBy( "D-"+tp._1._1, "C:"+tp._1._2+tp._1._3, tp._2(0).toLong)
          jedis.hincrByFloat( "D-"+tp._1._1, "M:"+tp._1._2+tp._1._3, tp._2(1))
          //设置key的过期时间
          jedis.expire("D-" + tp._1._1, 60 * 60 * 48)
        })
        jedis.close()
      })
  }

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

之前我们在做业务质量的时候只是将省份编号写进去了,并没有将省份编号和省份名称对应起来,接下来我们就来做这件事。

在配置文件application.conf中加入省份与省份编号的对应

#映射配置
pcode2pname{
  100="北京"
  200="广东"
  210="上海"
  220="天津"
  230="重庆"
  240="辽宁"
  250="江苏"
  270="湖北"
  280="四川"
  290="陕西"
  311="河北"
  351="山西"
  371="河南"
  431="吉林"
  451="黑龙江"
  471="内蒙古"
  531="山东"
  551="安徽"
  571="浙江"
  591="福建"
  731="湖南"
  771="广西"
  791="江西"
  851="贵州"
  871="云南"
  891="西藏"
  898="海南"
  931="甘肃"
  951="宁夏"
  971="青海"
  991="新疆"

}

我们如何才能将这样的配置文件读出来组成元组呢?

 def main(args: Array[String]): Unit = {
    val configObject: ConfigObject = config.getObject("pcode2pname")
    import scala.collection.JavaConversions._
    val map: Map[String, AnyRef] = configObject.unwrapped().toMap
    map.foreach(println)
  }

运行结果:

所以我们可以在AppParams中添加一段代码:

 /**
    * 省份code和省份名称的映射关系
    *
    */
    import scala.collection.JavaConversions._
   val  pcode2PName = config.getObject("pcode2pname").unwrapped().toMap

然后在主代码中将这些对应广播出去:

/**
    * 业务质量
    * @param baseData
    */
  def kpi_general_quality(baseData: RDD[(String, String, List[Double],String,String)],p2p:Broadcast[Map[String, AnyRef]]): Unit ={
    baseData.map(tp => ((tp._1, tp._4), tp._3(1))).reduceByKey(_+_)
      .foreachPartition(partition => {
        val jedis: Jedis = Jpools.getJedis
        partition.foreach(tp => {
          //充值成功订单数量
          jedis.hincrBy("C-" + tp._1._1,p2p.value.getOrElse(tp._1._2,tp._1._2).toString, tp._2.toLong)

          //设置key的过期时间
          jedis.expire("C-" + tp._1._1, 60 * 60 * 48)
        })
        jedis.close()
      })
   }

运行后的结果:

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

到现在我们已经把所有指标都分析完了。可是我们还有一件事没有做,就是维护偏移量。

接下来我们要将偏移量,存储到mysql

首先我们来了解一下这个类库:

我们要使用的话还要先导入依赖:

<dependency>
            <groupId>org.scalikejdbc</groupId>
            <artifactId>scalikejdbc_2.11</artifactId>
            <version>2.5.0</version>
        </dependency>

        <dependency>
            <groupId>org.scalikejdbc</groupId>
            <artifactId>scalikejdbc-config_2.11</artifactId>
            <version>2.5.0</version>
        </dependency>

我们先写一个demo来看看怎么用:

首先在application.conf中配置数据库的连接信息,注意!这里的配置名是不能改的。

#Mysql 连接信息
db.default.driver="com.mysql.jdbc.Driver"
db.default.url="jdbc:mysql://marshal:3306/lfr"
db.default.user="root"
db.default.password="123456"
package com.sheep.cmcc.app

import scalikejdbc.config._
import scalikejdbc._

/***
  * scalike 访问mysql测试
  */
object ScalikeJdbcDemo {
  def main(args: Array[String]): Unit = {
    //读取mysql的配置 application.conf -> application.json -> application.properties
    DBs.setup()
    //查询数据(只读)
    DB.readOnly(
      implicit session =>{
           SQL("select * from wordcount").map(rs=>{
             (rs.string("words"),
             rs.int(2))
           }).list().apply()
      }.foreach(println)
    )
    //删除数据
   DB.autoCommit(
     implicit session => {
       SQL("delete from wordcount where words='shabi'").update().apply()
     }
   )
    //事务
    DB.localTx(implicit session =>{
      SQL("insert into wordcount values(?,?)").bind("hadoop",10).update().apply()
      var r = 1 / 0
      SQL("insert into wordcount values(?,?)").bind("php",20).update().apply()
    })
  }
}

猜你喜欢

转载自blog.csdn.net/qq_37050372/article/details/83183752
今日推荐