VersionedCollapsingMergeTree如何应对多客户端并发写入的逻辑错误?
VersionedCollapsingMergeTree并发更新问题及解决方案
根据ClickHouse文档《多线程实时更新》所述:
VersionedCollapsingMergeTree表在多客户端/多线程插入时实现去重十分便捷。
但它依赖**取消行(cancel row)**抵消历史行的机制,在并发场景下容易出现逻辑错误。比如有一张以id为主键的表,初始数据如下:
┌──id─┬─author──┬─views─┬─version─┐─sign─┐ │ 123 │ ricardo │ 150 │ 1 │ 1 │ └─────┴─────────┴───────┴─────────┴──────┘
当两个客户端同时对该主键写入更新数据时:
client1: ┌──id─┬─author──┬─views─┬─version─┐─sign─┐ │ 123 │ ricardo │ 150 │ 1 │ -1 │ │ 123 │ ricardo │ 200 │ 2 │ 1 │ └─────┴─────────┴───────┴────────────────┘ client2: ┌──id─┬─author──┬─views─┬─version─┐─sign─┐ │ 123 │ ricardo │ 150 │ 1 │ -1 │ │ 123 │ ricardo │ 250 │ 2 │ 1 │ └─────┴─────────┴───────┴────────────────┘
初始的sign=1行会被两次抵消,最终残留两个sign=1的version=2行,导致数据失真。以下是几种可行的解决办法:
1. 使用全局唯一递增版本号
不要让客户端自行生成version,依赖全局递增的版本源(如分布式ID服务、ClickHouse序列表):
- 创建序列表:
CREATE TABLE version_sequence (id UInt64, next_version UInt64) ENGINE = ReplacingMergeTree PRIMARY KEY id; INSERT INTO version_sequence VALUES (1, 1); - 更新流程:先通过
SELECT next_version FROM version_sequence WHERE id=1 FOR UPDATE获取当前版本,将版本号+1后更新回序列表,再用新生成的version创建取消行和新数据行,确保同一主键的版本号全局唯一且递增。
2. 加分布式锁控制并发
针对要更新的主键(如id=123),写入前先获取分布式锁,确保同一时间只有一个客户端能操作该主键:
- 可借助外部锁服务(如Redis的SETNX命令),或利用ClickHouse的
SELECT ... FOR UPDATE结合ReplicatedMergeTree特性实现行级锁,避免并发写入冲突。
3. 改用ReplacingMergeTree替代
若业务对实时性要求不高,可切换为ReplacingMergeTree,利用版本列自动替换旧数据:
- 创建表语句示例:
CREATE TABLE user_stats ( id UInt64, author String, views UInt64, version UInt64 ) ENGINE = ReplacingMergeTree(version) ORDER BY id; - 每次更新直接写入带更高
version的新行即可,Merge操作会自动保留同一id下版本最高的行,无需手动生成取消行。
4. 统一汇总更新请求
将多客户端的更新请求汇总到中间服务,由该服务统一处理同一主键的更新,生成正确的取消行和新行后批量写入ClickHouse,从根源避免并发冲突。
内容的提问来源于stack exchange,提问作者Nick Allen
相关产品推荐
相关产品推荐

