ClickHouse-数据一致性
在生产环境中,数据一致性的重要性,不论如何强调都不过分。而 ClickHouse 在进行数据变更时,都会产生一个临时分区,而不会更改原始数据文件,对数据文件的修改操作会要等到数据合并时才进行。所以 ClickHouse 只能保证数据的最终一致性,而不能保证强一致性。很可能数据变更后,程序通过 ClickHouse 查到之前的错误数据。因此使用 ClickHouse ,要尽量避免数据的增删改这类数据变更操作。但是实际使用时,又不可避免的要使用数据变更操作。这时就需要有一套策略来全面处理数据一致性问题。
首先,对于分布式表,最好的办法是尽量避免使用。如果非要使用分布式表,一定要打开internal_replication。每个分片一定要配置多副本机制,使用副本机制来保证副本之间的数据一致性。
一般来说,分布式表会带来非常多的问题。往分布式表中导入数据时,数据是异步写入到不同的分片当中的,这样数据写入过程中就不可避免的有先有后。在最后一个分片的数据写入完成之前,不可避免的就会产生数据一致性的问题。
另外,对于分布式表,如果在数据写入时,这个分片的服务宕机了,那么插入的数据就有可能会丢失。ClickHouse 的做法是将这个数据分片转移到 broken 子目录中,并不再使用这个数据分片。也就是说,这时,ClickHouse 这一次的数据写入操作 ius 丢失了。造成的结果就是有可能就是一次 update 操作要更新 1000 条数据,但是最终却只更新了 900 条。
然后,对于本地的数据库,也一定要注意多副本造成的数据一致性问题。ClickHouse 中,即使是提供了去重功能的 ReplacingMergeTree,它只能保证在数据合并时会去重,只能保证数据的最终一致性,而不能保证强一致性(具体可参考官网说明:https://clickhouse.com/docs/zh/engines/table-engines/mergetree-family/replacingmergetree/)。

我们在使用 ReplacingMergeTree、SummingMergeTree 这类表引擎的时候,会出现短暂数据不一致的情况。
在某些对一致性非常敏感的场景,通常有以下几种解决方案。
1.准备测试表和数据
1.1创建表
CREATE TABLE test_a ( `user_id` UInt64, `score` String, `deleted` UInt8 DEFAULT 0, `create_time` DateTime DEFAULT toDateTime(0) ) ENGINE = ReplacingMergeTree(create_time) ORDER BY user_id Query id: 04dd344f-62ce-4434-809c-377d5e224870 Ok. 0 rows in set. Elapsed: 0.039 sec.
其中:
user_id 是数据去重更新的标识;
create_time 是版本号字段,每组数据中 create_time 最大的一行表示最新的数据;
deleted 是自定的一个标记位,比如 0 代表未删除,1 代表删除数据。
1.2写入1000万测试数据
INSERT INTO test_a (user_id, score) WITH ( SELECT ['A', 'B', 'C', 'D', 'E', 'F', 'G'] ) AS dict SELECT number AS user_id, dict[(number % 7) + 1] FROM numbers(10000000) Query id: 40b4ea66-5856-4ebb-887f-748a1d25c666 Ok. 0 rows in set. Elapsed: 1.184 sec. Processed 10.49 million rows, 83.88 MB (8.86 million rows/s., 70.86 MB/s.)
1.3修改前 50 万 行数据,修改内容包括 name 字段和 create_time 版本号字段
INSERT INTO test_a (user_id, score, create_time) WITH ( SELECT ['AA', 'BB', 'CC', 'DD', 'EE', 'FF', 'GG'] ) AS dict SELECT number AS user_id, dict[(number % 7) + 1], now() AS create_time FROM numbers(500000) Query id: e0668a9b-e8dc-43a0-a1d7-4afb2a337269 Ok. 0 rows in set. Elapsed: 0.078 sec. Processed 500.00 thousand rows, 4.00 MB (6.42 million rows/s., 51.37 MB/s.)
1.4统计总数
SELECT COUNT() FROM test_a Query id: 64df094e-2c7b-4b69-83da-e16f04627ebc ┌──count()─┐ │ 10500000 │ └──────────┘ 1 rows in set. Elapsed: 0.007 sec.
还未触发分区合并,所以还未去重。
2.手动optimize
在写入数据后,立刻执行 OPTIMIZE 强制触发新写入分区的合并动作。
superset-BI :) OPTIMIZE TABLE test_a FINAL; OPTIMIZE TABLE test_a FINAL Query id: 76479daa-1d23-4ac4-9938-6b1cf41c2dbd Ok. 0 rows in set. Elapsed: 1.197 sec. superset-BI :) SELECT COUNT() FROM test_a; SELECT COUNT() FROM test_a Query id: b81f2d27-3af6-4fe0-9751-010cc1ba6c71 ┌──count()─┐ │ 10000000 │ └──────────┘ 1 rows in set. Elapsed: 0.002 sec.
3.通过Group by 去重
3.1执行去重的查询
SELECT user_id, argMax(score, create_time) AS score, argMax(deleted, create_time) AS deleted, max(create_time) AS ctime FROM test_a GROUP BY user_id HAVING deleted = 0 Query id: 3c3e698a-9dd7-48c3-8957-3cacd8357b0e Showed first 10000. 10000000 rows in set. Elapsed: 3.014 sec. Processed 10.00 million rows, 230.50 MB (3.32 million rows/s., 76.47 MB/s.)
函数说明:
argMax(field1,field2):按照 field2 的最大值取 field1 的值。
当我们更新数据时,会写入一行新的数据,例如上面语句中,通过查询最大的create_time 得到修改后的 score 字段值。
3.2创建视图,方便测试
CREATE VIEW view_test_a AS SELECT user_id, argMax(score, create_time) AS score, argMax(deleted, create_time) AS deleted, max(create_time) AS ctime FROM test_a GROUP BY user_id HAVING deleted = 0;
3.3插入重复数据,再次查询
#再次插入一条数据 INSERT INTO test_a (user_id, score, create_time) FORMAT Values Query id: 64630a6a-1339-4a9f-a5d8-18f6c3f964df Ok. 1 rows in set. Elapsed: 0.010 sec.
#再次查询 SELECT * FROM test_a WHERE user_id = 0 Query id: 12c40b30-c9dd-4db5-aa90-38f65f5cc213 ┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐ │ 0 │ AA │ 0 │ 2022-05-09 18:23:09 │ └─────────┴───────┴─────────┴─────────────────────┘ ┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐ │ 0 │ AAAA │ 0 │ 2022-05-09 21:44:27 │ └─────────┴───────┴─────────┴─────────────────────┘ ┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐ │ 0 │ AA │ 0 │ 2022-05-09 21:41:34 │ └─────────┴───────┴─────────┴─────────────────────┘ 3 rows in set. Elapsed: 0.004 sec. Processed 16.39 thousand rows, 393.24 KB (4.54 million rows/s., 108.98 MB/s.)
从视图查,只能看到最新一条数据
SELECT * FROM view_test_a WHERE user_id = 0 Query id: 35d437b9-5d09-4ab3-8c7f-30d1a3a67fd5 ┌─user_id─┬─score─┬─deleted─┬───────────────ctime─┐ │ 0 │ AAAA │ 0 │ 2022-05-09 21:44:27 │ └─────────┴───────┴─────────┴─────────────────────┘ 1 rows in set. Elapsed: 0.018 sec. Processed 16.39 thousand rows, 393.24 KB (894.47 thousand rows/s., 21.47 MB/s.)
3.4删除数据测试
#再次插入一条标记为删除的数据 INSERT INTO test_a (user_id, score, deleted, create_time) FORMAT Values Query id: 96130c41-b29e-4d3e-b9ea-058d91afe7b9 Ok. 1 rows in set. Elapsed: 0.003 sec. #再次查询 SELECT * FROM test_a WHERE user_id = 0 Query id: e1062fc6-07c1-4ef6-9555-2e051e707c87 ┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐ │ 0 │ AAAA │ 0 │ 2022-05-09 21:44:27 │ └─────────┴───────┴─────────┴─────────────────────┘ ┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐ │ 0 │ AAAA │ 1 │ 2022-05-09 21:47:28 │ └─────────┴───────┴─────────┴─────────────────────┘ ┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐ │ 0 │ AA │ 0 │ 2022-05-09 18:23:09 │ └─────────┴───────┴─────────┴─────────────────────┘ ┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐ │ 0 │ AA │ 0 │ 2022-05-09 21:41:34 │ └─────────┴───────┴─────────┴─────────────────────┘ 4 rows in set. Elapsed: 0.004 sec. Processed 16.39 thousand rows, 393.27 KB (3.82 million rows/s., 91.65 MB/s.)
查视图,刚才那条数据看不到了
SELECT * FROM view_test_a WHERE user_id = 0 Query id: 5026ccbd-eae1-45c2-abb2-6ac214ed48d1 Ok. 0 rows in set. Elapsed: 0.006 sec. Processed 16.39 thousand rows, 393.27 KB (2.87 million rows/s., 68.94 MB/s.)
这行数据并没有被真正的删除,而是被过滤掉了。在一些合适的场景下,可以结合 表级别的 TTL 最终将物理数据删除。
4.通过final查询
在查询语句后增加 FINAL 修饰符,这样在查询的过程中将会执行 Merge 的特殊逻辑(例如数据去重,预聚合等)。
但是这种方法在早期版本基本没有人使用,因为在增加 FINAL 之后,我们的查询将会变成一个单线程的执行过程,查询速度非常慢。
在 v20.5.2.7-stable 版本中,FINAL 查询支持多线程执行,并且可以通过 max_final_threads参数控制单个查询的线程数。但是目前读取 part 部分的动作依然是串行的。
FINAL 查询最终的性能和很多因素相关,列字段的大小、分区的数量等等都会影响到最终的查询时间,所以还要结合实际场景取舍。
使用 hits_v1 表进行测试:
4.1普通语句查询
SELECT * FROM datasets.visits_v1 WHERE StartDate = '2014-03-17' LIMIT 100 SETTINGS max_threads = 2 100 rows in set. Elapsed: 0.073 sec. Processed 13.18 thousand rows, 21.79 MB (181.18 thousand rows/s., 299.57 MB/s.)
查看执行计划
EXPLAIN PIPELINE SELECT * FROM datasets.visits_v1 WHERE StartDate = '2014-03-17' LIMIT 100 SETTINGS max_threads = 2 Query id: 83200393-6f6e-4df9-9c88-8744084b7bd8 ┌─explain─────────────────────────┐ │ (Expression) │ │ ExpressionTransform × 2 │ │ (SettingQuotaAndLimits) │ │ (Limit) │ │ Limit 2 → 2 │ │ (ReadFromMergeTree) │ │ MergeTreeThread × 2 0 → 1 │ └─────────────────────────────────┘ 7 rows in set. Elapsed: 0.012 sec.
明显将由2个线程并行读取part查询
4.2final查询
SELECT * FROM datasets.visits_v1 final WHERE StartDate = '2014-03-17' LIMIT 100 SETTINGS max_threads = 2; 100 rows in set. Elapsed: 0.548 sec. Processed 152.74 thousand rows, 239.19 MB (278.91 thousand rows/s., 436.76 MB/s.)
查询速度没有普通的查询快,但是相比之前已经有了一些提升,查看 FINAL 查询的执行计划:
EXPLAIN PIPELINE SELECT * FROM datasets.visits_v1 FINAL WHERE StartDate = '2014-03-17' LIMIT 100 SETTINGS max_threads = 2 Query id: ba101258-083a-417e-b77f-ac71af6fdb73 ┌─explain──────────────────────────────────┐ │ (Expression) │ │ ExpressionTransform × 2 │ │ (Limit) │ │ Limit 2 → 2 │ │ (Filter) │ │ FilterTransform × 2 │ │ (SettingQuotaAndLimits) │ │ (ReadFromMergeTree) │ │ ExpressionTransform × 2 │ │ CollapsingSortedTransform × 2 │ │ Copy 1 → 2 │ │ AddingSelector │ │ ExpressionTransform │ │ MergeTreeInOrder 0 → 1 │ └──────────────────────────────────────────┘ 14 rows in set. Elapsed: 0.017 sec.
从 CollapsingSortedTransform 这一步开始已经是多线程执行,但是读取 part 部分的动作还是串行。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库