上一篇文章中我们说了 Fluss 与 Paimon 数据湖的三个相关问题:

  1. 如何查询 Paimon 数据湖中的数据?
  2. 如何查询 Fluss 和 Paimon 数据的“联合视图”?
  3. 如何只查询 Fluss 中的数据?

大家可以先去看这一篇文章,其中第二点 如何查询 Fluss 和 Paimon 数据的“联合视图” 中还遗留一个问题:在做数据查询的时候 Fluss 和 Paimon 数据湖是怎么保证数据一致性的,也就是事务的。还有第三点 如何只查询 Fluss 中的数据 这个问题也没有说,今天这篇文章主要解决上面这几个问题。

Fluss 和 Paimon 数据湖保证事务

当我们在 Fluss 里面建表的时候配置这个表的数据会同步到数据湖的时候,当我们往 Fluss 写入数据的时候,这份数据同时会写入你配置的数据湖里面,在写入过程中会记录数据入湖的Snapshot 和 Fluss Offset 的关联关系。

那么这样 Fluss 和 Paimon 是怎么保证事务的呢?其实当 Fluss 和 Paimon 联合读取数据时,读取的瞬间,Fluss 会:

  1. 锁定 Paimon 的快照:读取当前 Paimon 的最新快照数据。
  2. 获取 Fluss 的日志范围(end_offset):在读取时瞬间获取当前日志的结束位置。
  3. 限制日志读取范围:Fluss 仅读取 <paimon_snapshot_offset, end_offset> 范围内的日志数据。
  4. 与 Paimon 快照数据进行排序合并:将日志增量数据和 Paimon 快照数据合并,确保结果完整且无重复。

比如下面这样一个数据场景:

1. 数据场景:订单表的实时查询

表结构和现有数据

假设有一张订单表 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;
查询过程中并发写入的情况
  1. 初始读取时的数据:

    • 查询开始时,读取到了:

      Paimon 快照数据:
      订单 ID = 001,金额 = 100,状态 = Pending
      订单 ID = 002,金额 = 200,状态 = Confirmed
    • Fluss 日志:

      Offset 4: 订单 ID = 003,金额 = 300,状态 = Shipped
  2. 读取进行中,新的写入发生:

    • 在查询进行的过程中,新的数据被写入 Fluss:

      Offset 5: 订单 ID = 004,金额 = 400,状态 = Delivered
      Offset 6: 订单 ID = 005,金额 = 500,状态 = Pending
  3. 读取到的结果:

    • 由于未锁定 end_offset,读取的瞬间不是原子的,查询结果可能会因读取进度不同而出现多种结果:

      • 如果读取日志时,Offset = 5 被写入:

        SUM(amount) = 100 + 200 + 300 + 400 = 1000
      • 如果读取日志时,Offset = 6 被写入:

        SUM(amount) = 100 + 200 + 300 + 400 + 500 = 1500
  4. 最终结果的不确定性:

    • 查询的返回结果可能会随着写入进度不同而变化,导致结果不一致。
    • 原因:读取过程并非原子操作,新写入的数据动态影响了查询结果。

2. 不锁定 end_offset 的问题

问题:读取结果的不确定性
  • 由于读取 Fluss 日志和 Paimon 快照是分阶段完成的,如果查询过程中有新的写入发生:

    • 快照数据已经读取完成,但 日志数据正在读取时被新增写入
    • 这会导致查询结果依赖于写入的时间点和读取的进度,结果具有不确定性。
关键点:读取不是原子的
  • 读取过程分阶段:

    • 第一步读取 Paimon 的快照数据。
    • 第二步读取 Fluss 的日志数据。
  • 如果写入发生在读取日志数据的过程中,读取范围不固定,导致结果受动态写入影响。

3. 解决方法:锁定 end_offset

通过在查询开始时锁定 Paimon 快照Fluss 的 end_offset,可以确保读取过程是确定且一致的。

机制:锁定读取范围
  1. 锁定 Paimon 快照:

    • 确保快照数据在查询期间保持一致。
    • 例如,锁定快照 ID = 1,包含以下数据:

      订单 ID = 001,金额 = 100,状态 = Pending
      订单 ID = 002,金额 = 200,状态 = Confirmed
  2. 锁定 Fluss 的 end_offset

    • 查询开始时,记录当前日志的结束位置,例如:

      end_offset = 4
    • 限制日志读取范围为:

      Offset > 3 且 <= 4
    • 查询只读取以下日志数据:

      Offset 4: 订单 ID = 003,金额 = 300,状态 = Shipped

查询过程示例

  1. 锁定后读取范围明确:

    • Paimon 数据:

      订单 ID = 001,金额 = 100,状态 = Pending
      订单 ID = 002,金额 = 200,状态 = Confirmed
    • Fluss 日志数据:

      Offset 4: 订单 ID = 003,金额 = 300,状态 = Shipped
  2. 合并结果:

    • 查询结果为:

      SUM(amount) = 100 + 200 + 300 = 600
  3. 后续写入不影响查询:

    • 查询结束后,新写入的数据(Offset 5 和 Offset 6)不会影响当前查询。
    • 它们将在下一次查询中被读取。

4. 总结

不锁定 end_offset 的问题:
  • 查询过程中数据写入,导致读取范围动态变化,结果具有不确定性。
  • 原因:读取过程不是原子的,新数据在读取期间被引入。
锁定 end_offset 的好处:

确保读取范围一致:

  • 快照数据和日志数据范围明确,结果不受写入影响。

避免结果不确定性:

  • 查询结果固定,且与查询开始时的数据状态一致。

隔离性和一致性:

  • 查询过程锁定数据范围,确保事务隔离性。

通过锁定 end_offset,Fluss 实现了事务性的读取,解决了不确定性问题,确保查询结果的准确性和一致性。

5.Paimon 数据湖如何利用 MVCC 保证事务

Paimon 数据湖使用 多版本并发控制(MVCC) 来保证事务的一致性和隔离性。MVCC 的核心思想是:每次对数据的写入操作(新增、更新或删除)都会生成一个新的 快照(Snapshot),而查询操作总是基于某个固定的快照进行。以下通过一个具体数据的例子,说明 Paimon 如何利用 MVCC 保证事务。


MVCC 的核心概念
  • 快照(Snapshot): 每次写入都会生成一个快照,快照记录了写入时刻数据的完整状态。
  • 时间旅行查询: 用户可以基于指定的快照读取历史数据,不受后续写入影响。
  • 并发操作: 写入操作生成新的快照,不会修改旧快照的数据,保证并发读写的隔离性。

数据场景:订单表的读写操作

表结构

我们有一个订单表 order_table,包含以下字段:

订单 ID(order_id): STRING,主键
金额(amount): INT
状态(status): STRING

初始数据

假设当前 Paimon 数据湖中已有以下数据:

快照 ID = 1:
订单 ID = 001,金额 = 100,状态 = Pending
订单 ID = 002,金额 = 200,状态 = Confirmed

写入操作:新增和更新订单

新写入的操作

  1. 新增订单:

    订单 ID = 003,金额 = 300,状态 = Shipped
  2. 更新订单:

    订单 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

查询期间的写入

  • 查询执行时,Paimon 收到了新的写入请求,并生成了新的快照(快照 ID = 2)。

    • 新写入不会影响当前查询。
    • 当前查询仍然基于快照 ID = 1

查询结果

  • 查询操作读取的是快照 ID = 1 的数据:

    查询结果:
    订单 ID = 001,金额 = 100,状态 = Pending
    订单 ID = 002,金额 = 200,状态 = Confirmed

写入后的查询

  • 新的查询如果基于快照 ID = 2,则会读取最新的数据状态:

    查询结果:
    订单 ID = 001,金额 = 150,状态 = Confirmed
    订单 ID = 002,金额 = 200,状态 = Confirmed
    订单 ID = 003,金额 = 300,状态 = Shipped

MVCC 的优势

并发读写隔离

  • 读取固定快照

    • 查询基于某个快照,保证数据的一致性和隔离性。
    • 查询不受写入操作的影响。
  • 写入生成新快照

    • 写入操作不会修改现有快照的数据,而是生成新的快照,保证并发写入的隔离性。

时间旅行

  • 用户可以基于指定快照回溯历史数据:

    SELECT * FROM order_table$snapshots WHERE snapshot_id = 1;

    返回快照

    ID = 1

    的数据状态。

事务保证

  • 所有写入都是原子的,只有在快照生成成功后,新的数据状态才会对外可见。

总结

通过 MVCC,Paimon 数据湖实现了以下事务特性:

  1. 一致性:查询总是基于固定快照,保证数据状态一致。
  2. 隔离性:写入操作生成新快照,不会影响并发查询。
  3. 持久性:每个快照都是持久化存储,支持历史数据的回溯。

Paimon 的 MVCC 机制使其在高并发的读写场景下,能够保证事务性和高效性,同时为实时数据分析提供强有力的支持。

如何只查询 Fluss 中的数据

其实无论你建表的时候 加不加 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、大数据、大模型等相关内容的面试题,欢迎大家来取。关注微信公众号 大圣数据星球 带你搞定数据开发不迷路。


十点以后就睡觉了
1 声望3 粉丝