客快物流大数据项目(一百零一):实时OLAP开发 clickhouse的OLAP代码

实时OLAP开发

一、实时ETL处理

使用ClickHouse分析物流指标数据,必须将数据存储到ClickHouse中。

业务流程:

二、SparkSQL基于DataSourceV2自定义数据源

1、​​​​​​​​​​​​​​Data Source API V1

Spark 1.3 版本开始引入了 Data Source API V1,通过这个 API 我们可以很方便的读取各种来源的数据,而且 Spark 使用 SQL 组件的一些优化引擎对数据源的读取进行优化,比如列裁剪、过滤下推等等。

这个版本的 Data Source API 有以下几个优点:

  • 接口实现非常简单
  • 能够满足大部分的使用场景

同时存在一些问题:

  • 扩展能力有限,难以下推其他算子
  • 缺乏对列式存储读取的支持
  • 写操作不支持事务
  • 缺乏分区和排序信息
  • 不支持流处理
2、Data Source API V2

Data Source API V2为了解决 Data Source V1 的一些问题,从 Apache Spark 2.3.0 版本开始,社区引入了 Data Source API V2,在保留原有的功能之外,还解决了 Data Source API V1 存在的一些问题,比如不再依赖上层 API,扩展能力增强。

这个版本的 Data Source API V2 有以下几个优点:

  • DataSourceV2 API使用Java编写
  • 不依赖于上层API(DataFrame/RDD)
  • 易于扩展,可以添加新的优化,同时保持向后兼容
  • 提供物理信息,如大小、分区等
  • 支持Streaming Source/Sink
  • 灵活、强大和事务性的写入API

Spark2.3中V2的功能

  • 支持列扫描和行扫描
  • 列裁剪和过滤条件下推
  • 可以提供基本统计和数据分区
  • 事务写入API
  • 支持微批和连续的Streaming Source/Sink

三、​​​​​​​​​​​​​​基于DataSourceV2实现输入源

1、​​​​​​​​​​​​​​ReadSupport & WriteSupport

为了使用 Data Source API V2,我们肯定是需要使用到 Data Source API V2 包里面相关的类库,对于读取程序,我们只需要实现 ReadSupport 相关接口就行,如下:

代码实现:

/**
 * Spark SQL 基于DataSourceV2接口实现自定义数据源
 * 1.继承DataSourceV2向Spark注册数据源
 * 2.继承ReadSupport支持读数据
 * 3.继承WriteSupport支持写数据
 */
class CustomDataSourceV2 extends DataSourceV2 with ReadSupport with WriteSupport { /** * 创建Reader * * @param options 用户自定义的options * @return 返回自定义的DataSourceReader */ override def createReader(options: DataSourceOptions): DataSourceReader = ??? /** * 创建Writer * * @param jobId jobId * @param schema schema * @param mode 保存模式 * @param options 用于定义的option * @return Optional[自定义的DataSourceWriter] */ override def createWriter(jobId: String, schema: StructType, mode: SaveMode, options: DataSourceOptions): Optional[DataSourceWriter] = ??? }
2、​​​​​​​​​​​​​​DataSourceReader & DataSourceWriter

前面我们实现了 ReadSupport 接口,并重写了 createReader 方法。这里我们需要实现 DataSourceReader 接口相关的操作,如下:

/**
 * 自定义的DataSourceReader
 * 继承DataSourceReader
 * 重写readSchema方法用来生成schema
 * 重写planInputPartitions,每个分区拆分及读取逻辑
 * @param options options
 */
case class CustomDataSourceV2Reader(options: Map[String, String]) extends DataSourceReader { /** * 读取的列相关信息 * @return */ override def readSchema(): StructType = ??? /** * 每个分区拆分及读取逻辑 * @return */ override def planInputPartitions(): util.List[InputPartition[InternalRow]] = ??? } /** * 自定义的DataSourceWriter * 继承DataSourceWriter * 重写createWriterFactory方法用来创建RestDataWriter工厂类 * 重写commit方法,所有分区提交的commit信息 * 重写abort方法,当write异常时调用,该方法用于事务回滚,当write方法发生异常之后触发该方法 * @param dataSourceOptions options */ class CustomDataSourceWriter(dataSourceOptions: DataSourceOptions) extends DataSourceWriter { /** * 创建RestDataWriter工厂类 * @return DataWriterFactory */ override def createWriterFactory(): DataWriterFactory[InternalRow] = ??? /** * commit * @param writerCommitMessages 所有分区提交的commit信息 * 触发一次 */ override def commit(writerCommitMessages: Array[WriterCommitMessage]): Unit = ??? /** * * abort * @param writerCommitMessages 当write异常时调用,该方法用于事务回滚,当write方法发生异常之后触发该方法 */ override def abort(writerCommitMessages: Array[WriterCommitMessage]): Unit = ??? }
3、读写实现

最后一个需要我们实现的就是分片读取,在 DataSource V1 里面缺乏分区的支持,而 DataSource V2 支持完整的分区处理,也就是上面的 planInputPartitions 方法。

在那里我们可以定义使用几个分区读取数据源的数据。比如如果是 TextInputFormat,我们可以读取到对应文件的 splits 个数,然后每个 split 构成这里的一个分区,使用一个 Task 读取。为了简便起见,我这里使用了只使用了一个分区,也就是 List[InputPartition[InternalRow]].asJava。

SparkSQL的DataSourceV2的实现与StructuredStreaming自定义数据源如出一辙,思想是一样的,但是具体实现有所不同

主要步骤如下:

  • 继承DataSourceV2和ReadSupport创建XXXDataSource类,重写ReadSupport的creatReader方法,用来返回自定义的DataSourceReader类,如返回自定义XXXDataSourceReader实例
  • 继承DataSourceReader创建XXXDataSourceReader类,重写DataSourceReader的readSchema方法用来返回数据源的schema,重写DataSourceReader的planInputPartitions用来返回多个自定义DataReaderFactory实例
  • 继承DataReaderFactory创建DataReader工厂类,如XXXDataReaderFactory,重写DataReaderFactory的createDataReader方法,返回自定义DataRader实例
  • 继承DataReader类创建自定义的DataReader,如XXXDataReader,重写DataReader的next()方法,用来告诉Spark是否有下条数据,用来触发get()方法,重写DataReader的get()方法获取数据,重写DataReader的close()方法用来关闭资源

四、编写ClickHouse操作的自定义数据源

实现步骤:

  • logistics-etl模块cn.it.logistics.etl.realtime.ext.clickhouse程序包下创建ClickHouseDataSourceV2
  • 分别继承自ReadSupport、WriteSupport、StreamWriteSupport接口
  • 依次实现各个接口的方法
    • createReader(批处理方式下的数据读取)
    • createWriter(批处理方式下的数据写入)
    • createStreamWriter(流处理方式下的数据写入)
  • 创建连接Clickhouse所需要的的参数对象(ClickHouseOptions)
  • 创建操作ClickHouse的工具类(ClickHouseHelper)
    • 实现获取ClickHouse连接对象的方法
    • 实现创建表的方法
    • 实现生成插入sql语句的方法
    • 实现生成修改sql语句的方法
    • 实现生成删除sql语句的方法
    • 实现批量更新sql的方法
  • 创建测试单例对象读取clickhouse的数据以及将数据写入clickhouse中

实现方法:

  • logistics-etl模块cn.it.logistics.etl.realtime.ext.clickhouse程序包下创建ClickHouseDataSourceV2
package cn.it.logistics.etl.realtime.ext.clickhouse /** * @ClassName ClickHouseDataSourceV2 * @Description 扩展SparkSQL DataSourceV2的ClickHouse数据源实现 */ class ClickHouseDataSourceV2 { }
  • 分别继承自ReadSupport、WriteSupport、StreamWriteSupport接口
/**
 * @ClassName ClickHouseDataSourceV2
 * @Description 扩展SparkSQL DataSourceV2的ClickHouse数据源实现
 */
class ClickHouseDataSourceV2 extends DataSourceV2 with ReadSupport with WriteSupport with StreamWriteSupport { }
  • 依次实现各个接口的方法
    • createReader(批处理方式下的数据读取)
    • createWriter(批处理方式下的数据写入)
    • createStreamWriter(流处理方式下的数据写入)
/**
 * @ClassName ClickHouseDataSourceV2
 * @Description 扩展SparkSQL DataSourceV2的ClickHouse数据源实现
 */
class ClickHouseDataSourceV2 extends DataSourceV2 with ReadSupport with WriteSupport with StreamWriteSupport { /** 批处理方式下的数据读取 */ override def createReader(options: DataSourceOptions): DataSourceReader = ??? /** 批处理方式下的数据写入 */ override def createWriter(writeUUID: String, schema: StructType, mode: SaveMode, options: DataSourceOptions): Optional[DataSourceWriter] = ??? /** 流处理方式下的数据写入 */ override def createStreamWriter(queryId: String, schema: StructType, mode: OutputMode, options: DataSourceOptions): StreamWriter = ??? }
1、​​​​​​​​​​​​​​批处理方式下的数据读取

实现步骤:

  • 自定义ClickHouseDataSourceReader类继承自DataSourceReader接口
  • 实现如下方法:
    • readSchema()(该方法主要是基于Clickhouse的表结构构建schama对象)
    • planInputPartitions()(针对每个分区的数据读取逻辑的实现)
  • 自定义每个分区数据读取逻辑的实现类:ClickHouseInputPartition,继承InputPartition接口,并实现如下方法:
    • createPartitionReader(创建分区数据读取对象)
  • 自定义分区数据读取对象:ClickHouseInputPartitionReader,继承自InputPartitionReader接口及Serializable接口,并实现如下方法:
    • next()(是否有下一条数据,返回true表示有数据)
    • get()(读取数据)
    • close()(关闭jdbc连接,释放资源)
  • ClickHouseDataSourceV2 对象的createReader方法赋值

实现方法:

  • 自定义ClickHouseDataSourceReader类继承自DataSourceReader接口
/**
 * 基于批处理的方式对ClickHouse数据库中的数据进行读取
 */
class ClickHouseDataSourceReader(options: ClickHouseOptions) extends DataSourceReader { }
  • 实现如下方法:
    • readSchema()(该方法主要是基于Clickhouse的表结构构建schama对象)
    • planInputPartitions()(针对每个分区的数据读取逻辑的实现)
/**
 * 基于批处理的方式对ClickHouse数据库中的数据进行读取
 */
class ClickHouseDataSourceReader(options: ClickHouseOptions) extends DataSourceReader { //实例化ClickHouseHelper工具类 val ckHelper = new ClickHouseHelper(options) private val schema: StructType = ckHelper.getSparkTableSchema /** * 读取数据需要返回DataFrame对象(RDD+Schema组成) * 读取表的结构信息(schema) * @return */ override def readSchema(): StructType = schema /** * 每个分区拆分读取逻辑的实现(返回所有分区的数据) * @return */ override def planInputPartitions(): util.List[InputPartition[InternalRow]] = util.Arrays.asList(new ClickHouseInputPartition(schema, options)) }
  • ClickHouseHelper单例对象中实现getSparkTableSchema方法
package cn.it.logistics.etl.realtime.ext import java.sql.{Connection, Date, PreparedStatement, ResultSet, Statement} import java.text.SimpleDateFormat import java.util import org.apache.commons.lang3.StringUtils import org.apache.spark.internal.Logging import org.apache.spark.sql.catalyst.InternalRow import org.javatuples.Triplet import ru.yandex.clickhouse.domain.ClickHouseDataType import ru.yandex.clickhouse.response.{ClickHouseResultSet, ClickHouseResultSetMetaData} import ru.yandex.clickhouse.{ClickHouseConnection, ClickHouseDataSource, ClickHouseStatement} import ru.yandex.clickhouse.settings.ClickHouseProperties import org.apache.spark.sql.types.{BooleanType, DataType, DataTypes, DateType, DoubleType, FloatType, IntegerType, LongType, StringType, StructField, StructType} import scala.collection.mutable.ArrayBuffer /** * clickHouse操作的工具类 */ class ClickHouseHelper(options: ClickHouseOptions) extends Logging{ private val opType: String = options.getOpTypeField private var connection: ClickHouseConnection = getConnection private val id: String = options.getPrimaryKey /** * 获取Clickhouse的连接对象 */ def getConnection = { //获取clickhouse的连接字符串 val url: String = options.getURL //创建clickhouseDataSource对象 val clickHouseDataSource: ClickHouseDataSource = new ClickHouseDataSource(url, new ClickHouseProperties()) //返回clickhouse的连接对象 clickHouseDataSource.getConnection } /** * 返回指定表的schema信息 * @return StructType:sparkDataFrame对象的schema信息 */ def getSparkTableSchema: StructType = { import collection.JavaConversions._ val clickHouseTableSchema: util.LinkedList[Triplet[String, String, String]] = getClickHouseTableSchema //println(clickHouseTableSchema) val fileds = ArrayBuffer[StructField]() //基于clickhouse的表的列及列的类型创建schema对象 for (trp <- clickHouseTableSchema) { fileds += StructField(trp.getValue0, getSparkSqlType(trp.getValue1)) } //返回structType对象,该对象就是schema StructType(fileds) } /** * 根据clickhouseTable的列及列的类型集合 */ def getClickHouseTableSchema = { //定义列的集合 val fileds: util.LinkedList[Triplet[String, String, String]] = new util.LinkedList[Triplet[String, String, String]]() //查询指定的表数据,返回查询到的结果及列的信息 //定义clickhouse的connection对象 var connection: ClickHouseConnection = null var statement: ClickHouseStatement = null var resultSet: ClickHouseResultSet = null var metaData: ClickHouseResultSetMetaData = null try { //获取connection的连接对象 connection = getConnection statement = connection.createStatement() //定义要操作的表的sql语句,目前我们需要的是表的字段及字段类型,而不关心表的数据,因此给定不能满足的查询条件 val sql: String = s"select * FROM ${options.getFullTable} where 1=0" resultSet = statement.executeQuery(sql).asInstanceOf[ClickHouseResultSet] //获取到了指定表的元数据信息 metaData = resultSet.getMetaData.asInstanceOf[ClickHouseResultSetMetaData] val columnCount: Int = metaData
  • 13
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ClickHouse是一种OLAP(联机分析处理)存储引擎,它使用列式存储和MPP(大规模并行处理)的概念。它是一个目前比较受欢迎的OLAP存储引擎,适用于处理大规模数据集并进行复杂的分析查询。 ClickHouse具有高度的并行性和可伸缩性,能够将查询拆分为多个任务并在集群中的多台机器上并行处理,最后将结果汇总。对于具有多个副本的情况,ClickHouse还提供了多种查询下发策略,以确保高性能和高可用性。 总结来说,ClickHouse是一种用于大规模数据分析的OLAP存储引擎,它采用列式存储和MPP的架构,具有高度的并行性和可伸缩性,能够高效地处理复杂的分析查询。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [clickhouse文档.docx](https://download.csdn.net/download/a904364908/12853393)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [OLAPClickHouse讲解](https://blog.csdn.net/syyyyyyyyyyyyyyh/article/details/120082559)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值