1. 整体描述
一条上游消息进入系统后,往往不能直接成为页面查询或报表统计的数据。
消息可能包含嵌套数组,同一业务对象可能多次更新,不同来源也可能使用不同的字段和时间格式。如果把报文解析、版本判断和业务查询全部放在一个环节中,后续定位问题、调整规则和修复历史数据都会变得困难。
本文结合一套事件驱动的数据处理链路,介绍 Original、Raw、Processed 三层结构的职责与协作方式。客户名称、系统名称、表名、字段名和业务标识均已泛化;报文、时间、编号和数量示例为虚构内容,不对应真实业务记录。
这三层分别保留不同阶段的数据形态:
text
上游 Kafka 消息
↓
Original:保存来源报文及来源位置
↓ 解析、展平
Raw:保存结构化的来源明细
↓ 业务键识别、版本比较、结果加工
Processed:形成业务可消费的数据
↓
查询服务、业务应用、聚合视图和报表
层间使用 Kafka 通知触发后续处理,部分下游数据通过定时任务或同步链路生成。
分层的主要价值是让三个问题可以分别回答:
- 收到了什么:通过 Original 查看来源报文。
- 解析出了什么:通过 Raw 查看结构化明细。
- 业务当前使用什么:通过 Processed 查看按规则加工后的结果。
本文中的"三层"指数据处理的逻辑分层,不代表三个数据库、三个服务或三台服务器。它也不直接等同于 ODS、DWD、DWS:Processed 可以包含业务明细和最新状态,查询聚合与历史报表还可能继续建立在其后。
2. 具体描述
2.1 先按职责分层,再讨论表名
判断一张表属于哪一层,应先看数据来源、粒度和处理规则。
| 层次 | 保存的内容 | 主要用途 | 不能仅凭这一层确认的事情 |
|---|---|---|---|
| Original | 接入时保存的报文及来源元数据 | 追溯、重新解析、故障排查 | 明细已经完整写入 |
| Raw | 从报文展开的结构化字段和明细 | SQL 加工、来源核对、重新计算 | 数据已经符合最终业务口径 |
| Processed | 按业务键和版本等规则加工的结果 | 业务查询、下游共享、进一步聚合 | 任意历史时点的状态都可恢复 |
三层保留的是不同处理阶段的信息。它们不是把同一张业务表简单复制三份。
例如,一条消息包含多个商品和多个接收地点,Original 可以保存一条报文,Raw 会展开为多条明细,Processed 则根据有效版本保留业务需要的结果。三层行数并不要求一致。
2.2 Original:保留可以重新解释的数据
Original 的核心作用,是保留进入接入程序时的消息内容,让后续处理可以回到一个明确的来源。
以下为字段示意:
| 字段 | 含义 |
|---|---|
source_id |
本地原始记录标识 |
payload |
保存的消息内容 |
source_topic |
来源主题,可由记录或接入配置关联确定 |
source_partition |
来源分区 |
source_offset |
分区内消息位置 |
received_at |
本地接收或落库时间 |
需要区分"保存原始报文"和"保存原始字节"。如果接入程序已经将 Avro 等消息对象转换为 JSON,再存入数据库,保留的是转换后的内容,不能直接宣称它与上游传输字节完全一致。
Kafka 消息位置也需要主题、分区和 offset 一起解释。单独一个 offset 数字不能标识整个系统中的唯一消息,更不能作为跨分区的全局时间顺序。
Original 为重放提供了数据依据,但能否重放仍取决于报文是否完整、转换规则是否可用,以及关联配置是否保留。仅仅存在一列 JSON,并不意味着系统已经具备完整的重放能力。
2.3 Raw:将嵌套消息展开为可加工的明细
Raw 将消息结构转换为关系型数据,减少后续每次处理时重复解析报文的成本。
假设接收到以下虚构消息:
json
{
"documentNo": "DOC-DEMO",
"revision": 4,
"items": [
{
"lineNo": "L1",
"productCode": "PRODUCT-A",
"destinations": [
{"locationCode": "SITE-X", "quantity": 12},
{"locationCode": "SITE-Y", "quantity": 7}
]
}
]
}
按"单据 + 行 + 地点"展开后,得到两条明细:
| source_id | document_no | line_no | location_code | quantity | revision |
|---|---|---|---|---|---|
| SRC-DEMO | DOC-DEMO | L1 | SITE-X | 12 | 4 |
| SRC-DEMO | DOC-DEMO | L1 | SITE-Y | 7 | 4 |
这一步最需要明确的是明细粒度。只按单据号识别明细,会把不同商品行和地点混在一起;把地点数组错误地交叉展开,也可能制造额外记录。
Raw 通常仍然接近来源字段语义。日期可能暂时保留来源字符串,字段名也可能反映嵌套结构。业务展示格式和汇总计算不必在这一层全部完成。
同时,展开后的字段容量必须能承接来源内容。数组序列化后的文本写入过短字符串列,是一种实际可能出现的失败原因。Original 写入成功,不代表 Raw 一定能够完整落库。
2.4 Processed:将来源事件转换为业务结果
Raw 侧重表达来源数据,Processed 则开始回答业务如何使用这些数据。
本文参考的加工链路包含以下步骤:
- 从 Kafka 通知中取得关联标识。
- 查询对应 Raw 数据,记录本批次涉及的明细和业务键。
- 读取这些业务键已有的 Processed 数据。
- 合并新数据与已有结果,按约定规则选择有效版本。
- 将候选结果写入临时表。
- 替换目标范围内的 Processed 数据。
这是一种基于受影响业务键的增量加工方式,不必每收到一批消息就重新计算全部历史数据。
Processed 也不意味着所有业务规则都已统一。不同数据域可以有不同的粒度、版本含义和删除语义;实际接口还可能关联主数据、业务配置或汇总表。
2.5 将原始记录、明细和业务对象分别标识
三层链路中,至少存在三类标识:
| 标识 | 识别的对象 | 典型关系 |
|---|---|---|
| 原始记录 ID | 一次保存的来源消息 | 一条消息可生成多条明细 |
| 明细 ID | 一条 Raw 记录 | 多条明细可属于同一业务对象 |
| 业务键 | 跨消息持续更新的业务对象 | 一个对象可对应多个事件版本 |
三者不能互相替代。原始 ID 不同,可能只是同一业务对象发生了更新;业务键相同,也不代表两条来源消息可以直接当作重复消息删除。
在当前参考链路中,通知携带的关联值可以先定位原始记录,再找到对应 Raw 明细。不能仅凭程序中的变量名,就认定消息里一定传递 Raw 主键。
对外应将通知内容定义清楚:携带的是原始 ID、明细 ID,还是业务键,以及通知意味着"有数据待加工"还是"某阶段已经完成"。
2.6 版本选择不能简单理解为最后到达者覆盖
网络延迟、重试和历史补跑都可能让旧事件晚于新事件到达。
假设同一业务对象有两个版本:
| 事件 | 来源业务时间 | 本地到达顺序 |
|---|---|---|
| 较新事件 | 09:30 | 先到达 |
| 较旧事件 | 09:10 | 后到达 |
如果只按本地插入时间覆盖,后到的旧事件可能把业务状态回退。
参考实现会将新 Raw 数据与已有 Processed 结果合并,再按来源发布时间等字段排序;本地时间和原始记录标识也参与排序。这种处理意图是让新批次与现有结果共同参与版本选择。
但排序字段是否正确,需要结合来源契约判断:来源时间是否可靠,是否有正式版本号,相同时间如何处理,删除事件是否具有明确含义。数据库自增 ID 只能说明本地记录顺序,不能自动证明业务版本更高。
还要区分两种模型:
- 完整快照:新版本代表整个业务对象,应按对象选择一个版本,再保留其完整明细集合。
- 局部变更:新事件只更新部分字段或明细,需要按约定合并,不能直接替换整个对象。
参考 SQL 使用排名函数保留最高排名候选。并列排名可能保留多行,这可能正是保留同一版本多条明细所需要的行为;它不等于已经按明细业务键完成去重。
2.7 批次日志和临时表分别解决什么问题
批次日志记录本次处理涉及哪些 Raw 记录和业务键;临时表保存计算后的候选结果。
text
通知关联标识
↓
定位 Raw 明细和受影响业务键
↓
记录 batch_id + 明细标识 + 业务键
↓
合并候选、选择版本
↓
临时表保存本批次结果
↓
替换 Processed 中对应业务范围的数据
这种方式把"本次影响范围"和"本次计算结果"显式保存下来,便于核对加工过程。
批次隔离需要贯穿所有 SQL。即使日志和临时表具有 batch_id,如果某条查询没有按批次过滤,仍可能混入其他任务的数据。字段存在与隔离生效是两回事。
临时表也不自动等于数据库会话级临时表。它可能是带批次字段的普通表,生命周期、索引和清理方式需要单独管理。
2.8 先删后插的结果替换需要事务和并发约束
为了更新同一业务对象的多条明细,参考链路先生成候选结果,再删除目标范围中的旧记录,最后插入新记录。
text
计算候选结果 → 删除旧结果 → 插入新结果 → 提交
如果删除已经提交,而插入失败,业务查询就可能看到数据缺失。因此,相关步骤应处于明确的事务边界内,并确认实际使用的数据源、事务管理器和调用方式。
服务方法上的事务注解表达了设计意图,但不能单凭注解证明所有数据库操作都已由同一事务管理,更不能覆盖外部 Kafka 操作。
事务还需要与并发处理分别讨论。两个批次同时处理同一业务键,仍可能基于不同旧状态计算并交叉覆盖。需要结合消息分区、处理串行化、数据库锁或版本条件,明确同一对象的并发更新规则。
2.9 Kafka 负责触发处理,数据库承载阶段结果
在这类链路中,Kafka 通知可以携带关联标识,消费者收到通知后再读取数据库。
这样可以避免在层间重复传递完整报文,后续任务也可以基于已保存的数据重新计算。但消费者与表结构、数据可见时机及保留周期之间建立了依赖。
生产端应先确认当前阶段数据可用,再通知下一阶段。对于通知重复、投递失败和消费重试,也需要有对应处理方式。
text
数据库写入成功
↓
Kafka 通知发送失败
↓
当前层有数据,下一层可能没有启动
这个故障窗口不会因为增加了数据分层而消失。反过来,先发通知再提交数据库,也可能让消费者读取不到数据。
案例不能因此宣称具有跨数据库和 Kafka 的原子提交。若需要加强可靠性,可以进一步设计待发送事件表、投递补偿和下游幂等,但这些属于明确的增强措施,不能当作三层结构天然提供的能力。
2.10 分层让故障定位和恢复入口更清楚
当业务查询出现缺数时,可以沿着三个阶段逐步核对。
| 数据状态 | 优先检查 | 可考虑的恢复入口 |
|---|---|---|
| Original 缺失 | 上游发送、接入消费与原始落库 | 从上游恢复数据 |
| Original 存在,Raw 缺失 | 解析、展平、字段容量和写入异常 | 从 Original 重新解析 |
| Raw 存在,Processed 缺失 | 通知、消费、业务键和加工异常 | 从 Raw 重新加工或补通知 |
| Processed 存在,页面缺失 | 查询条件、同步链路、关联和权限 | 修复应用侧查询或同步 |
这也是保留 Original 和 Raw 的直接收益:不用每次修复都回到上游重新取数。
不过,重新加工仍需明确范围、代码版本、重复处理和覆盖规则。分层提供了恢复的数据基础,任务状态、检查点、幂等和验收对账则需要另外实现。
2.11 Processed 之后仍然需要面向应用的数据组织
业务列表和报表的粒度,未必与 Processed 明细粒度一致。
例如,页面按"单据 + 运输批次"分页,而底层存在多个事件和商品明细。查询时直接关联多张明细表,可能导致一条页面记录扩成多行,影响分页和统计。
当前工程中也存在先通过视图、临时表和同步表组织结果,再由业务接口查询的链路。它表明三层主链路之后,仍可以根据应用需求建立派生数据。
| 结果形态 | 需要明确的设计 |
|---|---|
| 当前状态查询表 | 业务键、更新规则与数据延迟 |
| 应用聚合表 | 分组粒度、关联基数与刷新机制 |
| 历史报表快照 | 统计范围、主数据版本与生成规则 |
Processed 保存当前有效结果,不代表历史报表可以直接查询它来恢复任意过去状态。历史快照需要额外的数据和规则约束。
因此,三层结构应被视为数据接入与加工的主干,而不是对所有后续分析模型的穷举。
2.12 分层需要支付存储、延迟和维护成本
Original 保存报文,Raw 展开明细,Processed 保存业务结果,加上索引、批次日志和临时数据,会带来额外存储成本。
层间异步处理也带来可见性延迟:Original 已收到数据时,Processed 可能尚未更新;应用同步表或报表又可能晚一步。
| 成本 | 需要考虑的事项 |
|---|---|
| 多阶段存储 | 保留周期、归档、索引和临时数据清理 |
| 多阶段延迟 | 各阶段积压、刷新时间和业务可接受时效 |
| 多套数据结构 | 来源字段变化、解析兼容和历史重算影响 |
| 多个故障入口 | 分阶段错误记录、通知补偿和差异核对 |
| 来源信息留存 | 访问范围、展示脱敏和日志内容控制 |
这些是架构需要持续管理的事项,不代表当前系统已经全部完成治理。
对于数据量小、结构简单、没有重放和复用需求的链路,完整保留三层未必划算。分层深度应由追溯、加工复杂度和消费方式决定。
2.13 用数据链路核对分层是否有效
验证三层结构,应检查各层之间的关系和处理语义,而不只确认三张表都有数据。
| 场景 | 应核对的结果 |
|---|---|
| 嵌套数组展开 | 明细粒度、数量及来源关联符合预期 |
| 旧事件晚到 | Processed 不因到达顺序错误回退 |
| 同一版本多条明细 | 完整保留所需明细,不误删、不扩行 |
| 重复通知 | 重复执行不产生不符合业务预期的结果 |
| 明细部分缺失 | 不以"已有一条数据"代替完整性判断 |
| 替换过程失败 | 核对回滚和提交后状态,不留下半次替换 |
| 同一对象并发加工 | 不混用批次,不发生未约束的覆盖 |
| 数据有而通知缺失 | 能够定位缺口并选择合适恢复入口 |
| 查询关联扩行 | 页面粒度、分页与汇总口径保持一致 |
以上是核对观察点,不代表已经完成全部验证。本文不提供未经测量的性能收益或测试通过结论。
3. 总结
Original、Raw、Processed 三层结构,把来源保存、结构转换和业务加工分成了可分别理解的阶段。
第一,Original 保留接入内容和来源位置,为追溯及重新解析提供依据。
第二,Raw 明确结构化明细的粒度,保留来源关联,让后续加工能够围绕关系型数据执行。
第三,Processed 根据业务键、版本和加工规则形成消费结果,查询聚合与历史快照仍可继续建立在其后。
第四,批次日志和临时表帮助组织增量加工,但批次隔离、事务、并发更新和通知可靠性需要分别落实。
第五,分层让故障恢复入口更清楚,同时也增加了存储、延迟和维护成本。它为可追溯、可复用和可重新计算提供基础,这些目标仍需要明确的数据契约与运行机制支撑。
附:实现检查表
| 检查项 | 需要确认的内容 |
|---|---|
| 层次职责 | 来源保存、结构转换、业务加工的边界明确 |
| 原始内容 | 清楚区分转换后报文与上游原始字节 |
| 来源位置 | 主题、分区、offset 可正确关联 |
| 明细粒度 | 展平规则与一对多关系明确 |
| 标识体系 | 原始 ID、明细 ID、业务键各司其职 |
| 消息契约 | 通知中的关联值及阶段含义明确 |
| 版本规则 | 区分业务时间、到达时间、快照与局部变更 |
| 批次隔离 | 日志、候选计算、临时表和更新范围一致 |
| 结果替换 | 事务资源及同一业务键并发规则明确 |
| 通知可靠性 | 数据库成功与 Kafka 投递成功分别确认 |
| 故障恢复 | 不同缺口有对应入口、范围和重复处理规则 |
| 应用消费 | 查询粒度、同步时效与历史快照分别设计 |
| 生命周期 | 保留、归档、清理及来源信息访问有明确约束 |
标签: 数据架构、数据分层、Kafka、数据加工、版本处理、数据追溯