在数据仓库领域,离线数仓和实时数仓是常见的两种架构类型。离线数仓一般通过定时任务在特定时间点(通常是凌晨)将业务数据同步到数据仓库中。这种方式适用于对数据实时性要求不高,更侧重于历史数据分析和报告生成的场景。
然而,采用离线同步方式可能会引发业务数据与数据仓库数据不一致的问题。本文的目标是深入分析这些问题的根本原因,并提供一些建议来优化同步流程,以确保数据的一致性。
在大数据平台中,业务部门常常需要查看历史某一天的表数据。为了记录历史数据的变化,离线数仓常见的解决方案是拉链表和快照表。而由于拉链表的查询方式较为复杂不便直观的展现问题,因此在这里我选择使用快照表作为示例,以便更清晰地阐述离线数仓的数据一致性问题。
快照表是用来存储某个时间点的所有数据-通常粒度是天,相当于是对每天的业务数据做了一次快照,存储当天的全量数据;例如:快照表12号分区中的数据是从历史到11号的所有数据,13号分区中的数据是从历史到12号的所有数据,其他的以此类推,示例如下:
id | name | phone | gender | create_time | update_time |
---|---|---|---|---|---|
1 | jack | 111 | 男 | 2023-06-01 13:00:00 | 2023-06-01 13:00:00 |
2 | jason | 222 | 男 | 2023-06-01 13:00:00 | 2023-06-01 13:00:00 |
3 | tom | 333 | 男 | 2023-06-01 13:00:00 | 2023-06-01 13:00:00 |
id | name | phone | gender | create_time | update_time | dt[分区字段] |
---|---|---|---|---|---|---|
1 | jack | 111 | 男 | 2023-06-01 13:00:00 | 2023-06-01 13:00:00 | 2023-06-01 |
2 | jason | 222 | 男 | 2023-06-01 13:00:00 | 2023-06-01 13:00:00 | 2023-06-01 |
3 | tom | 333 | 男 | 2023-06-01 13:00:00 | 2023-06-01 13:00:00 | 2023-06-01 |
加粗为分区字段
id | name | phone | gender | create_time | update_time |
---|---|---|---|---|---|
1 | jack | 111 | 男 | 2023-06-01 13:00:00 | 2023-06-01 13:00:00 |
2 | jason | 222 | 男 | 2023-06-01 13:00:00 | 2023-06-01 13:00:00 |
3 | tom | 444 | 男 | 2023-06-01 13:00:00 | 2023-06-02 09:00:00 |
4 | tony | 555 | 男 | 2023-06-02 10:00:00 | 2023-06-02 10:00:00 |
加粗为更新/新增数据
id | name | phone | gender | create_time | update_time | dt[分区字段] |
---|---|---|---|---|---|---|
1 | jack | 111 | 男 | 2023-06-01 13:00:00 | 2023-06-01 13:00:00 | 2023-06-01 |
2 | jason | 222 | 男 | 2023-06-01 13:00:00 | 2023-06-01 13:00:00 | 2023-06-01 |
3 | tom | 333 | 男 | 2023-06-01 13:00:00 | 2023-06-01 13:00:00 | 2023-06-01 |
1 | jack | 111 | 男 | 2023-06-01 13:00:00 | 2023-06-01 13:00:00 | 2023-06-02 |
2 | jason | 222 | 男 | 2023-06-01 13:00:00 | 2023-06-01 13:00:00 | 2023-06-02 |
3 | tom | 444 | 男 | 2023-06-01 13:00:00 | 2023-06-02 09:00:00 | 2023-06-02 |
4 | tony | 555 | 男 | 2023-06-02 10:00:00 | 2023-06-02 10:00:00 | 2023-06-02 |
加粗为更新/新增数据
以上是快照表的表现形式,接下来我们看下具体实现
离线数仓(T+1)中关于快照表的实现方式有两种:全量同步和增量同步。
值得强调的是,这些同步任务的执行方式并不局限于特定的工具或框架,例如sqoop/spark;因此在本文中我们将使用SQL语句来表达数据处理过程。
需要注意的是这两种实现方法都有可能导致数据不一致的问题,下一节将对此进行详细讨论和解释。
# 2023-06-03凌晨执行的全量同步sql语句
INSERT INTO 数仓快照表 PARTITION (date='2023-06-02')
select * from 业务用户表 where update_time < '2023-06-03 00:00:00';
增量同步顾名思义是将业务数据用户表按天为粒度将增量数据与数仓快照表中的前一天数据进行join对比后放入到指定分区内,关于增量同步的实现不在本文赘述,对此感兴趣的读者可参考笔者的另一篇文章:数仓日常维护:剖析每日增量同步的内部机制
以上述快照表为例,可能引发一致性问题的情况是指在执行层的Spark或Sqoop任务启动和执行期间,业务数据库表的数据发生了变化,从而导致快照表与业务表的数据不一致。这种不一致性问题可能会对数据处理和分析产生负面影响,示例如下:
假设业务表在2023年6月2日新增了“Tony”修改了“Tom”手机号这两条数据。在凌晨定时任务启动后,引擎初始化及加载数据时,业务数据中的“Tony”发生了变更,其“update_time”字段也随之变化。然后,执行引擎再次通过“update_time”字段读取业务数据时,由于变更,它可能会错过“Tony”这条记录。这将导致数仓快照表中2023年6月2日分区的数据缺失“Tony”用户信息,造成了当天数据不一致的问题,过程如下:
上图采用全量同步方式,增量同步同样会有此问题
以上问题的本质是数据同步执行层在启动或数据加载过程中,由于业务数据库表数据的动态变化,特别是在数据加载期间或引擎启动期间发生的数据更新操作,导致了读取到的数据无法准确地反映业务表在特定时间点的状态。这样的数据变化可能会使得快照表在某些情况下缺少或错误地反映了业务表的最新状态,导致了数据不一致的问题。
同步任务在凌晨前启动,当时钟指向零点时,对需要同步的数据库表进行锁定,以防止其他更改操作干扰数据读取,确保数据一致性。
然而,这种方式存在明显弊端。首先,要求业务库支持锁操作,并且同步任务必须具备相应的锁权限。更重要的是,这种方式会对业务库产生较大影响,因此不推荐使用。值得一提的是,Flink-CDC 1.x版本的全量同步采用的就是使用了这种对表加锁的策略,不过该痛点已在2.x版本后改为增量快照读取机制从而解决了加锁问题
对此感兴趣的读者可参考笔者另一篇文章:深入解析 Flink CDC 增量快照读取机制
实时同步是一种有效解决数据一致性问题的方法,因其同步方式大多是采用binlog + checkpoint分布式快照的形式故不会存在漏读情况,但这可能需要对现有技术架构做出较大的改变。实时同步具体实现不在本文赘述,感兴趣的同学可以看笔者另一篇文章:Flink实时数仓同步:拉链表实战详解
此思路灵感来源于 Flink-CDC 2.x 的增量快照读取机制。这种修正方式相对简单,且不会对现有的离线数仓架构产生改变,仍然可以使用 Spark 或 其他执行引擎。对此感兴趣的读者可参考笔者另一篇文章:深入解析 Flink CDC 增量快照读取机制
具体思路如下:相较于之前的离线同步,新增了一个读取 binlog 消息修正的步骤。当同步任务读取完业务数据后,它会读取从零点到当前时间内的 binlog 日志。如果发现了 update 操作的日志,则判断该条日志中 after 数据的 update_time 是否属于当前快照表的时间范围。若属于,则将 after 数据补充到已读取的业务数据中。
这种方法能够有效解决离线数仓同步数据一致性的问题,而且不需要修改现有的离线数仓架构。因此,对于那些不想对原有技术架构做出变更的人来说,这种方法值得推荐。