Flink实时数仓同步:拉链表实战详解

一、背景

在大数据领域,业务数据通常最初存储在关系型数据库,例如MySQL。然而,为了满足日常分析和报表等需求,大数据平台会采用多种不同的存储方式来容纳这些业务数据。这些存储方式包括离线仓库、实时仓库等,根据不同的业务需求和数据特性进行选择。

举例来说,假设业务部门需要在大数据平台中查看历史某一天的表数据,如下:

  1. [Mysql] 业务数据 - 用户表全量数据:
idnamephonegendercreate_timeupdate_time
1jack1112023-06-01 13:00:002023-06-01 13:00:00
2jason2222023-06-01 13:00:002023-06-01 13:00:00
3tom3332023-06-01 13:00:002023-06-01 13:00:00
  1. [Mysql] 2023-06-02 业务数据新增了一名用户,且更改了tom的手机号,此时表数据如下:
idnamephonegendercreate_timeupdate_time
1jack1112023-06-01 13:00:002023-06-01 13:00:00
2jason2222023-06-01 13:00:002023-06-01 13:00:00
3tom4442023-06-01 13:00:002023-06-02 09:00:00
4tony5552023-06-02 10:00:002023-06-02 10:00:00

加粗为更新/新增数据

  1. [大数据平台] 2023-06-02日业务人员在大数据平台中查看用户表实时数据,期望数据和Mysql业务数据一致,如下:
idnamephonegendercreate_timeupdate_time
1jack1112023-06-01 13:00:002023-06-01 13:00:00
2jason2222023-06-01 13:00:002023-06-01 13:00:00
3tom4442023-06-01 13:00:002023-06-02 09:00:00
4tony5552023-06-02 10:00:002023-06-02 10:00:00
  1. [大数据平台] 2023-06-03 日业务人员在大数据平台中查看2023-06-02日用户表的历史数据,期望数据如下:
idnamephonegendercreate_timeupdate_time
1jack1112023-06-01 13:00:002023-06-01 13:00:00
2jason2222023-06-01 13:00:002023-06-01 13:00:00
3tom3332023-06-01 13:00:002023-06-01 13:00:00

根据以上需求,业务人员希望既能够查看当天的实时数据,又希望查看以天为粒度的历史数据。这类需求比较常见,通常可以采用两种解决方式:

  1. Lambda架构
  2. 实时同步 + 拉链表架构

二、Lambda架构

实时领域的Lambda架构是一种大数据架构模式,旨在处理实时数据流和历史数据批处理,以满足同时满足实时查询和历史数据分析的需求。Lambda架构的核心思想是将数据分成两个独立的流程:实时流程和批处理流程,并在最终层将它们合并,以提供一致的查询结果,如下:

在这里插入图片描述

  1. 实时流程(Real-time Layer):实时流程负责处理实时产生的数据流。它通常包括以下关键组件:

    • 数据源:实时数据源,如binlog日志等。
    • 实时引擎:用于实时数据的处理和转换,例如Apache Kafka、Apache Flink等。
    • 存储层:用于存储实时数据,特点是插入快,支持OLAP查询。
  2. 离线处理流程(Batch Layer):离线处理流程用于处理历史数据,通常以 T+1 凌晨跑批方式运行,主要包括以下组件:

    • 数据仓库:批处理数据存储,通常使用分布式数据仓库,如Apache Hadoop HDFS、Apache Hive等。
    • 批处理作业:用于处理历史数据的定期批处理作业,例如数据清洗、转换和聚合。
  3. 合并层(Serving Layer):合并层负责将实时和历史数据合并以提供一致的查询接口:

    • 数据服务:根据用户查询内容选择性调用不同存储服务,用于将实时数据和批处理数据合并以生成一致的视图。
  4. Lambda架构的主要优点包括:

    • 实时性:能够提供近实时的数据处理和反馈,适用于需要快速决策和实时监控的场景。

    • 容错性:通过将数据存储在持久性存储中,保证了数据的可靠性和可恢复性。

    • 灵活性:可以应对多种不同的数据类型和查询需求,适用于各种大数据应用。

注意:尽管Lambda架构可以满足业务人员查看用户的实时或历史数据的需求,但离线数据仓库通常采用T+1批处理方式运行,因此在需要高度一致性的场景下会出现数据不一致问题。故本文未采用Lambda架构;

若想详细了解一致性问题的情况,请参考笔者另一篇文章:深入数仓离线数据同步:问题分析与优化措施

三、实时同步+拉链表架构

为了满足业务人员对实时或历史数据的高度一致性需求,并且为了简化架构,这里采用了实时+拉链表的技术方案。在这个架构中,只使用了一种计算引擎,具体的技术组件为 Flink-cdc-2.x + Doris。以下是我们架构的设计概述:

在这里插入图片描述

此架构的关键在于实时同步逻辑及拉链表设计这两块的实现。

3.1、拉链表设计

拉链表是一种维护历史状态以及最新状态数据的表,与快照表类似,算是在快照表的基础上去除了重复状态的数据;使用拉链表在更新频率和比例不是很大的情况下会十分节省存储。

3.1.1、示例

  1. 我们以背景需求为例,[Mysql]业务数据用户表如下:
idnamephonegendercreate_timeupdate_time
1jack1112023-06-01 13:00:002023-06-01 13:00:00
2jason2222023-06-01 13:00:002023-06-01 13:00:00
3tom3332023-06-01 13:00:002023-06-01 13:00:00
  1. [Doris]此时实时同步到Doris的拉链表数据为:
idnamephonegendercreate_timeupdate_timeexpirestart dateend date
1jack1112023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31
2jason2222023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31
3tom3332023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31

可以看到拉链表多了expire,start date,end date 三个字段,用于表示该条数据是否过期、开始时间及有效时间,下面会有说明

  1. [Mysql] 2023-06-02 业务数据新增了一名用户,更新了tom的手机号,此时表数据如下:
idnamephonegendercreate_timeupdate_time
1jack1112023-06-01 13:00:002023-06-01 13:00:00
2jason2222023-06-01 13:00:002023-06-01 13:00:00
3tom4442023-06-01 13:00:002023-06-02 09:00:00
4tony5552023-06-02 10:00:002023-06-02 10:00:00

加粗为更改/新增数据

  1. [Doris]此时实时同步到Doris的拉链表数据为:
idnamephonegendercreate_timeupdate_timeexpirestart_dateend_date备注
1jack1112023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31
2jason2222023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31
3tom3332023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-01(由9999-12-31改为2023-06-01)
3tom4442023-06-01 13:00:002023-06-02 09:00:0002023-06-029999-12-31(新增一条拉链数据)
4tony5552023-06-02 10:00:002023-06-02 10:00:0002023-06-029999-12-31(新增一条最新用户数据)

由于tom的手机号被修改,根据拉链表特性此时会新增一条最新的tom数据,且过期时间为9999-12-31,旧数据不会删除而是将有效时间end date改为2023-06-01

  1. [Mysql] 2023-06-03 当天多次更新业务数据jason用户的手机号,sql及表数据如下:
UPDATE `user` SET `phone`='333', `update_time`='2023-06-03 10:00:00' WHERE `id`=2;
UPDATE `user` SET `phone`='444', `update_time`='2023-06-03 12:00:00' WHERE `id`=2;
UPDATE `user` SET `phone`='555', `update_time`='2023-06-03 14:00:00' WHERE `id`=2;
idnamephonegendercreate_timeupdate_time备注
1jack1112023-06-01 13:00:002023-06-01 13:00:00
2jason5552023-06-01 13:00:002023-06-03 14:00:00(jason手机号从222 -> 333 -> 444 -> 555更改了三次)
3tom4442023-06-01 13:00:002023-06-02 09:00:00
4tony5552023-06-02 10:00:002023-06-02 10:00:00
  1. [Doris]此时实时同步到Doris的拉链表数据为:
idnamephonegendercreate_timeupdate_timeexpirestart_dateend_date备注
1jack1112023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31
2jason2222023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-02(由9999-12-31改为2023-06-02)
2jason3332023-06-01 13:00:002023-06-03 10:00:0012023-06-032023-06-03(当天更新多次的过期数据)
2jason4442023-06-01 13:00:002023-06-03 12:00:0012023-06-032023-06-03(当天更新多次的过期数据)
2jason5552023-06-01 13:00:002023-06-03 14:00:0002023-06-039999-12-31(新增一条拉链数据)
3tom3332023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-01
3tom4442023-06-01 13:00:002023-06-02 09:00:0002023-06-029999-12-31
4tony5552023-06-02 10:00:002023-06-02 10:00:0002023-06-029999-12-31

使用 expire 字段来表示记录是否过期,下面会说明

  1. 说明
  • start_date表示该条记录的生命周期开始时间【第一次全量同步时为系统时间,增量同步时为update_time时间】,end_date表示该条记录的生命周期结束时间

  • end_date = '9999-12-31’表示该条记录为最新数据

  • end_date = '2023-06-02’表示该条记录仅在2023-06-02当日有效

  • expire字段用于标识记录的状态,1表示记录已过期,0表示记录有效。该字段目的是用于过滤那些在一天之内多次更新的数据

  • 如果查询当前的最新记录,sql为:select * from user where end_date = ‘9999-12-31’

  • 如果查询2023-06-02的历史快照,sql为:select * from user where start_date <= ‘2023-06-02’ and end_date >= ‘2023-06-02’ and expire = 0(此处是拉链表比较重要的一块)

  • 解释上一条sql:需求是要查2023-06-02的历史快照,故start_date <= ‘2023-06-02’;而end_date = '2023-06-02’表示该条记录在2023-06-02当日是有效的,又因为end_date = '9999-12-31’表示目前一直处于有效状态【有可能从2023-06-02到目前一直有效的数据】,所以end_date >= ‘2023-06-02’

  • 示例:查询2023-06-01日历史数据:select * from user where start_date <= ‘2023-06-01’ and end_date >= ‘2023-06-01’ and expore = 0

idnamephonegendercreate_timeupdate_timestart dateend date
1jack1112023-06-01 13:00:002023-06-01 13:00:002023-06-019999-12-31
2jason2222023-06-01 13:00:002023-06-01 13:00:002023-06-012023-06-02
3tom3332023-06-01 13:00:002023-06-01 13:00:002023-06-012023-06-01
  • 示例:查询2023-06-02日历史数据:select * from user where start_date <= ‘2023-06-02’ and end_date >= ‘2023-06-02’ and expore = 0
idnamephonegendercreate_timeupdate_timestart dateend date
1jack1112023-06-01 13:00:002023-06-01 13:00:002023-06-019999-12-31
2jason2222023-06-01 13:00:002023-06-01 13:00:002023-06-012023-06-02
3tom4442023-06-01 13:00:002023-06-02 09:00:002023-06-029999-12-31
4tony5552023-06-02 10:00:002023-06-02 10:00:002023-06-029999-12-31
  • 示例:查询最新实时数据:select * from user where end_date = ‘9999-12-31’
idnamephonegendercreate_timeupdate_timestart dateend date
1jack1112023-06-01 13:00:002023-06-01 13:00:002023-06-019999-12-31
2jason5552023-06-01 13:00:002023-06-03 14:00:002023-06-039999-12-31
3tom4442023-06-01 13:00:002023-06-02 09:00:002023-06-029999-12-31
4tony5552023-06-02 10:00:002023-06-02 10:00:002023-06-029999-12-31

3.1.2、建表设计

在Doris中的表设计中,采用了Unique数据模型,这个决策的背后有一个关键因素,即利用唯一Key来处理Flink作业崩溃和重新启动时的数据覆盖操作,以及通过下游的幂等性来确保端到端的数据一致性。

唯一Key的选择在这里起到了至关重要的作用。在拉链表中,由于用户ID可能重复出现的情况【例如2023-06-02号tom就有两条数据】,故选择了一个组合Key: UNIQUE KEY(id, update_time) 来确保数据的唯一性。这种设计使得无论在什么情况下,我们都能够通过这个唯一Key来维护数据的一致性,即使在处理实时数据时发生了异常情况或重新启动作业时也不会出现问题。

  • 以上述为例建表语句如下:
CREATE TABLE IF NOT EXISTS example_user_zip
(
    `id` LARGEINT NOT NULL COMMENT "用户id",
    `update_time` DATETIME COMMENT "用户更新时间",
    `create_time` DATETIME COMMENT "用户注册时间",
    `name` VARCHAR(50) NOT NULL COMMENT "用户昵称",
    `phone` LARGEINT COMMENT "手机号",
    `gender` VARCHAR(5) COMMENT "用户性别",
    `expire` TINYINT DEFAULT '0' COMMENT "数据是否过期:0为有效,1为过期",
    `start_date` DATE COMMENT "开始时间",
    `end_date` DATE COMMENT "有效时间"
)
UNIQUE KEY(`id`, `update_time`) -- UNIQUE模型
COMMENT "用户拉链表"
DISTRIBUTED BY HASH(`id`) BUCKETS 32
PROPERTIES (
"replication_allocation" = "tag.location.default: 1"
);

关于doris更多数据模型可参考官网

3.2、实时同步逻辑【重要】

为了更清晰地解释拉链表的同步逻辑,我将以场景的方式逐步说明,如下:

  1. 全量更新

  2. 增量更新

    1. 新增数据
    2. 跨天更新数据
    3. 某条数据当天多次更新
    4. 删除数据
  3. 并发更新

3.2.1、全量更新

  1. 需先明确一点:拉链表的历史数据查询范围是从实时任务同步的那天开始,因为只有在实时任务开始同步的那一天之后,拉链表才正式形成,之前的历史数据是不可查询的。因此,当进行第一次全量同步时,我们会将 start_date 设置为当前系统日期。

  2. 另外,由于实时拉链表同步需要明确区分全量和增量更新,以及后续对 binlog 数据进行解析及判断增量更新操作类型,因此,Flink CDC SQL 方式的表建立不再满足我们的要求。为了更好地实现这一功能,我们需要采用 API 方式来构建解决方案,代码如下:

import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import com.ververica.cdc.debezium.JsonDebeziumDeserializationSchema;
import com.ververica.cdc.connectors.mysql.source.MySqlSource;

public class MySqlSourceExample {
  public static void main(String[] args) throws Exception {
    MySqlSource<String> mySqlSource = MySqlSource.<String>builder()
        .hostname("yourHostname")
        .port(yourPort)
        .databaseList("yourDatabaseName") // 设置捕获的数据库, 如果需要同步整个数据库,请将 tableList 设置为 ".*".
        .tableList("yourDatabaseName.yourTableName") // 设置捕获的表
        .username("yourUsername")
        .password("yourPassword")
        .deserializer(new JsonDebeziumDeserializationSchema()) // 将 SourceRecord 转换为 JSON 字符串
        .build();

    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

    // 设置 3s 的 checkpoint 间隔
    env.enableCheckpointing(3000);

    env
      .fromSource(mySqlSource, WatermarkStrategy.noWatermarks(), "MySQL Source")
      // 设置 source 节点的并行度为 4
      .setParallelism(4)
      .print().setParallelism(1); // 设置 sink 节点并行度为 1 

    env.execute("Print MySQL Snapshot + Binlog");
  }
}

代码摘自mysql-cdc-connector官网示例

  1. 这里我们仍以2023-06-01的[Mysql]业务数据为例:
idnamephonegendercreate_timeupdate_time
1jack1112023-06-01 13:00:002023-06-01 13:00:00
2jason2222023-06-01 13:00:002023-06-01 13:00:00
3tom3332023-06-01 13:00:002023-06-01 13:00:00
  1. 此时Flink应用获取到的数据如下:仅展示一条
{
	"before": null,
	"after": {		 # 实际数据
		"id": 1,
		"name": "jack",
		"phone": "111",
		"gender": "男",
		"create_time": "2023-06-01T05:00:00Z",  # 该日期是UTC时间,只需增加8小时即可转化为北京时间
		"update_time": "2023-06-01T05:00:00Z"	# 该日期是UTC时间,只需增加8小时即可转化为北京时间
	},
	"source": {		 # 元数据
		"version": "1.6.4.Final",
		"connector": "mysql",
		"name": "mysql_binlog_source",
		"ts_ms": 0,
		"snapshot": "false",
		"db": "yushu_dds",
		"sequence": null,
		"table": "user",
		"server_id": 0,
		"gtid": null,
		"file": "",
		"pos": 0,
		"row": 0,
		"thread": null,
		"query": null
	},
	"op": "r",  	 # 记录每条数据的操作类型[重要]
	"ts_ms": 1705471382867,
	"transaction": null
}
  1. 在我们使用 Flink CDC MySQL 同步数据时,默认采用 initial 模式,这意味着首先进行全量同步,然后再进行增量同步。因此,在区分全量和增量同步时,关键在于观察获取到的数据中的 op 字段。op 字段是用来记录每条数据的操作类型的标志。具体的操作类型如下:
    • op=d 代表删除操作
    • op=u 代表更新操作
    • op=c 代表新增操作
    • op=r 代表全量读取,而不是来自 binlog 的增量读取
  2. 在 Flink 程序中,只需要通过 op=r 即可筛选出全量数据。在全量数据同步阶段,Doris 拉链表的 start_date 字段设置为系统当前日期,而 end_date 字段则设置为 ‘9999-12-31’。导入语句如下:
INSERT INTO example_user_zip (id, update_time, create_time, name, phone, gender, expire, start_date, end_date)
VALUES 
(1, '2023-06-01 13:00:00', '2023-06-01 13:00:00', 'jack', 111, '男', 0, '2023-06-01', '9999-12-31'),
(2, '2023-06-01 13:00:00', '2023-06-01 13:00:00', 'jason', 222, '男', 0, '2023-06-01', '9999-12-31'),
(3, '2023-06-01 13:00:00', '2023-06-01 13:00:00', 'tom', 333, '男', 0, '2023-06-01', '9999-12-31');
  1. 此时doris拉链表数据如下所示:
idnamephonegendercreate_timeupdate_timeexpirestart_dateend date
1jack1112023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31
2jason2222023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31
3tom3332023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31

3.2.2、增量更新

当全量更新结束后即为增量更新,请注意以下内容:

  1. 在增量更新时,Doris 拉链表中的 start_date 字段【即开始时间】不再使用系统时间,而是业务数据的 update_time 截取后的日期。例如,update_time 为 “2023-06-02 13:00:00”,则对应的 start_date 为 “2023-06-02”。这么做的目的是为了确保使用事件时间来划分数据的开始时间,而不是系统时间。
  2. 举例来说,如果采用系统时间,假设实时同步任务某一天宕机并且没有重启,等到隔天再重启,那么 start_date 就会变成隔天日期,从而导致昨天的数据丢失。
  3. 为什么不使用业务数据的 create_time 作为拉链表的 start_date 呢?这是因为在业务数据更改时,通常只会更新 update_time。例如,2023-06-02 日更新了 “Tom” 的手机号码,此时同步到 Doris 新增的拉链数据如果使用 create_time,那么 start_date 仍然会是 “2023-06-01”,而实际上该条数据应该从 “2023-06-02” 日开始生效。因此,使用 update_time 更加合理,确保拉链表中的数据始终按照业务数据的更新时间来进行正确的版本管理。

接下来,我们将逐一讲解以下四个场景:新增更新、跨天更新、某条数据当天多次更新以及删除更新。

3.2.2.1、新增更新
  1. 我们仍以最初的示例为例:[Mysql] 2023-06-02 业务数据新增了一名用户,此时表数据如下:
idnamephonegendercreate_timeupdate_time
1jack1112023-06-01 13:00:002023-06-01 13:00:00
2jason2222023-06-01 13:00:002023-06-01 13:00:00
3tom3332023-06-01 13:00:002023-06-01 13:00:00
4tony5552023-06-02 10:00:002023-06-02 10:00:00

加粗为更新/新增数据

  1. 此时Flink应用获取到的数据如下所示:
{
	"before": null,
	"after": {
		"id": 4,
		"name": "tony",
		"phone": "555",
		"gender": "男",
		"create_time": "2023-06-02T02:00:00Z",
		"update_time": "2023-06-02T02:00:00Z"
	},
	"source": {
		# 此处元数据省略
	},
	"op": "c",
	"ts_ms": 1705477497504,
	"transaction": null
}
  1. 可以看到op=c 代表新增操作,对于新增操作doris拉链表的start_end为业务数据的update_time,而end_date均设置为9999-12-31,导入语句如下:
INSERT INTO example_user_zip (id, update_time, create_time, name, phone, gender, expire, start_date, end_date)
VALUES 
(4, '2023-06-02 10:00:00', '2023-06-02 10:00:00', 'tony', 555, '男', 0, '2023-06-02', '9999-12-31');
  1. 此时doris拉链表内容如下所示:
idnamephonegendercreate_timeupdate_timeexpirestart_dateend_date
1jack1112023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31
2jason2222023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31
3tom3332023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31
4tony5552023-06-02 10:00:002023-06-02 10:00:0002023-06-029999-12-31

加粗表示更新/新增数据

3.2.2.2、跨天更新

首先,解释一下为何要需要区分两种不同的更新场景:跨天更新和当天多次更新。这涉及到拉链表的历史数据粒度,拉链表通常以天为单位。如果一条数据在同一天内多次更新,那么每次更新后的数据的生存时间将只有几小时甚至几分钟。在这种情况下,我们希望在拉链表中将这种多次更新的临时数据设为过期数据;细节在后续会有讲解,先来解释跨天更新场景。

  1. 我们仍以最初的示例为例:[Mysql] 2023-06-02 业务数据更新了tom的手机号,此时表数据如下:
idnamephonegendercreate_timeupdate_time
1jack1112023-06-01 13:00:002023-06-01 13:00:00
2jason2222023-06-01 13:00:002023-06-01 13:00:00
3tom4442023-06-01 13:00:002023-06-02 09:00:00
4tony5552023-06-02 10:00:002023-06-02 10:00:00

加粗为更新/新增数据

  1. 此时Flink应用获取到的数据如下所示:
{
	"before": {	 # 更新前的数据
		"id": 3,
		"name": "tom",
		"phone": "333",
		"gender": "男",
		"create_time": "2023-06-01T05:00:00Z",
		"update_time": "2023-06-01T05:00:00Z"
	},
	"after": {	# 更新后的数据
		"id": 3,
		"name": "tom",
		"phone": "444",  # 手机号更新
		"gender": "男",
		"create_time": "2023-06-01T05:00:00Z",
		"update_time": "2023-06-02T01:00:00Z"  # 更新时间更新
	},
	"source": {
		# 此处元数据省略
	},
	"op": "u",
	"ts_ms": 1705479637926,
	"transaction": null
}
  1. 当我们在Flink应用中遇到op=u(代表更新操作)时,首先需要检查beforeafter字段中的update_time是否跨越了天粒度。可能跨越一天,也可能跨越多天,我们将在Doris拉链表中执行两条SQL语句:一条更新语句和一条插入语句。
    1. 对于更新语句,我们将更新拉链表中旧数据id的end_date字段,将其设置为after字段中update_time的前一天2023-06-01
    2. 对于插入语句,我们将插入after字段中的新数据,将start_date设置为update_time的日期,end_date设置9999-12-31),以确保该数据在拉链表中一直有效。
    3. sql如下所示:
-- 更新语句:
UPDATE example_user_zip SET end_date = '2023-06-01' WHERE `id`=3 AND `update_time`='2023-06-01 13:00:00';

-- 插入语句:
INSERT INTO example_user_zip (id, update_time, create_time, name, phone, gender, expire, start_date, end_date)
VALUES 
(3, '2023-06-02 09:00:00', '2023-06-01 13:00:00', 'tom', 444, '男', 0, '2023-06-02', '9999-12-31');
  1. 此时doris拉链表内容如下所示:
idnamephonegendercreate_timeupdate_timeexpirestart_dateend_date
1jack1112023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31
2jason2222023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31
3tom3332023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-01
3tom4442023-06-01 13:00:002023-06-02 09:00:0002023-06-029999-12-31
4tony5552023-06-02 10:00:002023-06-02 10:00:0002023-06-029999-12-31

加粗表示更新/新增数据

  1. 此时若要查看2023-06-01历史数据只需执行:
SELECT * FROM example_user_zip WHERE start_date <= '2023-06-01' AND end_date >= '2023-06-01' AND expire = 0;
idnamephonegendercreate_timeupdate_timeexpirestart dateend date
1jack1112023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31
2jason2222023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31
3tom3332023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-01
3.2.2.3、某条数据当天多次更新

在我们的拉链表中,数据的粒度是以天为单位。如果一条数据在同一天内多次更新,我们的处理策略是取最后一次更新为有效数据,而将之前的更新标记为过期数据。为了标记数据是否过期,我们会将过期数据的expire字段设置为1。

  1. 我们仍以最初的示例为例:[Mysql] 2023-06-03 当天多次更新业务数据jason用户的手机号,sql及表数据如下:
UPDATE `user` SET `phone`='333', `update_time`='2023-06-03 10:00:00' WHERE `id`=2;
UPDATE `user` SET `phone`='444', `update_time`='2023-06-03 12:00:00' WHERE `id`=2;
UPDATE `user` SET `phone`='555', `update_time`='2023-06-03 14:00:00' WHERE `id`=2;
idnamephonegendercreate_timeupdate_time备注
1jack1112023-06-01 13:00:002023-06-01 13:00:00
2jason5552023-06-01 13:00:002023-06-03 14:00:00(jason手机号从222 -> 333 -> 444 -> 555更改了三次)
3tom4442023-06-01 13:00:002023-06-02 09:00:00
4tony5552023-06-02 10:00:002023-06-02 10:00:00
  1. 此时Flink应用获取到的数据如下所示:
{
	"before": {
		"id": 2,
		"name": "jason",
		"phone": "222",
		"gender": "男",
		"create_time": "2023-06-01T05:00:00Z",
		"update_time": "2023-06-01T05:00:00Z"
	},
	"after": {
		"id": 2,
		"name": "jason",
		"phone": "333",
		"gender": "男",
		"create_time": "2023-06-01T05:00:00Z",
		"update_time": "2023-06-03T02:00:00Z"
	},
	"source": {
		# 元数据忽略		
	},
	"op": "u",
	"ts_ms": 1705548298335,
	"transaction": null
},
{
	"before": {
		"id": 2,
		"name": "jason",
		"phone": "333",
		"gender": "男",
		"create_time": "2023-06-01T05:00:00Z",
		"update_time": "2023-06-03T02:00:00Z"
	},
	"after": {
		"id": 2,
		"name": "jason",
		"phone": "444",
		"gender": "男",
		"create_time": "2023-06-01T05:00:00Z",
		"update_time": "2023-06-03T04:00:00Z"
	},
	"source": {
		# 元数据忽略		
	},
	"op": "u",
	"ts_ms": 1705548298392,
	"transaction": null
},
{
	"before": {
		"id": 2,
		"name": "jason",
		"phone": "444",
		"gender": "男",
		"create_time": "2023-06-01T05:00:00Z",
		"update_time": "2023-06-03T04:00:00Z"
	},
	"after": {
		"id": 2,
		"name": "jason",
		"phone": "555",
		"gender": "男",
		"create_time": "2023-06-01T05:00:00Z",
		"update_time": "2023-06-03T06:00:00Z"
	},
	"source": {
		# 元数据忽略
	},
	"op": "u",
	"ts_ms": 1705548298484,
	"transaction": null
}
  1. 当我们在Flink应用中遇到op=u(代表更新操作),且检查beforeafter字段中的update_time属于同一天,我们将在Doris拉链表中执行两条SQL语句:一条更新语句和一条插入语句。

    1. 对于更新语句,我们将更新拉链表中旧数据id的expire字段设置为1,将其设置为end_date字段值设置为update_time的当天日期2023-06-03
    2. 对于插入语句,我们将插入after字段中的新数据,将start_date设置为update_time的当天日期,end_date设置9999-12-31),以确保该数据在拉链表中一直有效。
    3. sql如下所示:
    -- 222 -> 333 跨天更新语句:
    UPDATE example_user_zip SET end_date = '2023-06-02' WHERE `id`=2 AND `update_time`='2023-06-01 13:00:00';
    
    -- 222 -> 333 跨天插入语句:
    INSERT INTO example_user_zip (id, update_time, create_time, name, phone, gender, expire, start_date, end_date)
    VALUES 
    (2, '2023-06-03 10:00:00', '2023-06-01 13:00:00', 'jason', 333, '男', 0, '2023-06-03', '9999-12-31');
    
    -- 333 -> 444 同一天更新语句:
    UPDATE example_user_zip SET expire = 1, end_date = '2023-06-03' WHERE `id`=2 AND `update_time`='2023-06-03 10:00:00';
    
    -- 333 -> 444 同一天插入语句:
    INSERT INTO example_user_zip (id, update_time, create_time, name, phone, gender, expire, start_date, end_date)
    VALUES 
    (2, '2023-06-03 12:00:00', '2023-06-01 13:00:00', 'jason', 444, '男', 0, '2023-06-03', '9999-12-31');
    
    -- 444 -> 555 同一天更新语句:
    UPDATE example_user_zip SET expire = 1, end_date = '2023-06-03' WHERE `id`=2 AND `update_time`='2023-06-03 12:00:00';
    
    -- 444 -> 555 同一天插入语句:
    INSERT INTO example_user_zip (id, update_time, create_time, name, phone, gender, expire, start_date, end_date)
    VALUES 
    (2, '2023-06-03 14:00:00', '2023-06-01 13:00:00', 'jason', 555, '男', 0, '2023-06-03', '9999-12-31');
    
  2. 此时doris拉链表内容如下所示:

idnamephonegendercreate_timeupdate_timeexpirestart_dateend_date备注
1jack1112023-06-01 13:00:002023-06-01 13:00:0002023-06-019999-12-31
2jason2222023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-02(由9999-12-31改为2023-06-02)
2jason3332023-06-01 13:00:002023-06-03 10:00:0012023-06-032023-06-03(当天更新多次的过期数据)
2jason4442023-06-01 13:00:002023-06-03 12:00:0012023-06-032023-06-03(当天更新多次的过期数据)
2jason5552023-06-01 13:00:002023-06-03 14:00:0002023-06-039999-12-31(新增一条最新数据)
3tom3332023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-01
3tom4442023-06-01 13:00:002023-06-02 09:00:0002023-06-029999-12-31
4tony5552023-06-02 10:00:002023-06-02 10:00:0002023-06-029999-12-31
  1. 此时若要查看2023-06-03历史数据只需执行:
SELECT * FROM example_user_zip WHERE start_date <= '2023-06-03' AND end_date >= '2023-06-03' AND expire = 0;
idnamephonegendercreate_timeupdate_timestart_dateend_date
1jack1112023-06-01 13:00:002023-06-01 13:00:002023-06-019999-12-31
2jason2222023-06-01 13:00:002023-06-01 13:00:002023-06-012023-06-02
3tom4442023-06-01 13:00:002023-06-02 09:00:002023-06-029999-12-31
4tony5552023-06-02 10:00:002023-06-02 10:00:002023-06-029999-12-31
3.2.2.4、删除更新

由于[Mysql]业务数据都具备唯一键,故业务数据的删除同步至拉链表无需判断是否跨天,只需更新删除数据的end_date日期为前一天即可。

  1. [Mysql] 2023-06-04 当天删除业务数据jack,表数据如下:
idnamephonegendercreate_timeupdate_time备注
2jason5552023-06-01 13:00:002023-06-03 14:00:00(jason手机号从222 -> 333 -> 444 -> 555更改了三次)
3tom4442023-06-01 13:00:002023-06-02 09:00:00
4tony5552023-06-02 10:00:002023-06-02 10:00:00
  1. 此时Flink应用获取到的数据如下所示:
{
	"before": {
		"id": 1,
		"name": "jack",
		"phone": "111",
		"gender": "男",
		"create_time": "2023-06-01T05:00:00Z",
		"update_time": "2023-06-01T05:00:00Z"
	},
	"after": null,
	"source": {
		# 忽略元数据
	},
	"op": "d", 	# 操作类型
	"ts_ms": 1705561813650,
	"transaction": null
}
  1. 可以看到op=d 代表删除操作,对于删除操作doris拉链表只需将before数据的date_date日期更新为前一日2023-06-03,导入语句如下:
-- 更新语句
UPDATE example_user_zip SET end_date = '2023-06-03' WHERE `id`=1 AND `update_time`='2023-06-01 13:00:00';
  1. 此时doris拉链表内容如下所示:
idnamephonegendercreate_timeupdate_timeexpirestart_dateend_date备注
1jack1112023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-03(由9999-12-31改为2023-06-03)
2jason2222023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-02
2jason3332023-06-01 13:00:002023-06-03 10:00:0012023-06-032023-06-03
2jason4442023-06-01 13:00:002023-06-03 12:00:0012023-06-032023-06-03
2jason5552023-06-01 13:00:002023-06-03 14:00:0002023-06-039999-12-31
3tom3332023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-01
3tom4442023-06-01 13:00:002023-06-02 09:00:0002023-06-029999-12-31
4tony5552023-06-02 10:00:002023-06-02 10:00:0002023-06-029999-12-31
  1. 此时若要查看2023-06-04数据只需执行:
SELECT * FROM example_user_zip WHERE start_date <= '2023-06-04' AND end_date >= '2023-06-04' AND expire = 0;
idnamephonegendercreate_timeupdate_timestart_dateend_date
2jason2222023-06-01 13:00:002023-06-01 13:00:002023-06-012023-06-02
3tom4442023-06-01 13:00:002023-06-02 09:00:002023-06-029999-12-31
4tony5552023-06-02 10:00:002023-06-02 10:00:002023-06-029999-12-31

3.2.3、并发更新

这里单独强调并发更新场景是因为在关系型数据库中,例如MySQL,通常使用timestamp类型来表示update_time,而该数据类型的最细粒度是秒。因此,当多个并发操作同时更新同一条数据时,update_time的值只会发生一次变化,但会产生多条binlog日志。由于Doris的拉链表以id + update_time作为唯一键,这种情况下会导致同一条数据多次更新。因此,这里单独讲解并发更新的情况。

需要注意的是,并发问题只存在于更新操作,删除和创建操作不会出现上述问题。

  1. [Mysql] 2023-06-05 当天 15:00:00 并发更新业务数据tony的手机号,表数据如下:
idnamephonegendercreate_timeupdate_time备注
2jason5552023-06-01 13:00:002023-06-03 14:00:00
3tom4442023-06-01 13:00:002023-06-02 09:00:00
4tony7772023-06-02 10:00:002023-06-05 15:00:00(tony手机号从555-> 666-> 777 并发更改两次)
  1. 此时Flink应用获取到的数据如下所示:
{
	"before": {
		"id": 4,
		"name": "tony",
		"phone": "555",
		"gender": "男",
		"create_time": "2023-06-02T02:00:00Z",
		"update_time": "2023-06-02T02:00:00Z"
	},
	"after": {
		"id": 4,
		"name": "tony",
		"phone": "666",
		"gender": "男",
		"create_time": "2023-06-02T02:00:00Z",
		"update_time": "2023-06-05T07:00:00Z"
	},
	"source": {
		# 元数据忽略
	},
	"op": "u",
	"ts_ms": 1705564093414,
	"transaction": null
},
{
	"before": {
		"id": 4,
		"name": "tony",
		"phone": "666",
		"gender": "男",
		"create_time": "2023-06-02T02:00:00Z",
		"update_time": "2023-06-05T07:00:00Z"
	},
	"after": {
		"id": 4,
		"name": "tony",
		"phone": "777",
		"gender": "男",
		"create_time": "2023-06-02T02:00:00Z",
		"update_time": "2023-06-05T07:00:00Z"
	},
	"source": {
		# 元数据忽略
	},
	"op": "u",
	"ts_ms": 1705564093478,
	"transaction": null
}
  1. 可以看到op=u 代表更新操作,这里我们仍沿用增量更新的逻辑,第一条日志中业务数据555->666属于跨天更新,第二条日志中业务数据666->777属于一条数据当天多次更新,DorisSql如下所示:
-- 555 -> 666 跨天更新语句:
UPDATE example_user_zip SET end_date = '2023-06-04' WHERE `id`=4 AND `update_time`='2023-06-02 10:00:00';

-- 555 -> 666 跨天插入语句:
INSERT INTO example_user_zip (id, update_time, create_time, name, phone, gender, expire, start_date, end_date)
VALUES 
(4, '2023-06-05 15:00:00', '2023-06-02 10:00:00', 'tony', 666, '男', 0, '2023-06-05', '9999-12-31');

-- 666 -> 777 同一天更新语句:
UPDATE example_user_zip SET expire = 1, end_date = '2023-06-05' WHERE `id`=4 AND `update_time`='2023-06-05 15:00:00';

-- 666 -> 777 同一天插入语句:
INSERT INTO example_user_zip (id, update_time, create_time, name, phone, gender, expire, start_date, end_date)
VALUES 
(4, '2023-06-05 15:00:00', '2023-06-02 10:00:00', 'tony', 777, '男', 0, '2023-06-05', '9999-12-31');
  1. 此时doris拉链表内容如下所示:
idnamephonegendercreate_timeupdate_timeexpirestart_dateend_date备注
1jack1112023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-03
2jason2222023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-02
2jason3332023-06-01 13:00:002023-06-03 10:00:0012023-06-032023-06-03
2jason4442023-06-01 13:00:002023-06-03 12:00:0012023-06-032023-06-03
2jason5552023-06-01 13:00:002023-06-03 14:00:0002023-06-039999-12-31
3tom3332023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-01
3tom4442023-06-01 13:00:002023-06-02 09:00:0002023-06-029999-12-31
4tony5552023-06-02 10:00:002023-06-02 10:00:0002023-06-022023-06-04(由9999-12-31改为2023-06-04)
4tony7772023-06-02 10:00:002023-06-05 15:00:0002023-06-059999-12-31(新增一条最新数据)

此时可以看到新增了一条Tony的数据,有些人可能注意到少了一条姓名为Tony、手机号为666、expire字段为1的数据。这是因为最后的更新和插入语句中的id + update_time完全一致,触发了Doris的replace替换操作。因此,最后一条插入语句覆盖了前一条更新语句,即"Tony, 666, expire=1"的数据被覆盖掉了。而这种替换操作反而变相解决了并发更新的问题。

  1. 此时若要查看2023-06-04数据只需执行:
SELECT * FROM example_user_zip WHERE start_date <= '2023-06-05' AND end_date >= '2023-06-05' AND expire = 0;
idnamephonegendercreate_timeupdate_timestart_dateend_date
2jason2222023-06-01 13:00:002023-06-01 13:00:002023-06-012023-06-02
3tom4442023-06-01 13:00:002023-06-02 09:00:002023-06-029999-12-31
4tony7772023-06-02 10:00:002023-06-05 15:00:002023-06-059999-12-31
  1. 此时有些同学会提出问题,即这个情况和上文中的跨天更新以及当天多次更新的逻辑有何不同? 似乎没有特殊的操作逻辑。确实,从逻辑上看,这两种情况是一致的。这是因为我们先执行更新操作,然后再执行新增操作。如果我们反过来,先执行新增操作,然后再执行更新操作,就会导致数据丢失。接下来,让我们看一下如果先执行新增操作再执行更新操作会发生什么情况。首先,我们将Doris拉链表恢复到前一天,如下所示:
idnamephonegendercreate_timeupdate_timeexpirestart_dateend_date备注
1jack1112023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-03(由9999-12-31改为2023-06-03)
2jason2222023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-02
2jason3332023-06-01 13:00:002023-06-03 10:00:0012023-06-032023-06-03
2jason4442023-06-01 13:00:002023-06-03 12:00:0012023-06-032023-06-03
2jason5552023-06-01 13:00:002023-06-03 14:00:0002023-06-039999-12-31
3tom3332023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-01
3tom4442023-06-01 13:00:002023-06-02 09:00:0002023-06-029999-12-31
4tony5552023-06-02 10:00:002023-06-02 10:00:0002023-06-029999-12-31
  1. 接下来我们将更新插入操作调换顺序,sql如下所示:
-- 555 -> 666 跨天插入语句:
INSERT INTO example_user_zip (id, update_time, create_time, name, phone, gender, expire, start_date, end_date)
VALUES 
(4, '2023-06-05 15:00:00', '2023-06-02 10:00:00', 'tony', 666, '男', 0, '2023-06-05', '9999-12-31');

-- 555 -> 666 跨天更新语句:
UPDATE example_user_zip SET end_date = '2023-06-04' WHERE `id`=4 AND `update_time`='2023-06-02 10:00:00';

-- 666 -> 777 同一天插入语句:
INSERT INTO example_user_zip (id, update_time, create_time, name, phone, gender, expire, start_date, end_date)
VALUES 
(4, '2023-06-05 15:00:00', '2023-06-02 10:00:00', 'tony', 777, '男', 0, '2023-06-05', '9999-12-31');

-- 666 -> 777 同一天更新语句:
UPDATE example_user_zip SET expire = 1, end_date = '2023-06-05' WHERE `id`=4 AND `update_time`='2023-06-05 15:00:00';
  1. 此时doris拉链表内容如下所示:
idnamephonegendercreate_timeupdate_timeexpirestart_dateend_date备注
1jack1112023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-03
2jason2222023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-02
2jason3332023-06-01 13:00:002023-06-03 10:00:0012023-06-032023-06-03
2jason4442023-06-01 13:00:002023-06-03 12:00:0012023-06-032023-06-03
2jason5552023-06-01 13:00:002023-06-03 14:00:0002023-06-039999-12-31
3tom3332023-06-01 13:00:002023-06-01 13:00:0002023-06-012023-06-01
3tom4442023-06-01 13:00:002023-06-02 09:00:0002023-06-029999-12-31
4tony5552023-06-02 10:00:002023-06-02 10:00:0002023-06-022023-06-04(由9999-12-31改为2023-06-04)
4tony7772023-06-02 10:00:002023-06-05 15:00:0012023-06-052023-06-05(更新拉链数据)

可以看到已经没有tony的最新数据了。

  1. 此时查看2023-06-05数据执行:只有两条数据
SELECT * FROM example_user_zip WHERE start_date <= '2023-06-05' AND end_date >= '2023-06-05' AND expire = 0;
idnamephonegendercreate_timeupdate_timestart_dateend_date
2jason2222023-06-01 13:00:002023-06-01 13:00:002023-06-012023-06-02
3tom4442023-06-01 13:00:002023-06-02 09:00:002023-06-029999-12-31

根据以上的测试结果,我们可以得出以下结论:当涉及到更新操作时,最好的做法是先执行更新,然后再执行插入操作。这种顺序可以有效避免并发更新问题。

此外,在实时引擎中处理数据通常涉及到分布式计算,因此需要特别注意确保相同ID的数据只在一个线程中按顺序执行,而不是让执行器01执行Tony的更新操作,而执行器02执行Tony的插入操作。相反,应该将具有相同ID的数据放置在同一个执行器中执行,以确保顺序性和一致性。这对于处理并发更新场景非常重要。

四、总结

本文我们深入探讨了如何使用Apache Flink实现实时数据仓库中拉链表的同步。拉链表是一种重要的数据模型,用于跟踪数据的历史变化,以便在分析和报告中提供准确的历史视图。我们介绍了如何借助Flink以及其他相关技术构建一个强大的实时同步引擎,以应对多种数据同步场景。

我们首先介绍了传统Lambda架构到实时同步+拉链表单引擎架构它们之间的区别。随后,我们深入讨论了Flink CDC(Change Data Capture)和Doris数据库的结合使用,以实现实时数据同步的基础架构。我们详细讨论了全量同步和增量同步两种关键同步模式,以及如何应对不同的更新场景。

在全文中,强调了以下关键点:

  • 实时同步+拉链表单引擎架构的设计和实施。
  • 全量同步和增量同步是实时数据仓库同步的两种关键模式,详细介绍了它们的实现逻辑。
  • 跨天更新和当天多次更新是需要特别注意的场景,提供了解决方案以确保数据的完整性。
  • 并发更新可能导致数据重复,需要采取适当的措施来应对。

通过深入了解实时同步和拉链表的实现细节,读者可以更好地理解如何构建强大的实时数据仓库,并满足不断变化的业务需求。

五、相关资料

  • Doris 数据模型
  • MySQL CDC Connector
  • 深入数仓离线数据同步:问题分析与优化措施

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/335144.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

github如果有别人给你的仓库提pull request,该如何验证他的代码并合并

我有一个github仓库&#xff0c;是做抖音直播数据对接的&#xff0c;有很多朋友给我点了star&#xff0c;也有朋友fork了这个仓库&#xff0c;最近接收到一个pull request的请求&#xff0c;他最直播结束的内容作了判断&#xff0c;我该如何在我本地校验它的代码并合并呢&#…

【Linux】03 GCC编译器的使用

一、编译过程 在使用gcc编译程序时&#xff0c;编译过程可以简要划分为4个阶段&#xff1a; 预处理、编译、汇编、链接 1.1 预处理&#xff08;preprocessing&#xff09; 这个阶段主要处理源文件中的#indef、#include和#define预处理命令&#xff1b; 这里主要是把一些include…

Linux 部署

jdk&tomcat安装 1.上传jdk、tomcat安装包 2.解压两个工具包 #解压tomcat tar -zxvf apache-tomcat-8.5.20.tar.gz #解压jdk tar -zxvf jdk-8u151-linux-x64.tar.gz 3.配置并且测试jdk安装 #配置环境变量 vim /etc/profile #java environment export JAVA_HOME/root/soft/…

【手撕C语言 第五集】分支和循环(下)

for循环 我们已经知道了while循环&#xff0c;但是我们为什么还要一个for循环呢&#xff1f; 首先来看看for循环的语法&#xff1a; 表达式1 表达式1为初始化部分&#xff0c;用于初始化循环变量的。 表达式2 表达式2为条件判断部分&#xff0c;用于判断循环时候终止。 表达式…

《仙剑4》、《仙剑6》双“空降”,这一次,网友的评论“真相了!”

“本以为《仙剑1》只是国产仙侠剧的起点&#xff0c;结果却没想到&#xff0c;它却真正意义上成为了巅峰。” 这一次&#xff0c;网友的评论“真相了&#xff01;” 先说《仙剑四》&#xff0c;这部据说耗资3.4亿的大制作&#xff0c;奈何笔者实在看不出它的“含金量”&#x…

2023 IoTDB Summit:湖南大唐先一科技有限公司主任架构师舒畅《IoTDB 在发电领域的应用实践》...

12 月 3 日&#xff0c;2023 IoTDB 用户大会在北京成功举行&#xff0c;收获强烈反响。本次峰会汇集了超 20 位大咖嘉宾带来工业互联网行业、技术、应用方向的精彩议题&#xff0c;多位学术泰斗、企业代表、开发者&#xff0c;深度分享了工业物联网时序数据库 IoTDB 的技术创新…

Deepin_Ubuntu_查看树形目录结构(tree)

Linux系统&#xff08;Deepin、Ubuntu&#xff09;中&#xff0c;可以使用tree命令来查看树形目录结构&#xff0c;下面是一些示例&#xff1a; 查看当前目录的树形结构&#xff1a; tree查看指定目录的树形结构&#xff0c;例如/etc/X11/fonts目录&#xff1a; tree /etc/X…

PSoc62™开发板之i2c通信

实验目的 使用模拟i2c接口读取温湿度气压模块BME280数据 实验准备 PSoc62™开发板温湿度气压模块BME280公母头杜邦线 板载资源 本次实验是通过模拟i2c时序的方式来进行通信&#xff0c;理论上可以有非常多的方式配置i2c引脚&#xff0c;不像硬件i2c那样芯片出厂引脚已经固…

C# Cad2016二次开发选择csv导入信息(七)

//选择csv导入信息 [CommandMethod("setdata")] //本程序在AutoCAD的快捷命令是"DLLLOAD" public void setdata() {Microsoft.Win32.OpenFileDialog dlg new Microsoft.Win32.OpenFileDialog();dlg.DefaultExt ".csv";// Display OpenFileDial…

1.php开发-个人博客项目文章功能显示数据库操作数据接收

&#xff08;2022-day12&#xff09; #知识点 1-php入门&#xff0c;语法&#xff0c;提交 2-mysql 3-HTMLcss ​ 演示案例 博客-文章阅读功能初步实现 实现功能&#xff1a; 前端文章导航&#xff0c;点入内容显示&#xff0c;更改ID显示不同内容 实现步骤&#xff1…

idea 连接docker

idea 连接docker步骤 services --> docker connection --->tcp://192.168.21.128:2375 修改docker配置 vi /usr/lib/systemd/system/docker.service 在ExecStart/usr/bin/dockerd -H fd:// --containerd/run/containerd/containerd.sock 后面加上 -H tcp://0.0.0.0:23…

matlab多元线性回归

1.matlab多元回归示例如下&#xff1a; 解决问题&#xff1a;油价预测 方法&#xff1a;多元线性回归 实现&#xff1a;matlab regress()函数 技巧&#xff1a;通过增加X1^2&#xff0c;X2^2&#xff0c;或者X1*X2等构造的特征项&#xff0c;可以提高回归模型的拟合准确度&…

CMake 实战构建TcpServer项目 静态库/动态库

爱编程的大丙CMake&#xff1a; 20. 举例 - 下_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV14s4y1g7Zj?p20&spm_id_frompageDriver&vd_sourcea934d7fc6f47698a29dac90a922ba5a3 hehedalinux:~/Linux/LinuxServerCpp$ ls Buffer.cpp Log.h Buffe…

RK3566 linux加入uvc app

SDK中external/uvc_app/目录提供了将板卡模拟成uvc camera的功能。 一、buildroot使能uvc_app 1、进入到buildroot目录 在sdk目录下执行以下命令&#xff1a; cd buildroot 2、选择defconfig 执行命令&#xff1a; source build/envsetup.sh 输入数字然后回车选择板卡&…

qt学习:实战 读取txt文件+定时器点名

目录 目标 步骤 头文件 配置ui界面 在.h里定义槽函数和字符串链表和定时器指针 在构造函数里读取txt文件并初始化定时器 开始按钮点击函数 开始定时器 停止按钮点击函数 关闭定时器 定时器槽函数 目标 两个按钮&#xff0c;一个开始点名&#xff0c;一个停止点名一个…

我在代码随想录|写代码Day10之双指针19. 删除链表的倒数第 N 个结点,面试题 02.07. 链表相交,142. 环形链表 II

博主介绍: 27dCnc 专题 : 数据结构帮助小白快速入门 &#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d; ☆*: .&#xff61;. o(≧▽≦)o .&#xff61;.:*☆ …

UVa1318/LA2797 Monster Trap

题目链接 本题是2003年ICPC亚洲区域赛会津(日本)赛区的H题 题意 给出一些线段障碍&#xff0c;你的任务是判断怪物能否逃到无穷远处。如下图所示&#xff0c;左图无法逃出&#xff0c;右图的可以逃出。 输入包含多组数据。每组数据第一行为整数n&#xff08;1≤n≤100&#xf…

[C#]C# winform部署yolov8目标检测的openvino模型

【官方框架地址】 https://github.com/ultralytics/ultralytics 【openvino介绍】 OpenVINO&#xff08;Open Visual Inference & Neural Network Optimization&#xff09;是由Intel推出的&#xff0c;用于加速深度学习模型推理的工具套件。它旨在提高计算机视觉和深度学…

【漏洞复现】Hikvision SPON IP网络对讲广播系统命令执行漏洞(CVE-2023-6895)

文章目录 前言声明一、系统简介二、漏洞描述三、影响版本四、漏洞复现五、修复建议 前言 Hikvision Intercom Broadcasting System是中国海康威视&#xff08;Hikvision&#xff09;公司的一个对讲广播系统。 声明 请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播…

C++PythonC# 三语言OpenCV从零开发(2):教程选择

文章目录 相关专栏前言视频教学和官方文档视频教程OpenCV 官方教程最终选择我的最终选择 相关专栏 C&Python&Csharp in OpenCV 前言 OpenCV 有官方的教程和简单的视频教程&#xff1a; OpenCV 官方教程 B站也有相关的视频教学 OpenCV4 C 快速入门视频30讲 - 系列合集 …