业务二:
统计每个省份的充值失败数据量,并以地图的方式显示分布情况。
数据说明:
充值的整个过程是包括:
订单创建->支付请求->支付通知->充值请求->充值通知
而我们需要处理的就是充值通知部分的数据。而我们的数据中是包含上面这五种类型的数据的。
那么我们如何从那么多数据中确定哪条数据是充值通知的数据呢?
我们可以通过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()
})
}
}