文章目录
- 大数据技术之实时项目-需求一日活
- 第1章 需求分析及实现思路
- 1.1 当日用户首次登录(日活)分时趋势图,昨日对比
- 1.2 实现思路
- 1.2.1 功能1:SparkStreaming消费kafka数据
- 1.2.2 功能2:利用redis过滤当日已经计入的日活设备(对一个用户的多次访问进行去重)
- 1.2.3 功能3:把每批次新增的当日日活信息保存到ES中
- 1.2.4 功能4:从ES中查询出数据,发布成数据接口,可视化工程进行调用
- 第2章 功能实现
- 2.1 环境搭建以及编写基础工具类
- 2.1.1 在gmall0421-realtime中编写代码
- 2.1.2 在resources目录添加config.properties配置文件
- 2.1.3 在resources目录添加log4j.properties配置文件
- 2.1.4 导入相关依赖
- 2.1.5 读取properties配置文件的工具类
- 2.1.6 读取Kafka消息的工具类
- 2.1.7 获取Redis连接的工具类
- 2.2 功能1:SparkStreaming消费kafka数据
- 2.2.1 思路
- 2.2.2 代码实现
- 2.2.3 测试
- 2.3 功能2:利用Redis过滤当日已经计入的日活设备
- 2.3.1 思路
- 2.3.2 代码实现
- 2.3.3 测试
- 2.4 功能3:把每批次新增的当日日活信息保存到ES中
- 2.4.1 思路
- 2.4.2 代码实现
- 2.4.3 测试
- 2.5 功能3优化:保证数据的精准一次性消费
- 2.5.1 定义
- 2.5.2 问题如何产生
- 2.5.3 如何解决
- 2.5.4 手动提交偏移流程
- 2.5.5 代码实现
- 2.5.6 幂等性操作
- 2.5.7 关于去重
- 2.6 利用kibana 搭建数据可视化
- 2.6.1 步骤一:创建 IndexPatterns
- 2.6.2 步骤二:配置单图
- 2.6.3 步骤三:配置仪表盘
- 2.7 功能4:从ES中查询出数据,发布成数据接口,可视化工程进行调用
- 2.7.1 思路
- 2.7.2 代码实现-搭建开发环境
- 2.7.3 代码实现-发布查询接口(日活总数)
- 2.7.4 代码实现-发布查询接口(分时值)
大数据技术之实时项目-需求一日活
第1章 需求分析及实现思路
1.1 当日用户首次登录(日活)分时趋势图,昨日对比
从项目的日志中获取用户的启动日志,如果是当日第一次启动,纳入统计。将统计结果保存到ES中,利用Kibana进行分析展示
1.2 实现思路
1.2.1 功能1:SparkStreaming消费kafka数据
Kafka作为数据来源,从kafka中获取日志,kafka中的日志类型有两种,启动和事件,我们这里统计日活,只获取启动日志即可
1.2.2 功能2:利用redis过滤当日已经计入的日活设备(对一个用户的多次访问进行去重)
每个用户每天可能启动多次。要想计算日活,我们只需要把当前用户每天的第一次启动日志获取即可,所以要对启动日志进行去重,相当于做了一次清洗。
实时计算中的去重是一个比较常见的需求,可以有许多方式实现,比如
- 将状态存在Redis中
- 存在关系型数据库中
- 通过Spark自身的updateStateByKey(checkPoint小文件等问题比较麻烦)等
我们这里结合Redis实现对当前用户启动日志去重操作
1.2.3 功能3:把每批次新增的当日日活信息保存到ES中
去重清洗后的数据如何处理
我们这里使用方案1
1.2.4 功能4:从ES中查询出数据,发布成数据接口,可视化工程进行调用
第2章 功能实现
2.1 环境搭建以及编写基础工具类
2.1.1 在gmall0421-realtime中编写代码
2.1.2 在resources目录添加config.properties配置文件
配置kafka、redis等连接信息
# Kafka配置
kafka.broker.list=hadoop105:9092,hadoop106:9092,hadoop107:9092
# Redis配置
redis.host=hadoop105
redis.port=6379
2.1.3 在resources目录添加log4j.properties配置文件
log4j.appender.atguigu.MyConsole=org.apache.log4j.ConsoleAppender
//日志输出位置
log4j.appender.atguigu.MyConsole.target=System.out
log4j.appender.atguigu.MyConsole.layout=org.apache.log4j.PatternLayout
log4j.appender.atguigu.MyConsole.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %10p (%c:%M) - %m%n
//日志级别
log4j.rootLogger =error,atguigu.MyConsole
2.1.4 导入相关依赖
完整pom.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu.realtime</groupId>
<artifactId>gmall0421-realtime</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<spark.version>3.0.0</spark.version>
<scala.version>2.12.11</scala.version>
<kafka.version>2.4.1</kafka.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.12</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.12</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>${kafka.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-10_2.12</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>io.searchbox</groupId>
<artifactId>jest</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>4.5.2</version>
</dependency>
<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>commons-compiler</artifactId>
<version>3.0.16</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>6.6.0</version>
</dependency>
<dependency>
<groupId>ru.yandex.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>0.2.4</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.phoenix</groupId>
<artifactId>phoenix-spark</artifactId>
<version>5.0.0-HBase-2.0</version>
<exclusions>
<exclusion>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.1-b06</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.12</artifactId>
<version>${spark.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 该插件用于将Scala代码编译成class文件 -->
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.4.6</version>
<executions>
<execution>
<!-- 声明绑定到maven的compile阶段 -->
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
2.1.5 读取properties配置文件的工具类
- 在scala->com.atguigu.gmall.realtime.utils包下创建创建MyPropertiesUtil
package com.atguigu.gmall.realtime.utils
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
import java.util.Properties
/**
* @Package: com.atguigu.gmall.realtime.utils
* @ClassName: MyPropertiesUtil
* @Author: fengbing
* @CreateTime: 2020/9/13 20:21
* @Description: 读取properties配置文件的工具类
*/
object MyPropertiesUtil {
def load(propertiesName: String): Properties = {
//1 读取配置文件 用properties
val prop: Properties = new Properties()
//2 返回prop 获取kafka
//properties从哪来 通过获取流
prop.load(new InputStreamReader(
// 从线程中获得stream 加载配置文件 指定编码方式
Thread.currentThread().getContextClassLoader.getResourceAsStream(propertiesName)
, StandardCharsets.UTF_8))
prop
}
def main(args: Array[String]): Unit = {
//3 测试加载配置文件
val properties: Properties = MyPropertiesUtil.load("config.properties")
//4 测试获取kafka信息
val str: String = properties.getProperty("kafka.broker.list")
println(str)
}
}
- 运行主函数测试输出
2.1.6 读取Kafka消息的工具类
- 在scala->com.atguigu.gmall.realtime.utils包下创建创建MyKafkaUtil
package com.atguigu.gmall.realtime.utils
import java.util.Properties
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
/**
* @Package: com.atguigu.gmall.realtime.utils
* @ClassName: MyKafkaUtil
* @Author: fengbing
* @CreateTime: 2020/9/13 21:03
* @Description: Sparkstreaming 消费Kafka工具类
*/
object MyKafkaUtil {
private val properties: Properties = MyPropertiesUtil.load("config.properties")
val broker_list: String = properties.getProperty("kafka.broker.list")
// 1 kafka消费者配置
val kafkaParam = collection.mutable.Map( //kafka参数封装)
"bootstrap.servers" -> broker_list, //用于初始化链接到集群的地址
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
// 用于标识这个消费者属于哪个消费团体
"group.id" -> "gmall0421_group",
// latest自动重置偏移量为最新的偏移量
"auto.offset.reset" -> "latest",
// 如果是true,则这个消费者的偏移量会在后台自动提交,但是kafka宕机容易丢失数据
// 如果是false,则需要手动维护kafka偏移量
"enable.auto.commit" -> (false: java.lang.Boolean)
)
// 2 创建DStream,返回接收到的输入数据
// createDirectStream:kafka上下文
def getKafkaStream(topic: String, ssc: StreamingContext):
InputDStream[ConsumerRecord[String, String]] = {
val dStream = KafkaUtils.createDirectStream[String, String](
ssc, //上下文StreamingContext
LocationStrategies.PreferConsistent, //位置策略
ConsumerStrategies.Subscribe[String, String](Array(topic), kafkaParam) //消费策略 指定主题消费
)
dStream //这个方法封装成离散化流 需要返回
}
// 3 重载的方法 多了一个groupid 多个消费者时 传入groupid
def getKafkaStream(topic: String, ssc: StreamingContext, groupId: String):
InputDStream[ConsumerRecord[String, String]] = {
kafkaParam("group.id") = groupId //替换传入的groupid
val dStream = KafkaUtils.createDirectStream[String, String](
ssc, //上下文StreamingContext
LocationStrategies.PreferConsistent, //位置策略
ConsumerStrategies.Subscribe[String, String](Array(topic), kafkaParam)) //消费策略 指定主题消费
dStream
}
// 4 多个offset
def getKafkaStream(topic: String, ssc: StreamingContext, offsets: Map[TopicPartition, Long],
groupId: String) = {
kafkaParam("group.id") = groupId
val dStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](
ssc,
LocationStrategies.PreferConsistent, //位置策略
ConsumerStrategies.Subscribe[String, String](Array(topic), kafkaParam, offsets))
dStream
}
}
2.1.7 获取Redis连接的工具类
- 修改redis.conf配置文件
#bind 127.0.0.1
protected-mode no
daemonize yes
注意:修改配置文件后,启动redis的时候需要指定配置文件
[atguigu@hadoop105 redis-3.2.5]$ src/redis-server redis.conf
- 代码
package com.atguigu.gmall.realtime.utils
import java.util.Properties
import redis.clients.jedis.{Jedis, JedisPool, JedisPoolConfig}
/**
* @Package: com.atguigu.gmall.realtime.utils
* @ClassName: MyRedisUtil
* @Author: fengbing
* @CreateTime: 2020/9/13 21:33
* @Description: 获取Jedis的工具类
*/
object MyRedisUtil {
// 1 声明JedisPool连接池
private var jedisPool: JedisPool = null
// 2 获取Jedis
def getJedisClient() = {
if (jedisPool == null) {
build()
}
jedisPool.getResource
}
// 3 创建连接池
def build() = {
val config: Properties = MyPropertiesUtil.load("config.properties")
val host: String = config.getProperty("redis.host")
val port: String = config.getProperty("redis.port")
val jedisPoolConfig = new JedisPoolConfig()
jedisPoolConfig.setMaxTotal(100) //最大连接数
jedisPoolConfig.setMaxIdle(20) //最大空闲
jedisPoolConfig.setMinIdle(20) //最小空闲
jedisPoolConfig.setBlockWhenExhausted(true) //忙碌时是否等待
jedisPoolConfig.setMaxWaitMillis(5000) //忙碌时等待时长 毫秒
jedisPoolConfig.setTestOnBorrow(true) //每次获得连接的进行测试
// 4 读取redis配置
jedisPool = new JedisPool(jedisPoolConfig, host, port.toInt)
}
// 5 测试连接 ping
def main(args: Array[String]): Unit = {
// 获取连接客户端
val jedisClient: Jedis = getJedisClient()
println(jedisClient.ping())
//关闭连接
jedisClient.close()
}
}
2.2 功能1:SparkStreaming消费kafka数据
2.2.1 思路
- 模拟日志程序运行生成启动和事件日志
- 请求交给Nginx进行处理
- Nginx反向代理三台处理日志的服务器
- 日志处理服务将日志写到Kafka的主题中
- 编写基本业务类,使用SparkStreming从Kafka主题中消费数据
- 目前只做打印输出
2.2.2 代码实现
package com.atguigu.gmall.realtime.app
import java.lang
import java.text.SimpleDateFormat
import java.util.Date
import com.alibaba.fastjson.{JSON, JSONObject}
import com.atguigu.gmall.realtime.utils.{MyKafkaUtil, MyRedisUtil}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import redis.clients.jedis.Jedis
import scala.collection.mutable.ListBuffer
/**
* @Package: com.atguigu.gmall.realtime.utils.realtime.app
* @ClassName: DauApp
* @Author: fengbing
* @CreateTime: 2020/9/13 21:41
* @Description: 日活用户统计业务类
*/
object DauApp {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("DauApp").setMaster("local[4]")
// 5秒钟一个采集周期
val ssc: StreamingContext = new StreamingContext(conf, Seconds(5))
// ==========功能1 消费kafka功能=============
val topic = "gmall_start_bak"
val groupId = "gmall_dau_bak"
val recordDstream: InputDStream[ConsumerRecord[String, String]] = MyKafkaUtil.getKafkaStream(topic, ssc, groupId)
//测试输出1
// recordDstream.map(_.value()).print(100)
val jsonObjDStream: DStream[JSONObject] = recordDstream.map { record =>
//获取启动日志
val jsonStr: String = record.value()
//将启动日志转换为json对象
val jsonObj: JSONObject = JSON.parseObject(jsonStr)
//获取时间戳 毫秒数
val ts: lang.Long = jsonObj.getLong("ts")
//获取字符串,转换成 日期 小时
val dataHourString: String = new SimpleDateFormat("yyyy-MM-dd HH").format(new Date(ts))
//对字符串日期和小时进行分割,分割后放到json对象中,方便后续处理
val dataHour: Array[String] = dataHourString.split(" ")
jsonObj.put("dt", dataHour(0))
jsonObj.put("hr", dataHour(1))
jsonObj
}
// 测试输出
jsonObjDStream.print()
ssc.start()
// 保持不中断
ssc.awaitTermination()
}
}
2.2.3 测试
- 启动Zookeeper
- 启动Kafka
- 启动logger.sh(日志处理服务-Nginx和SpringBoot程序)
- Idea中运行程序
- 运行模拟生成日志的jar
注意:因为涉及classpath环境变量,要切换到jar包所在目录下执行
- 查看输出效果
2.3 功能2:利用Redis过滤当日已经计入的日活设备
2.3.1 思路
- 利用Redis保存今天访问过系统的用户清单
即SparkStreaming从Kafka中读取到用户的启动日志之后,将用户的启动日志保存到Redis中,进行去重
- 根据保存反馈得到用户是否已存在
Redis的五大数据类型中,String和Set都可以完成去重功能,但是String管理不适合整体操作,比如设置失效时间或者获取当天用户等操作,所以我们项目中使用的是Set类型,处理批量管理以外,还可以根据saddAPI的返回结果判断用户是否已经存在
Key | Value |
dau:2019-01-22 | 设备id |
2.3.2 代码实现
package com.atguigu.gmall.realtime.app
import java.lang
import java.text.SimpleDateFormat
import java.util.Date
import com.alibaba.fastjson.{JSON, JSONObject}
import com.atguigu.gmall.realtime.utils.{MyKafkaUtil, MyRedisUtil}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import redis.clients.jedis.Jedis
import scala.collection.mutable.ListBuffer
/**
* @Package: com.atguigu.gmall.realtime.utils.realtime.app
* @ClassName: DauApp
* @Author: fengbing
* @CreateTime: 2020/9/13 21:41
* @Description: 日活用户统计业务类
*/
object DauApp {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("DauApp").setMaster("local[4]")
// 5秒钟一个采集周期
val ssc: StreamingContext = new StreamingContext(conf, Seconds(5))
// ==========功能1 消费kafka功能=============
val topic = "gmall_start_bak"
val groupId = "gmall_dau_bak"
val recordDstream: InputDStream[ConsumerRecord[String, String]] = MyKafkaUtil.getKafkaStream(topic, ssc, groupId)
//测试输出1
// recordDstream.map(_.value()).print(100)
val jsonObjDStream: DStream[JSONObject] = recordDstream.map { record =>
//获取启动日志
val jsonStr: String = record.value()
//将启动日志转换为json对象
val jsonObj: JSONObject = JSON.parseObject(jsonStr)
//获取时间戳 毫秒数
val ts: lang.Long = jsonObj.getLong("ts")
//获取字符串,转换成 日期 小时
val dataHourString: String = new SimpleDateFormat("yyyy-MM-dd HH").format(new Date(ts))
//对字符串日期和小时进行分割,分割后放到json对象中,方便后续处理
val dataHour: Array[String] = dataHourString.split(" ")
jsonObj.put("dt", dataHour(0))
jsonObj.put("hr", dataHour(1))
jsonObj
}
// 测试输出
// jsonObjDStream.print()
// ==========功能2 使用Redis进行去重=============
/*// 方案1 缺点:虽然我们从池中获取Redis,但是每次从流取数据进行过滤,连接还是过于频繁
val filteredDStream: DStream[JSONObject] = jsonObjDStream.filter {
jsonObj => {
// 获取当前日期
val dt: String = jsonObj.getString("dt")
// 获取设备mid
val mid: String = jsonObj.getJSONObject("common").getString("mid")
// 获取Redis客户端
val jedisClient: Jedis = MyRedisUtil.getJedisClient
// 拼接向Redis存放的数据的key
val dauKey: String = "dau:" + dt
// 判断Redis中是否存在该数据
val isNew: lang.Long = jedisClient.sadd(dauKey, mid)
// 设置当天的key数据失效时间为24小时
jedisClient.expire(dauKey, 3600 * 24)
jedisClient.close()
if (isNew == 1L) {
//Redis不存在,我们需要从DS流中将数据过滤出来,同时数据会保存到Redis中。
true
} else {
//Redis中已经存在该数据,我们需要把该数据从DS流中过滤掉
false
}
}
}
// 输出测试,数量会越来越少,最后变为0,因为mid我们只是模拟了50个
filteredDStream.count().print()*/
// 方案2 以分区为单位进行过滤,可以减少和连接池交互的次数
val filteredDStream: DStream[JSONObject] = jsonObjDStream.mapPartitions {
jsonObjItr => {
// 获取Redis客户端
val jedisClient: Jedis = MyRedisUtil.getJedisClient()
// 定义当前分区过滤后的数据 , 将集合转换成list
val filteredList = new ListBuffer[JSONObject]
for (jsonObj <- jsonObjItr) {
// 获取当前日期
val dt: String = jsonObj.getString("dt")
// 获取设备mid
val mid: String = jsonObj.getJSONObject("common").getString("mid")
// 拼接向Redis存放的数据的key
val dauKey: String = "dau:" + dt
// 判断Redis中是否存在该数据
val isNew: lang.Long = jedisClient.sadd(dauKey, mid)
// 设置当天的key数据失效时间为24小时
jedisClient.expire(dauKey, 3600 * 24)
if (isNew == 1L) {
// 如果Redis中不存在,那么将数据添加到新建的ListBuffer集合中,实现过滤的效果
filteredList.append(jsonObj)
}
}
jedisClient.close()
filteredList.toIterator
}
}
//输出测试 数量会越来越少,最后变为0 因为我们mid只是模拟了50个
filteredDStream.count().print()
ssc.start()
// 保持不中断
ssc.awaitTermination()
}
}
2.3.3 测试
- 启动Zookeeper
- 启动Kafka
- 启动logger.sh(日志处理服务-Nginx和SpringBoot程序)
- 启动Redis
- Idea中运行程序
- 运行模拟生成日志的jar
注意:因为涉及classpath环境变量,要切换到jar包所在目录下执行
- 查看输出效果
//输出测试 数量会越来越少,最后变为0 因为我们mid只是模拟了50个
filteredDStream.count().print()
2.4 功能3:把每批次新增的当日日活信息保存到ES中
2.4.1 思路
将去重后的结果保存的ElasticSearch中,以便后续业务操作
2.4.2 代码实现
- 在ES中创建索引模板
PUT _template/gmall0421_dau_info_template
{
"index_patterns": ["gmall0421_dau_info*"],
"settings": {
"number_of_shards": 3
},
"aliases" : {
"{index}-query": {},
"gmall0421_dau_info-query":{}
},
"mappings": {
"_doc":{
"properties":{
"mid":{
"type":"keyword"
},
"uid":{
"type":"keyword"
},
"ar":{
"type":"keyword"
},
"ch":{
"type":"keyword"
},
"vc":{
"type":"keyword"
},
"dt":{
"type":"keyword"
},
"hr":{
"type":"keyword"
},
"mi":{
"type":"keyword"
},
"ts":{
"type":"date"
}
}
}
}
}
- 创建一个样例类,用于封装需要的日志数据(并不是所有采集到的字段都需要)
package com.atguigu.gmall.realtime.bean
/**
* @Package: com.atguigu.gmall.realtime.bean
* @ClassName: DauInfo
* @Author: fengbing
* @CreateTime: 2020/9/14 21:03
* @Description: 封装日活数据的样例类
*/
case class DauInfo(
mid: String, //设备id
uid: String, //用户id
ar: String, //地区
ch: String, //渠道
vc: String, //版本
var dt: String, //日期
var hr: String, //小时
var mi: String, //分钟
ts: Long //时间戳
) {
}
- 在MyESUtil工具类中提供批量添加bulkInsert的方法
// 向ES中批量插入数据
// 参数1:批量操作的数据 参数2:索引名称
def bulkInsert(dauList: List[(String, Any)], indexName: String) = {
if (dauList != null && indexName.size != 0) {
// 获取操作对象
val jestClient: JestClient = getClient()
// 构造批次操作
val builder: Bulk.Builder = new Bulk.Builder
// 对批次操作的数据进行遍历
for ((id, dau) <- dauList) {
val index: Index = new Index.Builder(dau)
.index(indexName)
.`type`("_doc").id(id)
.build()
// 将每条数据添加到批量操作中
builder.addAction(index)
}
// Bulk是Action的实现类,主要实现批量操作
val bulk: Bulk = builder.build()
// 执行批量操作
val result: BulkResult = jestClient.execute(bulk)
val items: util.List[BulkResult#BulkResultItem] = result.getItems
println("向ES中插入了" + items.size() + "条数据")
// 关闭连接
jestClient.close()
}
}
- 在DauApp类中完成插入的功能
//==============功能3.向ES中保存日活信息================
filteredDStream.foreachRDD {
// 获取DS中的RDD
rdd => {
// 以分区为单位对RDD中的数据进行处理,方便批量插入
rdd.foreachPartition {
jsonItr => {
val dauList: List[(String, DauInfo)] = jsonItr.map {
jsonObj => {
// 每次处理的是一个json对象,将json对象封装为样例类
val commonJsonObj: JSONObject = jsonObj.getJSONObject("common")
val dauInfo: DauInfo = DauInfo(
commonJsonObj.getString("mid"),
commonJsonObj.getString("uid"),
commonJsonObj.getString("ar"),
commonJsonObj.getString("ch"),
commonJsonObj.getString("vc"),
jsonObj.getString("dt"),
jsonObj.getString("hr"),
// 分钟我们前面没有转换,默认00
"00",
jsonObj.getLong("ts")
)
(dauInfo.mid,dauInfo)
}
}.toList
// 对分区的数据进行批量处理
// 获取当前日志字符串
val dt: String = new SimpleDateFormat("yyyy-MM-dd").format(new Date())
MyESUtil.bulkInsert(dauList,"gmall0421_dau_info" + dt)
}
}
}
}
2.4.3 测试
- 启动Zookeeper
- 启动Kafka
- 启动logger.sh(日志处理服务-Nginx和SpringBoot程序)
- 启动Redis,清空Redis中所有数据
127.0.0.1:6379> FLUSHALL
- Idea中运行程序
- 运行模拟生成日志的jar
注意:因为涉及classpath环境变量,要切换到jar包所在目录下执行
- 查看输出效果
- 控制台
- ES
2.5 功能3优化:保证数据的精准一次性消费
2.5.1 定义
- 精确一次消费(Exactly-once)
是指消息一定会被处理且只会被处理一次。不多不少就一次处理。
如果达不到精确一次消费,可能会达到另外两种情况:
- 至少一次消费(at least once)
主要是保证数据不会丢失,但有可能存在数据重复问题。
- 最多一次消费 (at most once)
主要是保证数据不会重复,但有可能存在数据丢失问题。
2.5.2 问题如何产生
- 数据何时会丢失
比如实时计算任务进行计算,到数据结果存盘之前,进程崩溃,假设在进程崩溃前kafka调整了偏移量,那么kafka就会认为数据已经被处理过,即使进程重启,kafka也会从新的偏移量开始,所以之前没有保存的数据就被丢失掉了。
- 数据何时会重复
如果数据计算结果已经存盘了,在kafka调整偏移量之前,进程崩溃,那么kafka会认为数据没有被消费,进程重启,会重新从旧的偏移量开始,那么数据就会被2次消费,又会被存盘,数据就被存了2遍,造成数据重复。
如果同时解决了数据丢失和数据重复的问题,那么就实现了精确一次消费的语义了。
目前Kafka默认每5秒钟做一次自动提交偏移量,这样并不能保证精准一次消费
enable.auto.commit 的默认值是 true;就是默认采用自动提交的机制。
auto.commit.interval.ms 的默认值是 5000,单位是毫秒。
2.5.3 如何解决
(1) 策略一:利用关系型数据库的事务进行处理
出现丢失或者重复的问题,核心就是偏移量的提交与数据的保存,不是原子性的。如果能做成要么数据保存和偏移量都成功,要么两个失败,那么就不会出现丢失或者重复了。
这样的话可以把存数据和修改偏移量放到一个事务里。这样就做到前面的成功,如果后面做失败了,就回滚前面那么就达成了原子性,这种情况先存数据还是先修改偏移量没影响。
- 好处
事务方式能够保证精准一次性消费
- 问题与限制
- 数据必须都要放在某一个关系型数据库中,无法使用其他功能强大的nosql数据库
- 事务本身性能不好
- 如果保存的数据量较大一个数据库节点不够,多个节点的话,还要考虑分布式事务的问题。分布式事务会带来管理的复杂性,一般企业不选择使用,有的企业会把分布式事务变成本地事务,例如把Executor上的数据通过rdd.collect算子提取到Driver端,由Driver端统一写入数据库,这样会将分布式事务变成本地事务的单线程操作,降低了写入的吞吐量
- 使用场景
数据足够少(通常经过聚合后的数据量都比较小,明细数据一般数据量都比较大),并且支持事务的数据库
(2) 策略二:手动提交偏移量+幂等性处理
我们知道如果能够同时解决数据丢失和数据重复问题,就等于做到了精确一次消费。那就各个击破。
首先解决数据丢失问题,办法就是要等数据保存成功后再提交偏移量,所以就必须手工来控制偏移量的提交时机。
但是如果数据保存了,没等偏移量提交进程挂了,数据会被重复消费。怎么办?那就要把数据的保存做成幂等性保存。即同一批数据反复保存多次,数据不会翻倍,保存一次和保存一百次的效果是一样的。如果能做到这个,就达到了幂等性保存,就不用担心数据会重复了。
- 难点
话虽如此,在实际的开发中手动提交偏移量其实不难,难的是幂等性的保存,有的时候并不一定能保证,这个需要看使用的数据库,如果数据库本身不支持幂等性操作,那只能优先保证的数据不丢失,数据重复难以避免,即只保证了至少一次消费的语义。
一般有主键的数据库都支持幂等性操作upsert。
- 使用场景
处理数据较多,或者数据保存在不支持事务的数据库上
2.5.4 手动提交偏移流程
- 偏移量保存在哪
本身kafka 0.9版本以后consumer的偏移量是保存在kafka的**__consumer_offsets**主题中。但是如果用这种方式管理偏移量,有一个限制就是在提交偏移量时,数据流的元素结构不能发生转变,即提交偏移量时数据流,必须是InputDStream[ConsumerRecord[String, String]] 这种结构。但是在实际计算中,数据难免发生转变,或聚合,或关联,一旦发生转变,就无法在利用以下语句进行偏移量的提交:
xxDstream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
因为offset的存储于HasOffsetRanges,只有kafkaRDD继承了他,所以假如我们对KafkaRDD进行了转化之后,其它RDD没有继承HasOffsetRanges,所以就无法再获取offset了。
所以实际生产中通常会利用ZooKeeper,Redis,Mysql等工具手动对偏移量进行保存
- 流程
2.5.5 代码实现
- 创建偏移量管理类,用于读取和保存偏移量
package com.atguigu.gmall.realtime.utils
import java.util
import org.apache.kafka.common.TopicPartition
import org.apache.spark.streaming.kafka010.OffsetRange
import redis.clients.jedis.Jedis
/**
* @Package: com.atguigu.gmall.realtime.utils
* @ClassName: OffsetManagerUtil
* @Author: fengbing
* @CreateTime: 2020/9/14 23:58
* @Description: 手动维护offset偏移量
* 从Redis中读取偏移量
* Reids格式:type=>Hash [key=>offset:topic:groupId field=>partitionId value=>偏移量值] expire 不需要指定
* topicName 主题名称
* groupId 消费者组
* @return 当前消费者组中,消费的主题对应的分区的偏移量信息
* KafkaUtils.createDirectStream在读取数据的时候封装了Map[TopicPartition,Long]
*
*/
object OffsetManagerUtil {
// 从Redis中获取偏移量
def getOffset(topicName: String, groupId: String): Map[TopicPartition, Long] = {
// 获取Jedis客户端
val jedis: Jedis = MyRedisUtil.getJedisClient()
// 拼接Redis中存储偏移量的key
var offsetKey = "offset" + topicName + ":" + groupId
// 从Redis中获取偏移量
val offsetMap: util.Map[String, String] = jedis.hgetAll(offsetKey)
// 关闭jedis
jedis.close()
// 对获取到的偏移量map进行遍历
import scala.collection.JavaConverters._
offsetMap.asScala.map {
case (partitionId, offset) => {
println("读取分区偏移量: " + partitionId + ":" + offset)
(new TopicPartition(topicName, partitionId.toInt), offset.toLong)
}
}.toMap
}
// 向Redis中保存偏移量
def saveOffset(topicName: String, groupId: String, offsetRanges: Array[OffsetRange]): Unit = {
// 定义一个Java类型的Map,用于存放分区和偏移量信息
val offsetMap = new util.HashMap[String, String]()
// 对OffsetRange数据进行遍历
for (offsetRange <- offsetRanges) {
// 获取分区编号
val partitionId: Any = offsetRange.partition
// 获取结束偏移量
val untilOffset: Long = offsetRange.untilOffset
// 将分区和偏移量封装到map集合中
offsetMap.put(partitionId.toString, untilOffset.toString)
// 打印测试
println("保存分区:" + partitionId + ":" + offsetRange.fromOffset + "--->" + offsetRange.untilOffset)
}
// 拼接Redis中存储偏移量的Key
var offsetKey = "offset" + topicName + ":" + groupId
// 获取Jedis
val jedis: Jedis = MyRedisUtil.getJedisClient()
jedis.hmset(offsetKey, offsetMap)
// 关闭连接
jedis.close()
}
}
- 修改DauApp的代码
package com.atguigu.gmall.realtime.app
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 com.atguigu.gmall.realtime.utils.{MyESUtil, MyKafkaUtil, MyRedisUtil, OffsetManagerUtil}
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 redis.clients.jedis.Jedis
import scala.collection.mutable.ListBuffer
/**
* @Package: com.atguigu.gmall.realtime.utils.realtime.app
* @ClassName: DauApp
* @Author: fengbing
* @CreateTime: 2020/9/13 21:41
* @Description: 日活用户统计业务类
*/
object DauApp {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("DauApp").setMaster("local[4]")
// 5秒钟一个采集周期
val ssc: StreamingContext = new StreamingContext(conf, Seconds(5))
// ==========功能1 消费kafka功能=============
val topic = "gmall_start_bak"
val groupId = "gmall_dau_bak"
// 功能3优化:从Redis中读取kafka偏移量
val offsetMap: Map[TopicPartition, Long] = OffsetManagerUtil.getOffset(topic, groupId)
var recordDstream: InputDStream[ConsumerRecord[String, String]] = null
if (offsetMap != null && offsetMap.size > 0) {
// Redis中有偏移量,根据Redis中保存的偏移量读取
recordDstream = MyKafkaUtil.getKafkaStream(topic, ssc, offsetMap, groupId)
} else {
// Redis中没有保存偏移量,Kafka默认从最新读取
recordDstream = MyKafkaUtil.getKafkaStream(topic, ssc, groupId)
}
// 得到本批次中处理数据的分区对应的偏移量起始及结束位置
// 注意:这里我们从Kafka中读取数据之后,直接就获取了偏移量的位置,因为KafkaRDD可以转换为HasOffsetRanges,会自动记录位置
var offsetRanges = Array.empty[OffsetRange]
val offsetDStream: DStream[ConsumerRecord[String, String]] = recordDstream.transform {
rdd => {
offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
rdd
}
}
// 测试输出1
// recordDstream.map(_.value()).print(100)
// 开始从Kafka中读取偏移量
// val recordDstream: InputDStream[ConsumerRecord[String, String]] = MyKafkaUtil.getKafkaStream(topic, ssc, groupId)
// 将DS中数据进行结构的转换,只保留记录的value部分,将字符串value转换为json对象,像json对象中添加dt,
val jsonObjDStream: DStream[JSONObject] = offsetDStream.map {
record =>
//获取启动日志的json格式字符串
val jsonStr: String = record.value()
//将json格式字符串转换为Json对象
val jsonObj: JSONObject = JSON.parseObject(jsonStr)
//获取当前时间戳 毫秒数
val ts: lang.Long = jsonObj.getLong("ts")
//获取字符串,转换成 日期和小时的形式
val dataHourString: String = new SimpleDateFormat("yyyy-MM-dd HH").format(new Date(ts))
//对字符串日期和小时进行分割,分割后放到json对象中,方便后续处理
val dataHour: Array[String] = dataHourString.split(" ")
jsonObj.put("dt", dataHour(0))
jsonObj.put("hr", dataHour(1))
jsonObj
}
// 测试输出
// jsonObjDStream.print()
// ==========功能2 使用Redis进行去重=============
/*// 方案1 缺点:虽然我们从池中获取Redis,但是每次从流取数据进行过滤,连接还是过于频繁
val filteredDStream: DStream[JSONObject] = jsonObjDStream.filter {
jsonObj => {
// 获取当前日期
val dt: String = jsonObj.getString("dt")
// 获取设备mid
val mid: String = jsonObj.getJSONObject("common").getString("mid")
// 获取Redis客户端
val jedisClient: Jedis = MyRedisUtil.getJedisClient
// 拼接向Redis存放的数据的key
val dauKey: String = "dau:" + dt
// 判断Redis中是否存在该数据
val isNew: lang.Long = jedisClient.sadd(dauKey, mid)
// 设置当天的key数据失效时间为24小时
jedisClient.expire(dauKey, 3600 * 24)
jedisClient.close()
if (isNew == 1L) {
//Redis不存在,我们需要从DS流中将数据过滤出来,同时数据会保存到Redis中。
true
} else {
//Redis中已经存在该数据,我们需要把该数据从DS流中过滤掉
false
}
}
}
// 输出测试,数量会越来越少,最后变为0,因为mid我们只是模拟了50个
filteredDStream.count().print()*/
// 功能2:利用redis过滤当日已经计入的日活设备
// 方案2 以分区为单位进行过滤,可以减少和连接池交互的次数
val filteredDStream: DStream[JSONObject] = jsonObjDStream.mapPartitions {
jsonObjItr => {
// 获取Redis客户端
val jedisClient: Jedis = MyRedisUtil.getJedisClient()
// 定义一个集合,用于存放首次登录的json对象
val filteredList = new ListBuffer[JSONObject]
for (jsonObj <- jsonObjItr) {
// 获取当前日期
val dt: String = jsonObj.getString("dt")
// 获取设备mid
val mid: String = jsonObj.getJSONObject("common").getString("mid")
// 拼接向Redis存放的数据的key
val dauKey: String = "dau:" + dt
// 判断Redis中是否存在该数据
val isNew: lang.Long = jedisClient.sadd(dauKey, mid)
// 设置当天的key数据失效时间为24小时
jedisClient.expire(dauKey, 3600 * 24)
if (isNew == 1L) {
// 如果Redis中不存在,那么将数据添加到新建的ListBuffer集合中,实现过滤的效果
filteredList.append(jsonObj)
}
}
jedisClient.close()
filteredList.toIterator
}
}
//输出测试 数量会越来越少,最后变为0 因为我们mid只是模拟了50个
// filteredDStream.count().print()
//==============功能3.向ES中保存日活信息================
filteredDStream.foreachRDD {
// 获取DS中的RDD
rdd => {
// 以分区为单位对RDD中的数据进行处理,方便批量插入
rdd.foreachPartition {
jsonObjItr => {
val dauList: List[(String, DauInfo)] = jsonObjItr.map {
jsonObj => {
// 每次处理的是一个json对象,将json对象封装为样例类
val commonJsonObj: JSONObject = jsonObj.getJSONObject("common")
val dauInfo: DauInfo = DauInfo(
commonJsonObj.getString("mid"),
commonJsonObj.getString("uid"),
commonJsonObj.getString("ar"),
commonJsonObj.getString("ch"),
commonJsonObj.getString("vc"),
jsonObj.getString("dt"),
jsonObj.getString("hr"),
// 分钟我们前面没有转换,默认00
"00",
jsonObj.getLong("ts")
)
(dauInfo.mid, dauInfo)
}
}.toList
// 调用ES工具类,将dauList中的数据批量保存到ES中
val dt: String = new SimpleDateFormat("yyyy-MM-dd").format(new Date)
// 向ES中批量插入数据
MyESUtil.bulkInsert(dauList, "gmall0421_dau_info" + dt)
}
}
// 将偏移量提交到Redis中去
OffsetManagerUtil.saveOffset(topic, groupId, offsetRanges)
}
}
ssc.start()
// 保持不中断
ssc.awaitTermination()
}
}
- 测试
- 第一次运行,Redis中没有数据,先向Redis中写
- 再次运行,Redis中有数据,直接从Redis中读
2.5.6 幂等性操作
其实我们前面已经使用Redis进行了去重操作,基本上是可以保证幂等性的。如果更严格的保证幂等性,那我们在批量向ES写数据的时候,指定Index的id即可
- 修改MyESUtil
- 修改DauApp
- 测试
清除ES中的数据,重新运行程序,查看效果
2.5.7 关于去重
我们是通过Redis完成的去重,其实也可以在ES中进行去重操作
但是通过Redis去重,保留的是前面的数据,有就不向里加
通过ES或者其他数据库去重,是完成的替换,保留的是后面的数据
根据实际的需求选择合适的实现
2.6 利用kibana 搭建数据可视化
如果数据保存在Elasticsearch那么利用kibana进行可视化展示是一种非常高效的可视化解决方案。这种kibana可视化方案,优势是快速高效,但是对于展示效果的定制化和炫酷程度不能有太高的要求。
2.6.1 步骤一:创建 IndexPatterns
其实就是创建数据源 确定数据范围
- 在 Management中创建Index Patterns
- 配置匹配模式,通过用“*”来控制后缀可以得到不同的数据范围
- 选择一个时间字段,用于对可视化图形灵活筛选时间范围
2.6.2 步骤二:配置单图
- 在VIsualize选择加号
- 选择一个可视化图形,我们这里选择垂直柱状图
- 选择一个index pattern
- 柱状图配置
- Y轴配置度量值
Aggregation:聚合方法
Customer Label: Y轴的显示标签
- X 轴配置维度列
Aggregation: 分组方法
Field: 分组字段
Order by : 分组如何排序
Order 排序顺序
size 列出前N名
Group other …. : 是否把不在TopN的分组组合起来组成一个Other列
Show Missing Values 是否把空值数据汇总起来组成一个Missing列。
Customer Label: X轴的显示标签
- 右上角选择刷新频率和时间范
- 配置好后点击 执行按钮
- 最后保存,命名
练习:按照上面的步骤配置一个热力图
2.6.3 步骤三:配置仪表盘
仪表盘 Dashboard,就是配置多个图在一个页面上同时展示
- 选择Dashboard 然后选择Create new DashBoard
- 然后选择add
- 从之前配置的可视化图中,选择图形。
- 选好后选择save保存。就可以在Dashboard列表中查看大盘图了
- 拷贝内嵌代码
- 新建一个html页面,将拷贝的iframe粘贴进去,可以查看数据
- 修改/opt/module/rt_applog/application.properties,再生成一天数据,运行DauApp,可以看到动态变化
2.7 功能4:从ES中查询出数据,发布成数据接口,可视化工程进行调用
2.7.1 思路
- 目标效果
- 要求数据格式
总数 | [{“id”:“dau”,“name”:“新增日活”,“value”:1200}, {“id”:“new_mid”,“name”:“新增设备”,“value”:233} ] |
分时统计 | {“yesterday”:{“11”:383,“12”:123,“17”:88,“19”:200 }, “today”:{“12”:38,“13”:1233,“17”:123,“19”:688 }} |
- 访问路径
总数 | http://publisher:8070/realtime-total?date=2019-02-01 |
分时统计 | http://publisher:8070/realtime-hour?id=dau&date=2019-02-01 |
- 代码结构
控制层 | PublisherController | 实现接口的web发布 |
服务层 | ESService | 数据业务查询interface |
ESServiceImpl | 业务查询的实现类 | |
主程序 | GmallPublisherApplication | 增加扫描包 |
2.7.2 代码实现-搭建开发环境
- 创建新的module
- 勾选相关依赖
- 在pom.xml文件中添加ES相关依赖以及commons-lang3工具包
注意:目前SprintBoot版本更新到2.3.3,从2.3开始,SpringBoot不能自动注入JestClient了,所以我们将版本降低到2.1.5,修改后Test类报错,重新导包即可
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu.gmall.publisher</groupId>
<artifactId>gmall0421-publisher</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gmall0421-publisher</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.searchbox</groupId>
<artifactId>jest</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>4.5.2</version>
</dependency>
<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>commons-compiler</artifactId>
<version>3.0.16</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>6.6.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.4</version>
</dependency>
<dependency>
<groupId>ru.yandex.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>0.2.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 修改application.propertis配置文件
#指定web服务端口号
#指定web服务端口号
server.port=8070
#指定ES服务器地址 (jest)
spring.elasticsearch.jest.uris=http://hadoop105:9200,http://hadoop106:9200
- 搭建项目结构
2.7.3 代码实现-发布查询接口(日活总数)
- 在ESService中添加获取日活总数的方法
package com.atguigu.gmall.publisher.service;
import java.util.Map;
/**
* @Package: com.atguigu.gmall.publisher.service.impl
* @ClassName: ESService
* @Author: fengbing
* @CreateTime: 2020/9/15 21:20
* @Description: 数据业务查询interface,业务操作接口
*/
public interface ESService {
//日活总数查询
public Long getDauTotal(String date);
}
- 在ESServiceImpl中对获取日活总数的方法进行实现
package com.atguigu.gmall.publisher.service.impl;
import com.atguigu.gmall.publisher.service.ESService;
import io.searchbox.client.JestClient;
import io.searchbox.core.Search;
import io.searchbox.core.SearchResult;
import io.searchbox.core.search.aggregation.TermsAggregation;
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Package: com.atguigu.gmall.publisher.service.impl
* @ClassName: ESServicelmpl
* @Author: fengbing
* @CreateTime: 2020/9/15 21:20
* @Description: ES相关操作的具体实现
*/
@Service
public class ESServiceImpl implements ESService {
@Autowired
JestClient jestClient;
//获取日活总数
@Override
public Long getDauTotal(String date) {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(new MatchAllQueryBuilder());
String query = sourceBuilder.toString();
//拼接查询的索引名称
String indexName = "gamll0421_dau_into_" + date + "-query";
Search search = new Search.Builder(query).addIndex(indexName)
.addIndex(indexName)
.addType("_doc")
.build();
Long total = 0L;
try {
//获取当前日期对应索引的所有记录
SearchResult searchResult = jestClient.execute(search);
if (searchResult.getTotal() != null) {
total = searchResult.getTotal();
}
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("ES查询异常");
}
return total;
}
}
注意:如果注入的时候提示错误, 解决办法是:降低Autowired检测的级别,将Severity的级别由之前的error改成warning或其它可以忽略的级别
- 在PublisherController类中发布接口
package com.atguigu.gmall.publisher.controller;
import com.atguigu.gmall.publisher.service.ClickHouseService;
import com.atguigu.gmall.publisher.service.ESService;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* @Package: com.atguigu.gmall.publisher.controller
* @ClassName: PublisherController
* @Author: fengbing
* @CreateTime: 2020/9/15 21:19
* @Description: 对外发布接口的类(接收用户请求)
*/
@RestController
public class PublisherController {
/**
* 将ESService 注入进来
*/
@Autowired
ESService esService;
@Autowired
ClickHouseService clickHouseService;
//访问路径:http://localhost:8070/realtime-total?date=2020-09-15
/*
//返回格式
[
{
"id":"dau",
"name":"新增日活",
"value":1200
},
{"id":"new_mid","name":"新增设备","value":233}
]
*/
@RequestMapping("/realtime-total")
public Object realtimeTotal(@RequestParam("date") String dt) {
List<Map<String, Object>> dauList = new ArrayList<Map<String, Object>>();
//定义一个map集合,用于封装新增日活数据
Map<String, Object> dauMap = new HashMap<String, Object>();
dauMap.put("id", "dau");
dauMap.put("name", "新增日活");
//获取指定日期的日活数
Long dauTotal = esService.getDauTotal(dt);
dauMap.put("value", dauTotal);
dauList.add(dauMap);
//定义一个map集合,用于封装新增设备数据
Map<String, Object> midMap = new HashMap<String, Object>();
midMap.put("id", "mid");
midMap.put("name", "新增设备");
midMap.put("value", 66);
dauList.add(midMap);
return dauList;
}
/*//发布日活分时值接口
数据格式:
{
"yesterday":{"11":383,"12":123,"17":88,"19":200 },
"today":{"12":38,"13":1233,"17":123,"19":688 }
}
访问路径:http://localhost:8070/realtime-hour?id=dau&date=2020-09-15
*/
//2 接收请求
@RequestMapping("realtime-hour")
//1 定义realtimeHour方法
public Object realtimeHour(@RequestParam("id") String id, @RequestParam("date") String dt) {
if ("dau".equals(id)) {
//3 数据格式是json对象
Map<String,Map<String,Long>> hourMap = new HashMap<>();
//4 获取今天日活分时值
Map tdMap = esService.getDauHour(dt);
hourMap.put("today", tdMap);
//7 获取昨天日期字符串
String yd = getYd(dt);
//5 获取昨天的日活分时值
Map ydMap = esService.getDauHour(yd);
hourMap.put("yesterday", ydMap);
return hourMap;
}
return null;
}
//6 定义昨天日期的方法
private String getYd(String td) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String yd = null;
try {
Date tdDate = dateFormat.parse(td);
Date ydDate = DateUtils.addDays(tdDate, -1);
yd = dateFormat.format(ydDate);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("日期格式转变失败");
}
return yd;
}
}
- 修改host映射地址
127.0.0.1 publisher
- 访问http://localhost:8070/realtime-total?date=2020-08-18测试
2.7.4 代码实现-发布查询接口(分时值)
- 在ESService接口中添加日活分时查询的方法
//获取日活分时
//查询语句如下
/*
GET /gmall0421_dau_info_2020-09-15-query/_search
{
"aggs": {
"groupby_hr": {
"terms": {
"field": "hr",
"size": 24
}
}
}
}*/
@Override
public Map<String, Long> getDauHour(String date) {
//4构建查询
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
TermsAggregationBuilder termsAggregationBuilder =
AggregationBuilders.terms("groupby_hr").field("hr").size(24);
sourceBuilder.aggregation(termsAggregationBuilder);
//3定义查询
String query = sourceBuilder.toString();
//5 拼接查询的索引名称
String indexName = "gmall0421_dau_info_" + date + "-query";
//2定义查询 6查询的字段
Search search = new Search.Builder(query)
.addIndex(indexName)
.addType("_doc")
.build();
//7处理异常
try {
HashMap<String, Long> dauMap = new HashMap<String, Long>();
//1执行器
SearchResult searchResult = jestClient.execute(search);
if (searchResult.getAggregations().getTermsAggregation("groupby_hr") != null) {
List<TermsAggregation.Entry> buckets = searchResult.getAggregations()
.getTermsAggregation("groupby_hr").getBuckets();
for (TermsAggregation.Entry bucket : buckets) {
dauMap.put(bucket.getKey(), bucket.getCount());
}
}
return dauMap;
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("ES查询异常");
}
}
}
- 在PublisherController中发布接口
/*//发布日活分时值接口
数据格式:
{
"yesterday":{"11":383,"12":123,"17":88,"19":200 },
"today":{"12":38,"13":1233,"17":123,"19":688 }
}
访问路径:http://localhost:8070/realtime-hour?id=dau&date=2020-09-15
*/
//2 接收请求
@RequestMapping("realtime-hour")
//1 定义realtimeHour方法
public Object realtimeHour(@RequestParam("id") String id, @RequestParam("date") String dt) {
if ("dau".equals(id)) {
//3 数据格式是json对象
Map<String,Map<String,Long>> hourMap = new HashMap<>();
//4 获取今天日活分时值
Map tdMap = esService.getDauHour(dt);
hourMap.put("today", tdMap);
//7 获取昨天日期字符串
String yd = getYd(dt);
//5 获取昨天的日活分时值
Map ydMap = esService.getDauHour(yd);
hourMap.put("yesterday", ydMap);
return hourMap;
}
return null;
}
//6 定义昨天日期的方法
private String getYd(String td) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String yd = null;
try {
Date tdDate = dateFormat.parse(td);
Date ydDate = DateUtils.addDays(tdDate, -1);
yd = dateFormat.format(ydDate);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("日期格式转变失败");
}
return yd;
}
- 访问http://localhost:8070/realtime-hour?id=dau&date=2020-08-19测试
- 搭建可视化工程进行对接
从资料中拷贝dw-echart前端工程进行部署,启动主程序