文章目录

  • 大数据技术之实时项目-需求一日活
  • 第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进行分析展示


Spark实时统计交易数据架构 spark实时项目_Spark实时统计交易数据架构

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中

去重清洗后的数据如何处理

Spark实时统计交易数据架构 spark实时项目_apache_02

我们这里使用方案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)
  }
}
  • 运行主函数测试输出

Spark实时统计交易数据架构 spark实时项目_apache_03

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包所在目录下执行

  • 查看输出效果

Spark实时统计交易数据架构 spark实时项目_kafka_04

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"
         }  
       }
     }
   }
}

Spark实时统计交易数据架构 spark实时项目_kafka_05

  • 创建一个样例类,用于封装需要的日志数据(并不是所有采集到的字段都需要)
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包所在目录下执行

  • 查看输出效果
  • 控制台

Spark实时统计交易数据架构 spark实时项目_实时大数据_06

  • ES

Spark实时统计交易数据架构 spark实时项目_spark_07

2.5 功能3优化:保证数据的精准一次性消费

2.5.1 定义

  • 精确一次消费(Exactly-once)

是指消息一定会被处理且只会被处理一次。不多不少就一次处理。

如果达不到精确一次消费,可能会达到另外两种情况:

  • 至少一次消费(at least once)

主要是保证数据不会丢失,但有可能存在数据重复问题。

  • 最多一次消费 (at most once)

主要是保证数据不会重复,但有可能存在数据丢失问题。

2.5.2 问题如何产生

  • 数据何时会丢失

比如实时计算任务进行计算,到数据结果存盘之前,进程崩溃,假设在进程崩溃前kafka调整了偏移量,那么kafka就会认为数据已经被处理过,即使进程重启,kafka也会从新的偏移量开始,所以之前没有保存的数据就被丢失掉了。

Spark实时统计交易数据架构 spark实时项目_kafka_08

  • 数据何时会重复

如果数据计算结果已经存盘了,在kafka调整偏移量之前,进程崩溃,那么kafka会认为数据没有被消费,进程重启,会重新从旧的偏移量开始,那么数据就会被2次消费,又会被存盘,数据就被存了2遍,造成数据重复。

Spark实时统计交易数据架构 spark实时项目_kafka_09

如果同时解决了数据丢失和数据重复的问题,那么就实现了精确一次消费的语义了。

目前Kafka默认每5秒钟做一次自动提交偏移量,这样并不能保证精准一次消费

enable.auto.commit 的默认值是 true;就是默认采用自动提交的机制。

auto.commit.interval.ms 的默认值是 5000,单位是毫秒。

2.5.3 如何解决

(1) 策略一:利用关系型数据库的事务进行处理

出现丢失或者重复的问题,核心就是偏移量的提交与数据的保存,不是原子性的。如果能做成要么数据保存和偏移量都成功,要么两个失败,那么就不会出现丢失或者重复了。

这样的话可以把存数据和修改偏移量放到一个事务里。这样就做到前面的成功,如果后面做失败了,就回滚前面那么就达成了原子性,这种情况先存数据还是先修改偏移量没影响。

Spark实时统计交易数据架构 spark实时项目_apache_10

  • 好处

事务方式能够保证精准一次性消费

  • 问题与限制
  • 数据必须都要放在某一个关系型数据库中,无法使用其他功能强大的nosql数据库
  • 事务本身性能不好
  • 如果保存的数据量较大一个数据库节点不够,多个节点的话,还要考虑分布式事务的问题。分布式事务会带来管理的复杂性,一般企业不选择使用,有的企业会把分布式事务变成本地事务,例如把Executor上的数据通过rdd.collect算子提取到Driver端,由Driver端统一写入数据库,这样会将分布式事务变成本地事务的单线程操作,降低了写入的吞吐量
  • 使用场景

数据足够少(通常经过聚合后的数据量都比较小,明细数据一般数据量都比较大),并且支持事务的数据库

(2) 策略二:手动提交偏移量+幂等性处理

我们知道如果能够同时解决数据丢失和数据重复问题,就等于做到了精确一次消费。那就各个击破。

首先解决数据丢失问题,办法就是要等数据保存成功后再提交偏移量,所以就必须手工来控制偏移量的提交时机。

但是如果数据保存了,没等偏移量提交进程挂了,数据会被重复消费。怎么办?那就要把数据的保存做成幂等性保存。即同一批数据反复保存多次,数据不会翻倍,保存一次和保存一百次的效果是一样的。如果能做到这个,就达到了幂等性保存,就不用担心数据会重复了。

Spark实时统计交易数据架构 spark实时项目_spark_11

  • 难点

话虽如此,在实际的开发中手动提交偏移量其实不难,难的是幂等性的保存,有的时候并不一定能保证,这个需要看使用的数据库,如果数据库本身不支持幂等性操作,那只能优先保证的数据不丢失,数据重复难以避免,即只保证了至少一次消费的语义

一般有主键的数据库都支持幂等性操作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等工具手动对偏移量进行保存

  • 流程

Spark实时统计交易数据架构 spark实时项目_spark_12

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中写

Spark实时统计交易数据架构 spark实时项目_实时大数据_13

  • 再次运行,Redis中有数据,直接从Redis中读

Spark实时统计交易数据架构 spark实时项目_Spark实时统计交易数据架构_14

2.5.6 幂等性操作

其实我们前面已经使用Redis进行了去重操作,基本上是可以保证幂等性的。如果更严格的保证幂等性,那我们在批量向ES写数据的时候,指定Index的id即可

  • 修改MyESUtil

Spark实时统计交易数据架构 spark实时项目_apache_15

  • 修改DauApp

Spark实时统计交易数据架构 spark实时项目_kafka_16

  • 测试

清除ES中的数据,重新运行程序,查看效果

Spark实时统计交易数据架构 spark实时项目_实时大数据_17

2.5.7 关于去重

我们是通过Redis完成的去重,其实也可以在ES中进行去重操作

但是通过Redis去重,保留的是前面的数据,有就不向里加

通过ES或者其他数据库去重,是完成的替换,保留的是后面的数据

根据实际的需求选择合适的实现

2.6 利用kibana 搭建数据可视化

如果数据保存在Elasticsearch那么利用kibana进行可视化展示是一种非常高效的可视化解决方案。这种kibana可视化方案,优势是快速高效,但是对于展示效果的定制化和炫酷程度不能有太高的要求。

2.6.1 步骤一:创建 IndexPatterns

其实就是创建数据源 确定数据范围

  • 在 Management中创建Index Patterns

Spark实时统计交易数据架构 spark实时项目_kafka_18

  • 配置匹配模式,通过用“*”来控制后缀可以得到不同的数据范围

Spark实时统计交易数据架构 spark实时项目_实时大数据_19

  • 选择一个时间字段,用于对可视化图形灵活筛选时间范围

Spark实时统计交易数据架构 spark实时项目_apache_20

2.6.2 步骤二:配置单图

  • 在VIsualize选择加号

Spark实时统计交易数据架构 spark实时项目_spark_21

  • 选择一个可视化图形,我们这里选择垂直柱状图

Spark实时统计交易数据架构 spark实时项目_apache_22

  • 选择一个index pattern

Spark实时统计交易数据架构 spark实时项目_spark_23

  • 柱状图配置
  • Y轴配置度量值

Aggregation:聚合方法

Customer Label: Y轴的显示标签

  • X 轴配置维度列

Aggregation: 分组方法

Field: 分组字段

Order by : 分组如何排序

Order 排序顺序

size 列出前N名

Group other …. : 是否把不在TopN的分组组合起来组成一个Other列

Show Missing Values 是否把空值数据汇总起来组成一个Missing列。

Customer Label: X轴的显示标签

Spark实时统计交易数据架构 spark实时项目_apache_24

  • 右上角选择刷新频率和时间范

Spark实时统计交易数据架构 spark实时项目_kafka_25

  • 配置好后点击 执行按钮

Spark实时统计交易数据架构 spark实时项目_实时大数据_26

  • 最后保存,命名


Spark实时统计交易数据架构 spark实时项目_apache_27

Spark实时统计交易数据架构 spark实时项目_Spark实时统计交易数据架构_28

练习:按照上面的步骤配置一个热力图

2.6.3 步骤三:配置仪表盘

仪表盘 Dashboard,就是配置多个图在一个页面上同时展示

  • 选择Dashboard 然后选择Create new DashBoard

Spark实时统计交易数据架构 spark实时项目_Spark实时统计交易数据架构_29

  • 然后选择add

Spark实时统计交易数据架构 spark实时项目_Spark实时统计交易数据架构_30

  • 从之前配置的可视化图中,选择图形。

Spark实时统计交易数据架构 spark实时项目_apache_31

  • 选好后选择save保存。就可以在Dashboard列表中查看大盘图了

Spark实时统计交易数据架构 spark实时项目_kafka_32

  • 拷贝内嵌代码
  • 新建一个html页面,将拷贝的iframe粘贴进去,可以查看数据

Spark实时统计交易数据架构 spark实时项目_spark_33

  • 修改/opt/module/rt_applog/application.properties,再生成一天数据,运行DauApp,可以看到动态变化

2.7 功能4:从ES中查询出数据,发布成数据接口,可视化工程进行调用

2.7.1 思路

  • 目标效果

Spark实时统计交易数据架构 spark实时项目_spark_34

  • 要求数据格式

总数

[{“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

Spark实时统计交易数据架构 spark实时项目_实时大数据_35

  • 勾选相关依赖

Spark实时统计交易数据架构 spark实时项目_实时大数据_36

  • 在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
  • 搭建项目结构

Spark实时统计交易数据架构 spark实时项目_Spark实时统计交易数据架构_37

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或其它可以忽略的级别

Spark实时统计交易数据架构 spark实时项目_kafka_38

  • 在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测试

Spark实时统计交易数据架构 spark实时项目_spark_39

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测试

Spark实时统计交易数据架构 spark实时项目_实时大数据_40

  • 搭建可视化工程进行对接

从资料中拷贝dw-echart前端工程进行部署,启动主程序