ClickHouse-数据一致性

数据一致性

查询 CK 手册发现,即便对数据一致性支持最好的 Mergetree,也只是保证最终一致性:

在这里插入图片描述

我们在使用 ReplacingMergeTree、SummingMergeTree 这类表引擎的时候,会出现短暂数据不一致的情况。

在某些对一致性非常敏感的场景,通常有以下几种解决方案。

准备测试表和数据

(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;

其中:

user_id 是数据去重更新的标识;

create_time 是版本号字段,每组数据中 create_time 最大的一行表示最新的数据;

deleted 是自定的一个标记位,比如 0 代表未删除,1 代表删除数据。

2)写入 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);

(3)修改前 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);

(4)统计总数

SELECT COUNT() FROM test_a;
┌──count()─┐
│ 10500000 │
└──────────┘

还未触发分区合并,所以还未去重。

手动 OPTIMIZE

在写入数据后,立刻执行 OPTIMIZE 强制触发新写入分区的合并动作

OPTIMIZE TABLE test_a FINAL;

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

// 执行后
┌──count()─┐
│ 10000000 │
└──────────┘

[ON CLUSTER cluster]:整个集群合并

[DEDUPLICATE [BY expression]]:根据那些字段进行合并,如果指定 DEDUPLICATE,那么完全相同的行(除非指定了 by-clause)将被删除重复(所有列都被比较),这仅对 MergeTree 引擎有意义。

[FINAL]:如果指定 FINAL,即使所有数据都已在一个部分中,也会执行优化。即使执行并发合并,也会强制合并。

如果 OPTIMIZE 由于任何原因没有执行合并,它不会通知客户端。要启用通知,请使用 optimize_throw_if_noop 设置。

通过 Group by 去重

通过sql语法来保证在数据尚未合并之前的数据一致性

(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;

函数说明:

  • argMax(field1,field2):按照 field2 的最大值取 field1 的值。

当更新数据时 , 会写入一行新的数据 , 例如上面语句中 , 通过查询最大的create_time 得到修改后的 score 字段值。

(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)插入重复数据,再次查询

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);

// 再次插入一条数据
INSERT INTO TABLE test_a(user_id,score,create_time)
VALUES(0,'AAAA',now())

┌──count()─┐
│ 10500001 │
└──────────┘

// 再次查询
SELECT *
FROM view_test_a
WHERE user_id = 0;

┌─user_id─┬─score─┬─deleted─┬───────────────ctime─┐
│       0 │ AAAA  │       02021-12-10 11:27:04 │
└─────────┴───────┴─────────┴─────────────────────┘

// 已有数据
SELECT *
FROM test_a
WHERE user_id = 0

┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐
│       0 │ AA    │       02021-12-10 11:26:13 │
└─────────┴───────┴─────────┴─────────────────────┘
┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐
│       0 │ AAAA  │       02021-12-10 11:27:04 │
└─────────┴───────┴─────────┴─────────────────────┘
┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐
│       0 │ AA    │       02021-12-10 11:14:59 │
└─────────┴───────┴─────────┴─────────────────────┘

(4)删除数据测试

// 再次插入一条标记为删除的数据
INSERT INTO TABLE test_a(user_id,score,deleted,create_time)
VALUES(0,'AAAA',1,now());

┌──count()─┐
│ 10500002 │
└──────────┘

// 再次查询,刚才那条数据看不到了
SELECT *
FROM view_test_a
WHERE user_id = 0;

这行数据并没有被真正的删除,而是被过滤掉了。在一些合适的场景下,可以结合表级别的 TTL 最终将物理数据删除。

通过 FINAL 查询

在查询语句后增加 FINAL 修饰符,这样在查询的过程中将会执行 Merge 的特殊逻辑(例如数据去重,预聚合等)。

但是这种方法在早期版本基本没有人使用,因为在增加 FINAL 之后,我们的查询将会变成一个单线程的执行过程,查询速度非常慢。

v20.5.2.7-stable 版本中, FINAL 查询**支持多线程执行,**并且可以通过 max_final_threads参数控制单个查询的线程数。但是目前读取 part 部分的动作依然是串行的。

FINAL 查询最终的性能和很多因素相关,列字段的大小、分区的数量等等都会影响到最终的查询时间,所以还要结合实际场景取舍。

参考链接: https://github.com/ClickHouse/ClickHouse/pull/10463

使用 hits_v1 表进行测试: 分别安装了 20.4.5.36 和 21.7.3.14 两个版本的 ClickHouse 进行对比。也可以到官网去测试

老版本测试

// 普通查询语句
select * from visits_v1 WHERE StartDate = '2014-03-17' limit 100;

// FINAL 查询
select * from visits_v1 FINAL WHERE StartDate = '2014-03-17' limit 100;

// 先前的并行查询变成了单线程。

新版本测试

普通语句查询

select * from visits_v1 WHERE StartDate = '2014-03-17' limit 100 settings max_threads = 2;

// 查看执行计划
EXPLAIN PIPELINE
SELECT *
FROM visits_v1
WHERE StartDate = '2014-03-17'
LIMIT 100
SETTINGS max_threads = 2

┌─explain─────────────────────────┐
│ (Expression)                    │
│ ExpressionTransform × 2         │
│   (SettingQuotaAndLimits)       │
│     (Limit)                     │
│     Limit 22                 │
│       (ReadFromMergeTree)       │
│       MergeTreeThread × 2 01 │
└─────────────────────────────────┘

明显将由 2 个线程并行读取 part 查询

FINAL 查询

select * from visits_v1 final WHERE StartDate = '2014-03-17' limit 100 settings max_final_threads = 2;

查询速度没有普通的查询快,但是相比之前已经有了一些提升,查看 FINAL 查询的执行计划:

EXPLAIN PIPELINE
SELECT *
FROM visits_v1
FINAL
WHERE StartDate = '2014-03-17'
LIMIT 100
SETTINGS max_final_threads = 2

┌─explain───────────────────────────────┐
│ (Expression)                          │
│ ExpressionTransform × 2               │
│   (SettingQuotaAndLimits)             │
│     (Limit)                           │
│     Limit 22                       │
│       (ReadFromMergeTree)             │
│       ExpressionTransform × 2         │
│         CollapsingSortedTransform × 2 │
│           Copy 12                  │
│             AddingSelector            │
│               ExpressionTransform     │
│                 MergeTree 01       │
└───────────────────────────────────────┘

从 CollapsingSortedTransform 这一步开始已经是多线程执行,但是读取分区数据的部分的动作还是串行。

总结

replacingMergeTree不能保证查询时没有重复,只能保证最终的一致性;

解决方法:

  1. 手动执行合并,生产环境不推荐;
  2. 通过sql实现去重:group by ==> 高级一点的话可以通过加标记字段;
  3. 使用final关键字,如果是20.5之前的版本final是单线程;20.5之后是多线程,但是读取分区数据的时候是串行;
  4. 重复一点无所谓的情况,不作处理

2和3都是牺牲效率来保证数据的一致性。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值