<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>ETL - tag - 沐木</title><link>https://oldletter.cn/tags/etl/</link><description>ETL - tag - 沐木</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sun, 19 Jul 2026 01:00:00 +0800</lastBuildDate><atom:link href="https://oldletter.cn/tags/etl/" rel="self" type="application/rss+xml"/><item><title>千万级数据同步与 ETL 实战（一）：边界、降级策略与整体架构</title><link>https://oldletter.cn/posts/data-sync-etl-01-boundaries-and-architecture/</link><pubDate>Sun, 19 Jul 2026 01:00:00 +0800</pubDate><author>mumu</author><guid>https://oldletter.cn/posts/data-sync-etl-01-boundaries-and-architecture/</guid><description><![CDATA[<blockquote>
<p>系列导航：<a href="/posts/data-sync-etl-01-boundaries-and-architecture/" rel="">1</a> | <a href="/posts/data-sync-etl-02-snapshot-watermark/" rel="">2</a> | <a href="/posts/data-sync-etl-03-hash-wheel-fingerprint/" rel="">3</a> | <a href="/posts/data-sync-etl-04-micro-batch-correctness/" rel="">4</a> | <a href="/posts/data-sync-etl-05-merkle-iblt/" rel="">5</a> | <a href="/posts/data-sync-etl-06-true-streaming/" rel="">6</a></p></blockquote>
<p>数据同步项目开工时，团队常先讨论 Kafka、Flink 或 Debezium。这个顺序容易忽略一个基础问题：源系统究竟能提供什么。很多企业数据源只有只读账号、分页接口或定期投递的文件。数据库日志权限拿不到，表里也未必有可靠的更新时间。此时直接套用 CDC 架构，实施阶段会在权限、版本、网络和运维能力上不断返工。</p>
<p>本文先给系统划定边界。目标数据集以单表或单集合不超过 1000 万行为基准，变更速率不高，业务接受几十秒到数小时的延迟。系统需要发现新增、修改和删除，支持断点恢复，并限制对源库的影响。每秒数十万事件、复杂事件时间计算、强实时交易链路不在这套微批架构的处理范围内，第六篇会单独讨论真正的流式系统。</p>
<h2 id="一先盘点数据源能力">一、先盘点数据源能力</h2>
<p>同一个“查询数据”接口，能提供的增量能力差别很大。建任务前要探测并固化以下信息：</p>
<table>
  <thead>
      <tr>
          <th>能力</th>
          <th>需要确认的内容</th>
          <th>缺失后的影响</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>变更日志</td>
          <td>Binlog、WAL、LSN、SCN、日志保留时间</td>
          <td>无法按日志位置持续消费</td>
      </tr>
      <tr>
          <td>版本字段</td>
          <td>单调版本号、精度、是否覆盖删除</td>
          <td>只能依赖时间或扫描</td>
      </tr>
      <tr>
          <td>更新时间</td>
          <td>是否有索引、是否随每次修改更新</td>
          <td>历史回改可能漏读</td>
      </tr>
      <tr>
          <td>稳定键</td>
          <td>单主键、复合业务键、键是否会变化</td>
          <td>无法可靠识别同一条记录</td>
      </tr>
      <tr>
          <td>一致性快照</td>
          <td>隔离级别、快照时长、日志衔接点</td>
          <td>首次全量与增量可能断层</td>
      </tr>
      <tr>
          <td>分页方式</td>
          <td>游标、Keyset、页码、排序稳定性</td>
          <td>并发写入时可能漏读或重复</td>
      </tr>
      <tr>
          <td>删除信息</td>
          <td>删除日志、软删字段、墓碑记录</td>
          <td>需要周期扫描才能识别删除</td>
      </tr>
      <tr>
          <td>源端改造</td>
          <td>能否加索引、生成列、触发器</td>
          <td>分桶扫描可能退化成全表计算</td>
      </tr>
  </tbody>
</table>
<p>能力探测结果进入数据集版本，不能只留在部署人员的经验里。任务发布后如果主键、时区或更新时间精度发生变化，旧 Checkpoint 可能失效。调度器需要暂停任务，完成迁移验证后再恢复。</p>
<p>建议把增量策略设计成一条降级链：</p>
<div class="mermaid" id="id-4" data-mermaid-definition="Zmxvd2NoYXJ0IExSCiAgICBBWyLor7vlj5bmupDnq6/og73lipsiXSAtLT4gQnsi5Y&#43;v5raI6LS55Y&#43;Y5pu05pel5b&#43;XIn0KICAgIEIgLS0&#43;fOaYr3wgQ1siQ0RDIC8g5pel5b&#43;X5aKe6YePIl0KICAgIEIgLS0&#43;fOWQpnwgRHsi5pyJ54mI5pys5Y&#43;35oiW5pu05paw5pe26Ze0In0KICAgIEQgLS0&#43;fOaYr3wgRVsiVmVyc2lvbiAvIFdhdGVybWFyayJdCiAgICBEIC0tPnzlkKZ8IEZ7IuaVsOaNruWPqui/veWKoCJ9CiAgICBGIC0tPnzmmK98IEdbIuiHquWinumUruWinumHjyJdCiAgICBGIC0tPnzlkKZ8IEhbIkhhc2ggV2hlZWwg5YiG5qG25omr5o&#43;PIl0KICAgIEggLS0&#43;IElbIuWFqOmHj&#43;aMh&#43;e6ueagoemqjOWFnOW6lSJd">flowchart LR
    A[&#34;读取源端能力&#34;] --&gt; B{&#34;可消费变更日志&#34;}
    B --&gt;|是| C[&#34;CDC / 日志增量&#34;]
    B --&gt;|否| D{&#34;有版本号或更新时间&#34;}
    D --&gt;|是| E[&#34;Version / Watermark&#34;]
    D --&gt;|否| F{&#34;数据只追加&#34;}
    F --&gt;|是| G[&#34;自增键增量&#34;]
    F --&gt;|否| H[&#34;Hash Wheel 分桶扫描&#34;]
    H --&gt; I[&#34;全量指纹校验兜底&#34;]</div><p>降级需要记录原因。例如数据库支持 Binlog，但账号缺少复制权限，任务可以暂时使用 Watermark，同时持续告警“当前链路无法直接识别物理删除”。若系统只把模式保存为一个枚举，维护人员看不到能力缺口，也无法判断将权限补齐后能获得什么收益。</p>]]></description></item><item><title>千万级数据同步与 ETL 实战（二）：快照、Watermark 与无漏数分页</title><link>https://oldletter.cn/posts/data-sync-etl-02-snapshot-watermark/</link><pubDate>Sun, 19 Jul 2026 01:00:00 +0800</pubDate><author>mumu</author><guid>https://oldletter.cn/posts/data-sync-etl-02-snapshot-watermark/</guid><description><![CDATA[<blockquote>
<p>系列导航：<a href="/posts/data-sync-etl-01-boundaries-and-architecture/" rel="">1</a> | <a href="/posts/data-sync-etl-02-snapshot-watermark/" rel="">2</a> | <a href="/posts/data-sync-etl-03-hash-wheel-fingerprint/" rel="">3</a> | <a href="/posts/data-sync-etl-04-micro-batch-correctness/" rel="">4</a> | <a href="/posts/data-sync-etl-05-merkle-iblt/" rel="">5</a> | <a href="/posts/data-sync-etl-06-true-streaming/" rel="">6</a></p></blockquote>
<p>增量查询写成 <code>WHERE update_time &gt; ?</code> 很容易，难点集中在三个交界处：首次快照进行时源表仍在写；同一时间戳容纳多行；目标端已提交而 Checkpoint 尚未提交。任一处处理不清，系统都会在低负载测试中正常运行，在真实并发和故障下出现漏数或旧值覆盖。</p>
<p>本文从一个约束明确的例子展开。源表有 1000 万行，主键 <code>id</code>，更新时间精确到毫秒，业务允许 1 分钟调度周期。源端只提供普通查询权限，没有日志消费能力。目标端支持按业务键 Upsert。</p>
<h2 id="一首次快照需要-epoch">一、首次快照需要 Epoch</h2>
<p>快照不是简单的 <code>SELECT *</code>。它是目标数据集的一次基线构建，必须能和快照期间发生的变化衔接。为每次基线分配单调递增的 <code>snapshotEpoch</code>：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-text">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title=""><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">task = customer_sync
</span></span><span class="line"><span class="cl">snapshotEpoch = 42
</span></span><span class="line"><span class="cl">schemaId = customer-v7</span></span></code></pre></div></div>
<p>Epoch 42 的每个 Batch 都引用 <code>customer-v7</code>，其编码口径在本轮基线构建期间保持冻结。新的快照开始后，旧 Epoch 仍可继续服务，直到新 Epoch 完成校验并原子切换。直接清空目标表再写入，会把数小时的快照过程暴露给查询方，也失去快速回退能力。</p>
<p>完整流程如下：</p>
<div class="mermaid" id="id-3" data-mermaid-definition="c2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDIGFzIENvbnRyb2wgUGxhbmUKICAgIHBhcnRpY2lwYW50IFMgYXMgU291cmNlCiAgICBwYXJ0aWNpcGFudCBCIGFzIEJhdGNoIFN0b3JlCiAgICBwYXJ0aWNpcGFudCBUIGFzIFRhcmdldAogICAgQy0&#43;PlM6IOaNleiOt&#43;W/q&#43;eFp&#43;i&#43;ueeVjCBIMAogICAgQy0&#43;PlM6IOWcqOi&#43;ueeVjOWGheaMieS4u&#43;mUruWIhumhteivu&#43;WPlgogICAgUy0tPj5COiDnlJ/miJAgRXBvY2ggNDIg55qE5b&#43;r54WnIEJhdGNoCiAgICBDLT4&#43;Uzog5LuOIEgwIOW8gOWni&#43;e0r&#43;enr&#43;WinumHjwogICAgQi0&#43;PlQ6IOW5guetieWGmeWFpSBFcG9jaCA0MgogICAgQy0&#43;PlQ6IOagoemqjOihjOaVsOOAgeaRmOimgeWSjOe6puadnwogICAgQy0&#43;PlQ6IOWOn&#43;WtkOWIh&#43;aNouW9k&#43;WJjSBFcG9jaAogICAgQy0&#43;PlM6IOe7p&#43;e7rea2iOi0ueWinumHj&#43;W5tuaPkOS6pCBDaGVja3BvaW50">sequenceDiagram
    participant C as Control Plane
    participant S as Source
    participant B as Batch Store
    participant T as Target
    C-&gt;&gt;S: 捕获快照边界 H0
    C-&gt;&gt;S: 在边界内按主键分页读取
    S--&gt;&gt;B: 生成 Epoch 42 的快照 Batch
    C-&gt;&gt;S: 从 H0 开始累积增量
    B-&gt;&gt;T: 幂等写入 Epoch 42
    C-&gt;&gt;T: 校验行数、摘要和约束
    C-&gt;&gt;T: 原子切换当前 Epoch
    C-&gt;&gt;S: 继续消费增量并提交 Checkpoint</div><p>如果数据库支持一致性快照和可对应的日志位置，<code>H0</code> 可以是事务快照对应的 LSN、SCN 或 Binlog Position。快照读取看到边界时刻的一致状态，增量从该位置之后消费，衔接关系能够精确定义。</p>]]></description></item><item><title>千万级数据同步与 ETL 实战（三）：Hash Wheel、行指纹与删除识别</title><link>https://oldletter.cn/posts/data-sync-etl-03-hash-wheel-fingerprint/</link><pubDate>Sun, 19 Jul 2026 01:00:00 +0800</pubDate><author>mumu</author><guid>https://oldletter.cn/posts/data-sync-etl-03-hash-wheel-fingerprint/</guid><description><![CDATA[<blockquote>
<p>系列导航：<a href="/posts/data-sync-etl-01-boundaries-and-architecture/" rel="">1</a> | <a href="/posts/data-sync-etl-02-snapshot-watermark/" rel="">2</a> | <a href="/posts/data-sync-etl-03-hash-wheel-fingerprint/" rel="">3</a> | <a href="/posts/data-sync-etl-04-micro-batch-correctness/" rel="">4</a> | <a href="/posts/data-sync-etl-05-merkle-iblt/" rel="">5</a> | <a href="/posts/data-sync-etl-06-true-streaming/" rel="">6</a></p></blockquote>
<p>有些源表只有主键和业务字段，没有更新时间、行版本、删除标记，也拿不到数据库日志。要发现历史记录被修改或删除，完整周期仍需检查全部记录。工程上的问题变成：怎样把一次重扫描拆开，怎样保证同一条记录总落到同一个分区，怎样避免读取中断造成误删除，以及怎样把目标写入量限制在真实变化量附近。</p>
<p>Hash Wheel 负责把全表检查平摊到时间轴，Fingerprint Index 保存上一次看到的状态，Generation 在完整扫描后识别删除。这三个组件必须共同工作。只实现“主键取模加定时查询”，无法形成可恢复的变化检测链路。</p>
<h2 id="一身份键和内容指纹分开">一、身份键和内容指纹分开</h2>
<p>每条记录先抽象为二元组：</p>
<p>$$
Entry=(IdentityKey, RowFingerprint)
$$</p>
<p><code>IdentityKey</code> 由稳定主键或业务键得到，用于确认记录身份。<code>RowFingerprint</code> 由参与同步的字段计算，用于判断内容变化。两者的判断关系如下：</p>
<table>
  <thead>
      <tr>
          <th>旧状态</th>
          <th>本轮状态</th>
          <th>结果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>不存在</td>
          <td>存在</td>
          <td>INSERT</td>
      </tr>
      <tr>
          <td>存在</td>
          <td>指纹不同</td>
          <td>UPDATE</td>
      </tr>
      <tr>
          <td>存在</td>
          <td>指纹相同</td>
          <td>NO_CHANGE</td>
      </tr>
      <tr>
          <td>存在</td>
          <td>完整扫描后未见</td>
          <td>DELETE</td>
      </tr>
  </tbody>
</table>
<p>业务键允许修改时，它不再是稳定身份。系统只能把键变更表达成旧键删除和新键新增，除非源端另有不可变 ID 或变更日志能够关联前后值。这个限制需要进入数据集契约。</p>
<p>身份键可直接保存原主键，也可以保存带命名空间的摘要：</p>
<p>$$
IdentityKey=H(datasetId \parallel canonical(primaryKey))
$$</p>
<p>跨数据集使用同一主键时，<code>datasetId</code> 防止状态键冲突。原主键是否保留取决于审计和回查需求；只有摘要会增加碰撞处理与问题定位成本。</p>
<h2 id="二hash-前先定义-canonicalization">二、Hash 前先定义 Canonicalization</h2>
<p>指纹错误通常源于编码规则，而非 Hash 函数。以下值在某些业务语义下相同，直接转字符串却会得到不同字节：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-text">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title=""><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Decimal: 1、1.0、1.00
</span></span><span class="line"><span class="cl">Time:    2026-07-19 10:00:00、2026-07-19T02:00:00Z
</span></span><span class="line"><span class="cl">JSON:    {&#34;a&#34;:1,&#34;b&#34;:2}、{&#34;b&#34;:2,&#34;a&#34;:1}
</span></span><span class="line"><span class="cl">String:  Unicode 组合字符与预组合字符</span></span></code></pre></div></div>
<p>每次编码口径调整都要提升 <code>canonicalizationVersion</code>，旧指纹只按旧口径解释。字段规则包括：</p>]]></description></item><item><title>千万级数据同步与 ETL 实战（四）：Micro Batch、幂等写入与故障恢复</title><link>https://oldletter.cn/posts/data-sync-etl-04-micro-batch-correctness/</link><pubDate>Sun, 19 Jul 2026 01:00:00 +0800</pubDate><author>mumu</author><guid>https://oldletter.cn/posts/data-sync-etl-04-micro-batch-correctness/</guid><description><![CDATA[<blockquote>
<p>系列导航：<a href="/posts/data-sync-etl-01-boundaries-and-architecture/" rel="">1</a> | <a href="/posts/data-sync-etl-02-snapshot-watermark/" rel="">2</a> | <a href="/posts/data-sync-etl-03-hash-wheel-fingerprint/" rel="">3</a> | <a href="/posts/data-sync-etl-04-micro-batch-correctness/" rel="">4</a> | <a href="/posts/data-sync-etl-05-merkle-iblt/" rel="">5</a> | <a href="/posts/data-sync-etl-06-true-streaming/" rel="">6</a></p></blockquote>
<p>增量检测得到一组 INSERT、UPDATE 和 DELETE 后，数据仍未安全到达目标端。读取进程可能宕机，网络可能在响应返回前断开，目标库可能提交成功却没有被调用方确认，两个 Worker 也可能短时间内同时持有同一分片。系统能否恢复，取决于批次是否可重放、写入是否幂等、Checkpoint 何时推进，以及旧租约持有者能否继续提交。</p>
<p>Micro Batch 的作用不只是凑够若干行再发送。它是抽取结果的持久化边界，也是源端读取和目标端提交之间的恢复凭证。</p>
<h2 id="一batch-创建后保持不可变">一、Batch 创建后保持不可变</h2>
<p>一个批次由数据文件和 Manifest 组成。数据文件可使用 Arrow、Parquet 或有明确 Schema 的二进制格式；Manifest 描述这批数据从哪里来、覆盖哪个游标区间、包含多少变化，以及如何验证内容。</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-json">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title=""><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;batchId&#34;</span><span class="p">:</span> <span class="s2">&#34;01J2SYNTHETIC8TQ5F6X8R3&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;taskId&#34;</span><span class="p">:</span> <span class="s2">&#34;customer-sync&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;taskVersion&#34;</span><span class="p">:</span> <span class="mi">12</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;partitionId&#34;</span><span class="p">:</span> <span class="s2">&#34;bucket-0037&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;epoch&#34;</span><span class="p">:</span> <span class="mi">42</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;schemaId&#34;</span><span class="p">:</span> <span class="s2">&#34;customer-v7&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;cursorFrom&#34;</span><span class="p">:</span> <span class="s2">&#34;2026-07-19T10:00:00.000Z|80000&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;cursorTo&#34;</span><span class="p">:</span> <span class="s2">&#34;2026-07-19T10:01:00.000Z|84321&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;recordCount&#34;</span><span class="p">:</span> <span class="mi">5000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;insertCount&#34;</span><span class="p">:</span> <span class="mi">120</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;updateCount&#34;</span><span class="p">:</span> <span class="mi">47</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;deleteCount&#34;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;dataUri&#34;</span><span class="p">:</span> <span class="s2">&#34;batch://customer-sync/42/bucket-0037/part-0001.parquet&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;checksum&#34;</span><span class="p">:</span> <span class="s2">&#34;sha256:...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;createdAt&#34;</span><span class="p">:</span> <span class="s2">&#34;2026-07-19T10:01:12Z&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p><code>batchId</code> 一经生成就对应固定字节。重试只能重复发送该批次，不能复用同一个 ID 重新查询源端并覆盖内容。源数据可能在两次查询之间变化，同 ID 不同内容会破坏去重和审计。</p>
<p>批次写入可采用临时对象加原子完成标记：先写数据文件，计算长度和校验和，再写 Manifest，随后将状态改为 <code>READY</code>。本地文件系统可使用同卷原子重命名；对象存储通常没有目录重命名语义，可以用不可变对象 Key 和独立状态记录。看到 <code>READY</code> 前，Sink Worker 不得消费。</p>]]></description></item><item><title>千万级数据同步与 ETL 实战（五）：桶摘要、Merkle Tree 与 IBLT 对账</title><link>https://oldletter.cn/posts/data-sync-etl-05-merkle-iblt/</link><pubDate>Sun, 19 Jul 2026 01:00:00 +0800</pubDate><author>mumu</author><guid>https://oldletter.cn/posts/data-sync-etl-05-merkle-iblt/</guid><description><![CDATA[<blockquote>
<p>系列导航：<a href="/posts/data-sync-etl-01-boundaries-and-architecture/" rel="">1</a> | <a href="/posts/data-sync-etl-02-snapshot-watermark/" rel="">2</a> | <a href="/posts/data-sync-etl-03-hash-wheel-fingerprint/" rel="">3</a> | <a href="/posts/data-sync-etl-04-micro-batch-correctness/" rel="">4</a> | <a href="/posts/data-sync-etl-05-merkle-iblt/" rel="">5</a> | <a href="/posts/data-sync-etl-06-true-streaming/" rel="">6</a></p></blockquote>
<p>两个节点各保存 1000 万条记录，直接交换全部身份键和行指纹可以得到精确差异，但通信量和排序成本很高。若双方已经维护稳定分桶和本地指纹状态，可以先交换小摘要，只对不一致区域继续下钻。差异条目很少时，还可以用 IBLT 直接恢复差集。</p>
<p>这些结构优化的是对账和差异定位。源端没有日志、版本或预存摘要时，首次生成全量状态仍需读取 $N$ 条记录。任何摘要结构都无法从未读取的数据中推断变化。</p>
<h2 id="一对账对象先固定">一、对账对象先固定</h2>
<p>对账不能直接对任意行对象求 Hash。双方先约定：</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-text">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title=""><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">datasetId
</span></span><span class="line"><span class="cl">schemaId
</span></span><span class="line"><span class="cl">canonicalizationVersion
</span></span><span class="line"><span class="cl">identityKeyEncoding
</span></span><span class="line"><span class="cl">rowFingerprintAlgorithm
</span></span><span class="line"><span class="cl">partitionAlgorithmVersion
</span></span><span class="line"><span class="cl">snapshotEpoch 或 comparisonBoundary</span></span></code></pre></div></div>
<p>每条记录映射为集合元素：</p>
<p>$$
x=encode(IdentityKey, RowFingerprint)
$$</p>
<p>如果只比较身份键，只能发现新增和删除，无法发现内容更新。把身份和内容指纹共同编码后，同一键内容变化会表现为删除旧元素并新增新元素。结果解码后再按身份键合并成 UPDATE。</p>
<p>对账边界也要一致。节点 A 对应 10:00 的状态，节点 B 已处理到 10:05，摘要不同并不代表数据损坏。双方应冻结一个可比较 Epoch，或记录各分区的日志位置和 Watermark，在共同边界上生成摘要。</p>
<h2 id="二单个-hash-无法解释差异">二、单个 Hash 无法解释差异</h2>
<p>把所有元素排序后计算一个 SHA-256，可以判断“集合大概率相同”，但摘要不一致时无法定位哪条记录有问题。重新传输全量集合会失去摘要的工程价值。</p>
<p>只使用 XOR 也不够。定义：</p>
<p>$$
X(S)=\bigoplus_{x\in S}h(x)
$$</p>
<p>XOR 可交换、可结合，相同元素能抵消，适合合并分片。不过它丢失了计数信息，重复元素会相互抵消，多组不同输入也可能得到相同结果。对账摘要可组合多个统计量：</p>]]></description></item></channel></rss>