ClickHouse 如何实现数据一致性

本文涉及的产品
实时数仓Hologres,5000CU*H 100GB 3个月
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: 本文探讨了在 ClickHouse 中实现数据一致性的方法,主要关注 `ReplacingMergeTree` 引擎。该引擎允许更新已有数据,通过定期合并操作删除重复并保持最终一致性。然而,由于合并时间不可预测,单纯依赖此引擎无法确保实时一致性。为解决此问题,文章提出了四种策略:1)手动触发合并,但不建议频繁使用;2)使用 `FINAL` 查询,但在查询时合并数据,效率较低;3)通过标记和 `GroupBy` 查询实现一致性;4)在允许一定偏差的情况下,直接使用 `ReplacingMergeTree` 保持最终一致性。在实践中,推荐结合标记列和 `GroupBy` 以保证数据一致性。

@[toc]

前言:在大数据中,基本上所有组件都要求做到数据的一致性,因为大多数环境都是分布式的情况,如果数据无法做到一致,最终在进行分析计算时,导致指标出现问题,影响业务。

本篇文章将探讨在 ClickHouse 中实现数据一致性的几种方式。

ReplacingMegreTree 引擎

在 ClickHouse 中,推荐使用 ReplacingMegreTree 作为确保数据一致性的引擎。

与传统的 MergeTree 引擎相比,ReplacingMergeTree 允许更新已有的数据,而不是简单地追加新数据。当有新数据插入到表中时,它会删除排序键值相同的重复项,替换旧数据。

当有新数据替换旧数据时,旧数据并不会被立即删除,而是被标记为过时的版本。它定期会触发数据块合并操作,此时才会将历史数据进行删除,只保留新版本数据,优化性能、减少存储空间,做到最终一致性。

只有在触发合并操作后,数据才会发生真正的替换操作,在这之前,你查询到的都是历史版本数据。

数据一致性实现方式

1.ReplacingMegreTree 引擎

如果我们仅仅依赖 ReplacingMegreTree 引擎是无法做到数据一致性的,虽然它可以做到最终一致性,但是在未进行数据合并前,它可能存在重复数据。

而且官方说明,合并时间是无法预测的,也就是说我们并不知道它具体什么时候会发生合并操作。

那么为什么数据的合并时间无法预测呢?

  • 数据量和数据分布不确定性:合并操作的时间受到数据量和数据分布的影响。如果数据量较大,合并操作可能需要花费更长的时间。此外,数据的分布情况也会影响合并操作的时间,例如数据块的大小和数量、数据块之间的差异等。

  • 系统负载和资源竞争:合并操作需要消耗系统资源,包括 CPU、内存、磁盘等。当系统处于高负载状态时,合并操作可能需要等待资源空闲才能执行,这会导致合并操作的时间不确定。

2.ReplacingMegreTree 引擎 + 手动合并

虽然我们无法预知 ReplacingMegreTree 合并的具体时间,但是我们可以提前触发手动合并。

在新数据写入后,通过如下语句,主动执行合并:

OPTIMIZE TABLE table_name FINAL;

# 完成语法
OPTIMIZE TABLE [db.]name [ON CLUSTER cluster] [PARTITION partition | PARTITION ID 'partition_id'] [FINAL] [DEDUPLICATE [BY expression]]

但是手动合并会付出很大的代价,官方建议不要使用 OPTIMIZE TABLE ... FINAL,因为它用于管理(测试),而不是日常操作。

原因是,使用该查询时,它会尝试将指定表计划外的数据部分合并到一个原来的数据部分中。在此过程中,ClickHouse 读取所有数据部分,解压缩、合并、压缩为单个部分,然后重写回对象存储,造成巨大的 CPU 和 IO 消耗。这一优化会重写单个部分,即使它们已经合并为单个部分,所以代价很大,日常避免使用。

3.ReplacingMegreTree 引擎 + FINAL 查询

在进行查询时,添加 FINAL 后缀,如下所示:

SELECT * FROM test FINAL;

当指定 FINAL 时,ClickHouse 会在返回结果之前完全合并数据。但它限制引擎,只适用于从 ReplacingMergeTree、SummingMergeTree、AggregatingMergeTree、CollapsingMergeTree 和 VersionedCollapsingMergeTree 表中选择数据。

FINAL 查询在 v20.5.2.7 之前是单线程操作,十分缓慢,在这之后的版本都是并行执行的,默认采用 16 线程数,如果机器不能满足这么多线程,则默认使用当前机器最大线程数。我们也可以指定线程数运行该语句:

SELECT * FROM test FINAL settings max_threads = n;

当然,它会比日常的查询慢很多,主要原因有:

  • 数据是在查询执行期间才进行合并的。

  • 除了查询中指定的列之外,还会读取主键列的数据。

  • 需要额外的计算和内存资源,因为只有在查询时才会进行合并,所有操作都是在内存中进行的。你虽然可以在查询中使用 FINAL 获得最终所需的结果,但是要注意资源的消耗。

4.ReplacingMegreTree 引擎 + 标记 + GroupBy

在创建表时,我们可以增加一列作为标记,记录该值是否被删除或更新。通过两个字段来完成这一操作:

  • 标记列:使用特殊值标识该行数据是否被删除,例如:0 表示存在,1 表示过期。

  • 时间戳:标记列只能确保数据是否被删除,并不能标识是否发生过更新操作。所以我们可以借助数据中本身存在的时间戳来选择最新的数据,从而避免重复数据,做到数据一致性。

在日常开发中推荐使用这种方式来保证数据的一致性。

实现案例

假设有一个表存储用户的信息,包括用户ID、用户名、邮箱和标记列表示是否被删除。

首先,创建表

CREATE TABLE users (
    id UInt32,
    name String,
    email String,
    is_deleted UInt8,
    event_time DateTime
) ENGINE = ReplacingMergeTree(event_time)
ORDER BY (id, event_time);

在这个表中,我使用了 ReplacingMergeTree 引擎,并指定了 event_time 字段作为排序键,以确保数据的时间顺序。is_deleted 字段表示该行数据是否被删除,0 表示存在,1 表示被删除。

接下来,插入一些测试数据:

INSERT INTO users VALUES
    (1, 'Alice', 'alice@example.com', 0, '2024-04-25 08:00:00'),
    (2, 'Bob', 'bob@example.com', 0, '2024-04-25 09:00:00'),
    (3, 'Charlie', 'charlie@example.com', 0, '2024-04-25 10:00:00');

进行查询:

SELECT * FROM users;

image.png

现在,来模拟更新和删除操作的增量写入,假设用户 Bob 的邮箱地址被更新,用户 Charlie 被删除:

-- 更新 Bob 的邮箱地址
INSERT INTO users VALUES
    (2, 'Bob', 'new_bob@example.com', 0, '2024-04-25 11:00:00');

-- 删除 Charlie
INSERT INTO users VALUES
    (3, 'Charlie', '', 1, '2024-04-25 12:00:00');

增量写入后,表中的数据如下所示:

image.png

可以看到,这种情况就出现了重复数据。

但是,我们现在可以使用标记以及 Group By 语句查询每个用户的最新信息,手动过滤失效信息:

SELECT 
    id,
    argMax(name,event_time) name,
    argMax(email,event_time) email,
    argMax(is_deleted,event_time) is_deleted,
    max(event_time) max_event_time
FROM 
    users
GROUP BY 
    id
HAVING
    is_deleted = 0;

按照用户 ID 进行分组,按 event_time 字段当前的最大值,取对应行所有字段的数据。

这里需要注意的是,最后取 event_time 最大值时,重命名字段必须与之前不同,否则会报错。

例如 max(event_time) event_time 这种写法是错误的,因为该列已经被其它聚合函数 argMax 引用了。

在 ClickHouse 中,可以使用 argMax 函数来获取满足指定条件的某个字段的最大值所在行对应的另一个字段的值。argMax 函数通常与 GROUP BY 结合使用,以便在每个分组中找到满足条件的最大值对应的其他字段的值。

运行结果如下:

image.png

可以看到,我们成功的过滤掉了失效数据(用户 Bob 的邮箱地址被更新,用户 Charlie 被删除)

5.允许偏差

当我们在对某个指标进行计算时,并不关心该指标最终特别准确的值,或者说允许偏差一点,重复的数据量并不大,不会对总体造成影响

那么这种情况我们就不需要去确保该份数据的一致性,只需要确保最终一致性即可,此时选用 ReplacingMegreTree 引擎作为数据去重方案即可,不用大费周章的去花时间设计。

相关文章
ClickHouse数据一致性
ClickHouse数据一致性
269 0
|
6月前
|
存储 关系型数据库 数据库
【DDIA笔记】【ch2】 数据模型和查询语言 -- 多对一和多对多
【6月更文挑战第7天】该文探讨数据模型,比较了“多对一”和“多对多”关系。通过使用ID而不是纯文本(如region_id代替"Greater Seattle Area"),可以实现统一、避免歧义、简化修改、支持本地化及优化搜索。在数据库设计中,需权衡冗余和范式。文档型数据库适合一对多但处理多对多复杂,若无Join,需应用程序处理。关系型数据库则通过外键和JOIN处理这些关系。文章还提及文档模型与70年代层次模型的相似性,层次模型以树形结构限制了多对多关系处理。为克服层次模型局限,发展出了关系模型和网状模型。
60 6
|
6月前
|
XML NoSQL 数据库
【DDIA笔记】【ch2】 数据模型和查询语言 -- 概念 + 数据模型
【6月更文挑战第5天】本文探讨了数据模型的分析,关注点包括数据元素、关系及不同类型的模型(关系、文档、图)与Schema模式。查询语言的考量涉及与数据模型的关联及声明式与命令式编程。数据模型从应用开发者到硬件工程师的各抽象层次中起着简化复杂性的关键作用,理想模型应具备简洁直观和可组合性。
42 2
|
6月前
|
SQL 人工智能 关系型数据库
【DDIA笔记】【ch2】 数据模型和查询语言 -- 文档模型中Schema的灵活性
【6月更文挑战第8天】网状模型是层次模型的扩展,允许节点有多重父节点,但导航复杂,需要预知数据库结构。关系模型将数据组织为元组和关系,强调声明式查询,解耦查询语句与执行路径,简化了访问并通过查询优化器提高效率。文档型数据库适合树形结构数据,提供弱模式灵活性,但在Join支持和访问局部性上不如关系型。关系型数据库通过外键和Join处理多对多关系,适合高度关联数据。文档型数据库的模式灵活性体现在schema-on-read,写入时不校验,读取时解析,牺牲性能换取灵活性。适用于不同类型或结构变化的数据场景。
50 0
|
6月前
|
SQL JSON NoSQL
【DDIA笔记】【ch2】 数据模型和查询语言 -- 关系模型与文档模型
【6月更文挑战第6天】关系模型是主流数据库模型,以二维表形式展示数据,支持关系算子。分为事务型、分析型和混合型。尽管有其他模型挑战,如网状和层次模型,但关系模型仍占主导。然而,随着大数据增长和NoSQL的出现(如MongoDB、Redis),强调伸缩性、专业化查询和表达力,关系模型的局限性显现。面向对象编程与SQL的不匹配导致“阻抗不匹配”问题,ORM框架缓解但未完全解决。文档模型(如JSON)提供更自然的嵌套结构,适合表示复杂关系,具备模式灵活性和更好的数据局部性。
54 0
|
6月前
|
敏捷开发 存储 缓存
【DDIA笔记】【ch1】 可靠性、可扩展性和可维护性 -- 可维护性
【6月更文挑战第4天】本文探讨了Twitter面临的一次发推文引发的巨大写入压力问题,指出用户粉丝数分布是决定系统扩展性的关键因素。为解决此问题,Twitter采用混合策略,大部分用户推文扇出至粉丝主页时间线,而少数名人推文则单独处理。性能指标包括吞吐量、响应时间和延迟,其中高百分位响应时间对用户体验至关重要。应对负载的方法分为纵向和横向扩展,以及自动和手动调整。文章强调了可维护性的重要性,包括可操作性、简单性和可演化性,以减轻维护负担和适应变化。此外,良好设计应减少复杂性,提供预测性行为,并支持未来改动。
62 0
|
6月前
|
缓存 关系型数据库 数据库
【DDIA笔记】【ch1】 可靠性、可扩展性和可维护性 -- 可扩展性
【6月更文挑战第3天】可扩展性关乎系统应对负载增长的能力,但在产品初期过度设计可能导致失败。理解基本概念以应对可能的负载增长是必要的。衡量负载的关键指标包括日活、请求频率、数据库读写比例等。推特的扩展性挑战在于"扇出",即用户关注网络的广度。两种策略包括拉取(按需查询数据库)和推送(预计算feed流)。推送方法在推特案例中更为有效,因为它减少了高流量时的实时计算压力。
59 0
|
6月前
|
存储 消息中间件 缓存
【DDIA笔记】【ch1】 可靠性、可扩展性和可维护性 -- part1 可靠性
【6月更文挑战第2天】本书探讨现代数据系统,阐述其在信息社会中的关键作用,包括数据库、缓存、搜索引擎、流处理、批处理和消息队列等组成部分。随着技术发展,工具如Kafka、Spark和Redis等多功能组件使得系统设计更为复杂。面对可靠性、可扩展性和可维护性的挑战,书中强调了容错和韧性的重要性,区分了硬件故障、软件错误和人为错误,并提出了应对措施。可靠性关乎用户数据、企业声誉和生存,因此是系统设计的核心考量。
54 0
硬件开发笔记(十): 硬件开发基本流程,制作一个USB转RS232的模块(九):创建CH340G/MAX232封装库sop-16并关联原理图元器件
有了原理图,可以设计硬件PCB,在设计PCB之间还有一个协同优先动作,就是映射封装,原理图库的元器件我们是自己设计的。为了更好的表述封装设计过程,本文描述了CH340G和MAX232芯片封装创建(SOP-16),并将原理图的元器件关联引脚封装。
硬件开发笔记(十): 硬件开发基本流程,制作一个USB转RS232的模块(九):创建CH340G/MAX232封装库sop-16并关联原理图元器件