上一篇文章中我们说了 Fluss 与 Paimon 数据湖的三个相关问题:
大家可以先去看这一篇文章,其中第二点 如何查询 Fluss 和 Paimon 数据的“联合视图” 中还遗留一个问题:在做数据查询的时候 Fluss 和 Paimon 数据湖是怎么保证数据一致性的,也就是事务的。还有第三点 如何只查询 Fluss 中的数据 这个问题也没有说,今天这篇文章主要解决上面这几个问题。
当我们在 Fluss 里面建表的时候配置这个表的数据会同步到数据湖的时候,当我们往 Fluss 写入数据的时候,这份数据同时会写入你配置的数据湖里面,在写入过程中会记录数据入湖的Snapshot 和 Fluss Offset 的关联关系。
那么这样 Fluss 和 Paimon 是怎么保证事务的呢?其实当 Fluss 和 Paimon 联合读取数据时,读取的瞬间,Fluss 会:
end_offset
):在读取时瞬间获取当前日志的结束位置。<paimon_snapshot_offset, end_offset>
范围内的日志数据。比如下面这样一个数据场景:
假设有一张订单表 order_table
,配置了 table.datalake.enabled=true
。当前数据如下:
Paimon 快照数据(paimon_snapshot_offset = 3
):
订单 ID = 001,金额 = 100,状态 = Pending 订单 ID = 002,金额 = 200,状态 = Confirmed
Fluss 日志数据(Offset > 3):
Offset 4: 订单 ID = 003,金额 = 300,状态 = Shipped
用户发起查询:
SELECT SUM(amount) FROM order_table;
初始读取时的数据:
查询开始时,读取到了:
Paimon 快照数据: 订单 ID = 001,金额 = 100,状态 = Pending 订单 ID = 002,金额 = 200,状态 = Confirmed
Fluss 日志:
Offset 4: 订单 ID = 003,金额 = 300,状态 = Shipped
读取进行中,新的写入发生:
在查询进行的过程中,新的数据被写入 Fluss:
Offset 5: 订单 ID = 004,金额 = 400,状态 = Delivered Offset 6: 订单 ID = 005,金额 = 500,状态 = Pending
读取到的结果:
由于未锁定 end_offset,读取的瞬间不是原子的,查询结果可能会因读取进度不同而出现多种结果:
如果读取日志时,Offset = 5 被写入:
SUM(amount) = 100 + 200 + 300 + 400 = 1000
如果读取日志时,Offset = 6 被写入:
SUM(amount) = 100 + 200 + 300 + 400 + 500 = 1500
最终结果的不确定性:
end_offset
的问题end_offset
通过在查询开始时锁定 Paimon 快照 和 Fluss 的 end_offset
,可以确保读取过程是确定且一致的。
锁定 Paimon 快照:
确保快照数据在查询期间保持一致。
例如,锁定快照 ID = 1,包含以下数据:
订单 ID = 001,金额 = 100,状态 = Pending 订单 ID = 002,金额 = 200,状态 = Confirmed
锁定 Fluss 的 end_offset
:
查询开始时,记录当前日志的结束位置,例如:
end_offset = 4
限制日志读取范围为:
Offset > 3 且 <= 4
查询只读取以下日志数据:
Offset 4: 订单 ID = 003,金额 = 300,状态 = Shipped
锁定后读取范围明确:
Paimon 数据:
订单 ID = 001,金额 = 100,状态 = Pending 订单 ID = 002,金额 = 200,状态 = Confirmed
Fluss 日志数据:
Offset 4: 订单 ID = 003,金额 = 300,状态 = Shipped
合并结果:
查询结果为:
SUM(amount) = 100 + 200 + 300 = 600
后续写入不影响查询:
end_offset
的问题:end_offset
的好处:确保读取范围一致:
避免结果不确定性:
隔离性和一致性:
通过锁定 end_offset
,Fluss 实现了事务性的读取,解决了不确定性问题,确保查询结果的准确性和一致性。
Paimon 数据湖使用 多版本并发控制(MVCC) 来保证事务的一致性和隔离性。MVCC 的核心思想是:每次对数据的写入操作(新增、更新或删除)都会生成一个新的 快照(Snapshot),而查询操作总是基于某个固定的快照进行。以下通过一个具体数据的例子,说明 Paimon 如何利用 MVCC 保证事务。
表结构
我们有一个订单表 order_table
,包含以下字段:
订单 ID(order_id): STRING,主键 金额(amount): INT 状态(status): STRING
初始数据
假设当前 Paimon 数据湖中已有以下数据:
快照 ID = 1: 订单 ID = 001,金额 = 100,状态 = Pending 订单 ID = 002,金额 = 200,状态 = Confirmed
新写入的操作
新增订单:
订单 ID = 003,金额 = 300,状态 = Shipped
更新订单:
订单 ID = 001,金额 = 150,状态 = Confirmed
生成新快照
Paimon 会基于上述写入生成一个新的快照:
快照 ID = 2: 订单 ID = 001,金额 = 150,状态 = Confirmed (更新) 订单 ID = 002,金额 = 200,状态 = Confirmed 订单 ID = 003,金额 = 300,状态 = Shipped (新增)
查询开始前
查询基于快照
ID = 1,此时 Paimon 的数据状态为:
快照 ID = 1: 订单 ID = 001,金额 = 100,状态 = Pending 订单 ID = 002,金额 = 200,状态 = Confirmed
查询期间的写入
查询结果
查询操作读取的是快照 ID = 1 的数据:
查询结果: 订单 ID = 001,金额 = 100,状态 = Pending 订单 ID = 002,金额 = 200,状态 = Confirmed
写入后的查询
新的查询如果基于快照 ID = 2,则会读取最新的数据状态:
查询结果: 订单 ID = 001,金额 = 150,状态 = Confirmed 订单 ID = 002,金额 = 200,状态 = Confirmed 订单 ID = 003,金额 = 300,状态 = Shipped
并发读写隔离
时间旅行
用户可以基于指定快照回溯历史数据:
SELECT * FROM order_table$snapshots WHERE snapshot_id = 1;
返回快照
ID = 1
的数据状态。
事务保证
通过 MVCC,Paimon 数据湖实现了以下事务特性:
Paimon 的 MVCC 机制使其在高并发的读写场景下,能够保证事务性和高效性,同时为实时数据分析提供强有力的支持。
其实无论你建表的时候 加不加 table.datalake.enabled=true 这个配置,在我们往 Fluss 写入数据的时候,你所写入的全部数据都会写到 Fluss 本地存储,然后根据一定的策略 Fluss 会把数据存储到远程存储,这就是我们讲Fluss 架构那篇文章里面说的那个远程存储,这个存储和数据湖是没有任何关系的,只是Fluss 框架自己的操作,因为 Fluss 作为一个存储和分析的系统,它肯定会存储所有的数据的。
只是你加了 table.datalake.enabled=true 这个配置的时候,这个配置就相当于一个开关,当我们往 Fluss 写入数据的时候,这份数据就会通过 Fluss 的 compact service 服务把数据同步到数据湖的。
在我们查询的时候也会根据你建表的时候有没有加 table.datalake.enabled=true 这个配置,如果没有加的话,这个时候是只能根据主键查询的,查询的所有数据都是来自 Fluss 自己框架的数据。当你建表的时候加了上面那个配置的时候,你去查询全表的时候,数据是按照我们上篇文章说的那样查询的是Paimon 数据湖和 Fluss 的数据,至于查询的原理在这两篇内容里面已经详细阐述了。
这篇文章讲了 Fluss 和 Paiom 数据湖的事务和 Fluss 一些设计原理,能让大家对 Fluss 从设计理念到实现细节上面都有了一个全面的认识。我讲的这些知识点和细节小伙伴在官网或者其他地方都是看不到的,欢迎大家一起来讨论大数据技术,同时我也给大家整理了 2024 年 最新的大厂Java、大数据、大模型等相关内容的面试题,欢迎大家来取。关注微信公众号 大圣数据星球 带你搞定数据开发不迷路。