Clickhouse-数据一致性
[toc]
# 数据一致性
查询 CK 手册发现,即便对数据一致性支持最好的 Mergetree,也只是保证**最终一致性
**。
我们在使用 ReplacingMergeTree、SummingMergeTree 这类表引擎的时候,会出现短暂数据不一致的情况。
在某些对一致性非常敏感的场景,通常有以下几种解决方案。
# 准备测试表和数据
- 创建表
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;
2
3
4
5
6
7
其中:
user_id 是数据去重更新的标识;
create_time 是版本号字段,每组数据中 create_time 最大的一行表示最新的数据;
deleted 是自定的一个标记位,比如 0 代表未删除,1 代表删除数据。
- 写入 1000 万 测试数据
INSERT INTO TABLE 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);
2
3
4
5
- 修改前 50 万 行数据,修改内容包括 name 字段和 create_time 版本号字段
INSERT INTO TABLE 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);
2
3
4
5
6
- 统计总数
select count() from test_a;
SELECT count()
FROM test_a
Query id: e4403850-108d-43a5-a6ce-47fd7e93b89e
┌──count()─┐
│ 10500000 │
└──────────┘
2
3
4
5
6
7
8
9
10
还未触发分区合并,所以还未去重。
# 方法一:通过手动 OPTIMIZE 保证数据一致性
在写入数据后,立刻执行 OPTIMIZE 强制触发新写入分区的合并动作。
OPTIMIZE TABLE test_a FINAL;
语法:OPTIMIZE TABLE [db.]name [ON CLUSTER cluster] [PARTITION partition |
PARTITION ID 'partition_id'] [FINAL] [DEDUPLICATE [BY expression]]
2
3
4
# 方法二:通过 Group by 去重
- 执行去重的查询
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 ;
2
3
4
5
6
7
8
函数说明:
argMax(field1,field2):按照 field2 的最大值取 field1 的值。
当我们更新数据时,会写入一行新的数据,例如上面语句中,通过查询最大的create_time 得到修改后的 score 字段值
- 创建视图,方便测试
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;
2
3
4
5
6
7
8
9
- 插入重复数据,再次查询
#再次插入一条数据
INSERT INTO TABLE test_a(user_id,score,create_time) VALUES(0,'AAAA',now())
#再次查询
SELECT *
FROM view_test_a
WHERE user_id = 0;
2
3
4
5
6
7
- 删除数据测试
INSERT INTO TABLE test_a(user_id,score,deleted,create_time) VALUES(0,'AAAA',1,now());
# 再次查询发现数据查询不到了
SELECT * FROM view_test_a WHERE user_id = 0;
2
3
4
这行数据并没有被真正的删除,而是被过滤掉了。在一些合适的场景下,可以结合表级别的 TTL 最终将物理数据删除。
# 方法三:通过 FINAL 查询
在查询语句后增加 FINAL 修饰符,这样在查询的过程中将会执行 Merge 的特殊逻辑(例如数据去重,预聚合等)。
但是这种方法在早期版本基本没有人使用,因为在增加 FINAL 之后,我们的查询将会变成一个单线程的执行过程,查询速度非常慢。
在 v20.5.2.7-stable 版本中,FINAL 查询支持多线程执行,并且可以通过 max_final_threads 参数控制单个查询的线程数。但是目前读取 part 部分的动作依然是串行的。
FINAL 查询最终的性能和很多因素相关,列字段的大小、分区的数量等等都会影响到最终的查询时间,所以还要结合实际场景取舍。
参考链接:https://github.com/ClickHouse/ClickHouse/pull/10463
查询:
SELECT * FROM test_a final WHERE user_id = 0 and deleted = 0 limit 100 settings max_threads = 2;
发现查询不到数据,说明查询成功。