<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>Watermark - tag - 沐木</title><link>https://oldletter.cn/tags/watermark/</link><description>Watermark - 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/watermark/" rel="self" type="application/rss+xml"/><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></channel></rss>