【实时数仓·2】CDC到Doris数据对不上——Sequence Column解了吗?

这是【实时数仓】系列第2篇。上篇讲Paimon选型,这篇讲CDC踩坑。

复制代码
CDC日志:正常 ✓
Flink日志:正常 ✓
Doris日志:正常 ✓

但数据:不对 ✗

排查:从CDC到Flink到Doris

#mermaid-svg-flUt7npgM8tqxHea{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-flUt7npgM8tqxHea .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-flUt7npgM8tqxHea .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-flUt7npgM8tqxHea .error-icon{fill:#552222;}#mermaid-svg-flUt7npgM8tqxHea .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-flUt7npgM8tqxHea .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-flUt7npgM8tqxHea .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-flUt7npgM8tqxHea .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-flUt7npgM8tqxHea .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-flUt7npgM8tqxHea .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-flUt7npgM8tqxHea .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-flUt7npgM8tqxHea .marker{fill:#333333;stroke:#333333;}#mermaid-svg-flUt7npgM8tqxHea .marker.cross{stroke:#333333;}#mermaid-svg-flUt7npgM8tqxHea svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-flUt7npgM8tqxHea p{margin:0;}#mermaid-svg-flUt7npgM8tqxHea .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-flUt7npgM8tqxHea .cluster-label text{fill:#333;}#mermaid-svg-flUt7npgM8tqxHea .cluster-label span{color:#333;}#mermaid-svg-flUt7npgM8tqxHea .cluster-label span p{background-color:transparent;}#mermaid-svg-flUt7npgM8tqxHea .label text,#mermaid-svg-flUt7npgM8tqxHea span{fill:#333;color:#333;}#mermaid-svg-flUt7npgM8tqxHea .node rect,#mermaid-svg-flUt7npgM8tqxHea .node circle,#mermaid-svg-flUt7npgM8tqxHea .node ellipse,#mermaid-svg-flUt7npgM8tqxHea .node polygon,#mermaid-svg-flUt7npgM8tqxHea .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-flUt7npgM8tqxHea .rough-node .label text,#mermaid-svg-flUt7npgM8tqxHea .node .label text,#mermaid-svg-flUt7npgM8tqxHea .image-shape .label,#mermaid-svg-flUt7npgM8tqxHea .icon-shape .label{text-anchor:middle;}#mermaid-svg-flUt7npgM8tqxHea .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-flUt7npgM8tqxHea .rough-node .label,#mermaid-svg-flUt7npgM8tqxHea .node .label,#mermaid-svg-flUt7npgM8tqxHea .image-shape .label,#mermaid-svg-flUt7npgM8tqxHea .icon-shape .label{text-align:center;}#mermaid-svg-flUt7npgM8tqxHea .node.clickable{cursor:pointer;}#mermaid-svg-flUt7npgM8tqxHea .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-flUt7npgM8tqxHea .arrowheadPath{fill:#333333;}#mermaid-svg-flUt7npgM8tqxHea .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-flUt7npgM8tqxHea .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-flUt7npgM8tqxHea .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-flUt7npgM8tqxHea .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-flUt7npgM8tqxHea .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-flUt7npgM8tqxHea .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-flUt7npgM8tqxHea .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-flUt7npgM8tqxHea .cluster text{fill:#333;}#mermaid-svg-flUt7npgM8tqxHea .cluster span{color:#333;}#mermaid-svg-flUt7npgM8tqxHea div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-flUt7npgM8tqxHea .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-flUt7npgM8tqxHea rect.text{fill:none;stroke-width:0;}#mermaid-svg-flUt7npgM8tqxHea .icon-shape,#mermaid-svg-flUt7npgM8tqxHea .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-flUt7npgM8tqxHea .icon-shape p,#mermaid-svg-flUt7npgM8tqxHea .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-flUt7npgM8tqxHea .icon-shape .label rect,#mermaid-svg-flUt7npgM8tqxHea .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-flUt7npgM8tqxHea .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-flUt7npgM8tqxHea .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-flUt7npgM8tqxHea :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} CDC
写入
正常
正常
数据不对
MySQL源库
Flink
Doris
查CDC日志
查Flink日志
查Doris数据
问题在Doris写入

第一步:确认CDC是否捕获了变更

sql 复制代码
SELECT order_id, status, update_time 
FROM mysql_source 
WHERE order_id = 'WB20260601001';

结果:CDC捕获了状态变更已发货→已完成,时间戳也对。

第二步:确认Flink是否正确处理

在Flink任务里加了日志,打印每条记录的order_id、status、update_time。确认输出是对的。

第三步:查Doris里的实际数据

sql 复制代码
SELECT order_id, status, update_time 
FROM doris_orders 
WHERE order_id = 'WB20260601001';

结果:status是"已发货",update_time是旧的时间戳。

CDC捕获了变更,Flink处理了变更,但Doris里存的还是旧值。

问题出在Doris写入。

定位:乱序导致数据被覆盖

查了Doris的导入日志,发现一个关键细节:

这个运单的两条记录(已发货、已完成)几乎同时到达Doris。但"已完成"先到,"已发货"后到。

Doris的Unique Key模型在没有Sequence Column的情况下,按写入版本号决定覆盖顺序------后写入的版本号更高,会覆盖先写入的。

顺序 T1(先到) T2(后到) Doris最终值 结果
正常 已发货 已完成 已完成
乱序 已完成 已发货 已发货

Doris不知道哪条数据更新,它只看版本号。

为什么乱序是CDC管道的固有问题

一开始我以为乱序是偶发的网络抖动。后来发现不是。

CDC管道里,同一条记录的多次变更可能在毫秒级内到达。Flink的并行度会影响处理顺序:同一个运单的两次变更,可能被不同的并行实例处理,谁先写入Doris取决于谁先完成,不取决于谁先发生。

复制代码
MySQL binlog:  已发货 → 已完成(时间顺序)
                ↓
Flink并行实例:  实例A处理"已完成"  实例B处理"已发货"
                ↓                    ↓
Doris写入:     "已完成"先到       "已发货"后到
                ↓
Doris最终值:   已发货 ✗

所以乱序不是网络问题,是CDC管道的结构性问题。只要用Flink+Doris,就一定会遇到。

解决:Sequence Column

Doris的Unique Key模型支持Sequence Column,可以指定一个字段作为排序依据。写入时,按这个字段的值排序,值大的覆盖值小的,值小的不会覆盖值大的。

核心机制: Doris在内部维护一个隐藏列__DORIS_SEQUENCE_COL__,用户指定哪个字段作为Sequence列后,Doris在每次写入时比较Sequence列的值,只有新值大于已有值才会覆盖。

两种启用方式

属性 含义 表中是否必须有对应列
function_column.sequence_col 把Sequence列映射到表中已有的列
function_column.sequence_type 只指定Sequence列的类型,存在隐藏列中

支持的类型:整数类型、DATE、DATETIME。建表后不能改。

方式一:映射到已有列

sql 复制代码
CREATE TABLE orders (
  order_id BIGINT,
  status VARCHAR(32),
  update_time DATETIME
) UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 4
PROPERTIES (
  "function_column.sequence_col" = "update_time"
);

导入时不需要额外参数,Doris自动用update_time列的值作为排序依据。

方式二:使用隐藏列

sql 复制代码
CREATE TABLE orders (
  order_id BIGINT,
  status VARCHAR(32),
  update_time DATETIME
) UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 4
PROPERTIES (
  "function_column.sequence_type" = "DATETIME"
);

导入时需要在header里指定映射:

bash 复制代码
curl --location-trusted -u root \
 -H "columns: order_id,status,update_time,source_sequence" \
 -H "function_column.sequence_col: source_sequence" \
 -T testData http://host:port/api/db/orders/_stream_load

验证效果

导入三条同Key数据:

复制代码
1  已发货  2026-06-01 10:00:00
1  运输中  2026-06-01 11:00:00
1  已完成  2026-06-01 12:00:00

不管到达顺序如何,Doris最终存的是update_time最大的那条:已完成 2026-06-01 12:00:00

在已有表上启用

sql 复制代码
ALTER TABLE orders ENABLE FEATURE "SEQUENCE_LOAD" 
WITH PROPERTIES ("function_column.sequence_type" = "DATETIME");

检查是否启用

sql 复制代码
SET show_hidden_columns = true;
DESC orders;

如果输出包含__DORIS_SEQUENCE_COL__,说明已启用。

Sequence Column的选择

update_time是最常见的选择,但有些业务场景下update_time可能重复------同一条记录在同一秒内多次变更,update_time一样大,Doris不知道谁覆盖谁。

这种情况下,需要更细粒度的字段,比如带毫秒的时间戳,或者自增的版本号。

Doris 2.0.14的Bug

我们用的是Doris 2.0.14。

改完Sequence Column之后,跑了两天没问题。第三天突然发现:某些运单在Doris里有两条记录,主键重复了。

查了半天,发现是Doris 2.0.14的一个已知Bug:执行ALTER TABLE ENABLE FEATURE "SEQUENCE_LOAD"后立即导入数据,会导致重复主键。

解决方案:删表重建,或者升级到2.0.15以上。

我们选了删表重建,因为升级涉及面太大。

回头看:CDC涉及四个层面

乱序问题排查了三天。不是问题难,是排查方向错了------一直在查CDC和Flink,没想到问题在Doris的写入顺序。

回头看,CDC看起来简单------配个连接器,起个任务,完事。但实际上涉及四个层面,每个层面都有坑:

层面 典型问题 根因
源库 连接失败、权限不足 binlog默认不开,CDC账号需要REPLICATION SLAVE权限
网络 偶尔断开、握手失败 MySQL开了SSL后CDC必须配SSL参数
SQL引擎 语法报错 Flink SQL语法和MySQL差别大(DATE_SUB不存在、value是保留字)
存储引擎 数据对不上 Doris后写覆盖先写,乱序是结构性问题

怎么判断问题在哪一层

排查CDC问题,不能只看CDC日志。关键是判断问题出在哪一层

特征 问题在哪层 排查方法
任务启动就失败 源库层 看报错信息
任务偶尔断开 网络层 看网络监控、检查SSL配置
处理数据时报语法错误 SQL引擎层 查Flink SQL文档
一切正常但数据不对 存储引擎层 逐条对比源库和目标库

我们的乱序问题就是第四种:一切正常,但数据不对。


写在最后

CDC是实时数仓最简单的一步,但涉及源库、网络、SQL引擎、存储引擎四个层面,每个层面都有坑。

乱序问题排查了三天。不是问题难,是排查方向错了------一直在查CDC和Flink,没想到问题在Doris的写入顺序。

什么时候该用Sequence Column?

场景 是否需要Sequence Column
同一条记录多次变更(订单状态变更) 必须用
同一条记录只写一次(日志数据) 不需要

CDC四个层面的排查优先级:

  1. 源库层:任务启动就失败,看报错信息
  2. 网络层:任务偶尔断开,看网络监控
  3. SQL引擎层:处理数据时报语法错误,查Flink文档
  4. 存储引擎层:一切正常但数据不对,最难排查

参考文档:Doris官方文档 - Unique Key并发更新控制

下一篇讲Flink实时ETL------数据抽过来之后怎么处理。

相关推荐
黎阳之光2 小时前
数字孪生赋能智慧油站建设|黎阳之光全场景可视化安防管控平台落地应用
大数据·物联网·算法·安全·数字孪生
真上帝的左手2 小时前
19. 大数据- BI 入门-数据集成全维度详解
大数据·bi
十六年开源服务商2 小时前
2026外贸WordPress社交媒体营销运营指南
大数据·人工智能·媒体
戴西软件2 小时前
戴西Trillion数字化平台应用开发框架技术解析:企业级应用的全栈开发基座
大数据
迈巴赫车主2 小时前
Hive中分组聚合导致的数据倾斜优化
数据仓库·hive·hadoop
白狐_7982 小时前
AI 数据分析 Skill 实战:用模拟游客数据生成文旅运营报告
大数据·服务器·人工智能
真上帝的左手2 小时前
19. 大数据- BI 入门-数仓实战5-ADS 整体设计框架
大数据·数据仓库·bi
TDengine (老段)2 小时前
TDengine Cache 与 Last 查询加速 — CACHEMODEL 机制与 RocksDB 缓存层
大数据·数据库·物联网·struts·缓存·时序数据库·tdengine
段一凡-华北理工大学2 小时前
工业领域的Hadoop架构学习~系列文章13:数据湖架构 - 工业大数据的统一存储底座
大数据·人工智能·hadoop·分布式·架构·高炉炼铁·高炉智能化