<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-CN"><title type="text">二夕说废话の地方</title><subtitle type="html">二夕的个人博客，一半献给技术，另一半还在寻找。。。</subtitle><updated>2026-07-03T16:08:03+00:00</updated><id>https://quakewang.github.io/</id><link rel="alternate" type="text/html" href="https://quakewang.github.io/"/><link rel="self" type="application/atom+xml" href="https://quakewang.github.io/atom.xml"/><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><generator uri="https://gohugo.io/" version="0.134.1">Hugo</generator><entry><title type="text">记一次 paimon-rust CI hang 的排查记录</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/paimon/paimon002_ci-hang/"/><id>https://quakewang.github.io/tech/paimon/paimon002_ci-hang/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2026-06-24T23:31:36+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h2 id="问题背景">问题背景&lt;/h2>
&lt;p>一个稳定的 CI 对于开源项目来说是至关重要的，如果提交 PR 大概率因为 CI 不稳定导致爆红，对于贡献者来说是比较打击积极性的。在前段时间，paimon-rust 的 GitHub CI 频繁卡在 Vortex 相关测试上。修过几轮，每次看起来好了，过几天又会复发，很是烦人。好在成功修复了，写这篇博客来简单记录一下。&lt;/p>……</summary><content type="html">&lt;h2 id="问题背景">问题背景&lt;/h2>
&lt;p>一个稳定的 CI 对于开源项目来说是至关重要的，如果提交 PR 大概率因为 CI 不稳定导致爆红，对于贡献者来说是比较打击积极性的。在前段时间，paimon-rust 的 GitHub CI 频繁卡在 Vortex 相关测试上。修过几轮，每次看起来好了，过几天又会复发，很是烦人。好在成功修复了，写这篇博客来简单记录一下。&lt;/p>
&lt;p>最近一次比较完整的现场是 &lt;a href="https://github.com/apache/paimon-rust/actions/runs/27869393079">CI run 27869393079&lt;/a>：&lt;code>integration&lt;/code> job 里的 &lt;code>DataFusion Vortex Integration Test&lt;/code> step 卡在 &lt;code>test_vortex_file_format_sql_e2e&lt;/code>，日志停在 &lt;code>has been running for over 60 seconds&lt;/code>，之后一直没有新的测试输出。这个 step 从 2026-06-20 11:21:40 UTC 跑到 17:12:06 UTC，接近 6 小时后被 Actions 取消。从现象来看可以定位是测试 hang 住了。不过比较奇怪的一点，在自己的开发环境，执行对应的测试，复现不出来（后面在 AI 的帮助下通过限制 CPU 能够大概率复现）。&lt;/p>
&lt;p>既然不好复现，倒不如直接顺着支持 Vortex 的时间线重新梳理一遍，如下：&lt;/p>
&lt;p>Vortex 支持最早来自 &lt;a href="https://github.com/apache/paimon-rust/pull/260">&lt;code>feat: add Vortex columnar file format support as optional feature&lt;/code>&lt;/a>。这个 PR 在 &lt;code>crates/paimon/Cargo.toml&lt;/code> 里引入了 Vortex &lt;code>0.68&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">vortex&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">version&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;0.68&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">features&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;tokio&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nx">optional&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>简单概括下这个 RP 的内容：新增了 &lt;code>crates/paimon/src/arrow/format/vortex.rs&lt;/code>，实现 Vortex 文件读写、predicate pushdown 和 row selection，并把 &lt;code>vortex&lt;/code> feature 加进 clippy、build 和 unit CI。也就是说，从 #260 开始，Vortex 读写路径进入了常规 CI。&lt;/p>
&lt;p>但这次挂住的 DataFusion SQL e2e 测试不是 #260 带来的，而是后面的 &lt;a href="https://github.com/apache/paimon-rust/pull/321">&lt;code>test(datafusion): add vortex SQL e2e&lt;/code>&lt;/a>。#321 新增了 &lt;code>crates/integrations/datafusion/tests/vortex_tables.rs&lt;/code>，并在 CI 里加了单独的 &lt;code>DataFusion Vortex Integration Test&lt;/code> step：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cargo &lt;span class="nb">test&lt;/span> -p paimon-datafusion --features vortex --test vortex_tables
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>第一次比较明确的 Vortex hang 线索出现在 &lt;a href="https://github.com/apache/paimon-rust/pull/353">&lt;code>build: pin unicode-segmentation for vortex tests&lt;/code>&lt;/a>。当时 revert 其他改动后，main CI 仍然 hanging。对比挂住的 Ubuntu unit job，差异集中在 Vortex tests 解析出来的依赖集合上。#353 先把 &lt;code>unicode-segmentation&lt;/code> pin 到 &lt;code>=1.13.2&lt;/code>，目标很直接：先让 CI 恢复。&lt;/p>
&lt;p>之后问题又复发，我顺着这个方向提了个 PR，从依赖解析转向 Vortex 读写路径本身。&lt;a href="https://github.com/apache/paimon-rust/pull/375">&lt;code>fix: keep Vortex runtime alive during async IO&lt;/code>&lt;/a> 处理的是 runtime handle 生命周期：旧代码直接用 &lt;code>VortexSession::default()&lt;/code>，async read/write 可能比临时 runtime owner 活得更久，后面的 background IO task 没有可用 executor 时就会 stall。&lt;/p>
&lt;p>再后面是之信老师提的 &lt;a href="https://github.com/apache/paimon-rust/pull/387">&lt;code>fix: stabilize Vortex runtime for async IO&lt;/code>&lt;/a>。这个 PR 把 Vortex read/write 移到 dedicated blocking thread 和 &lt;code>CurrentThreadRuntime&lt;/code> 上；读取时改用 &lt;code>open_buffer&lt;/code>，写入时先序列化到内存 buffer，同时绕开 Vortex &lt;code>0.68&lt;/code> 的 filtered-scan 路径。&lt;/p>
&lt;p>回头看，这几次修复都不是无效的。它们分别处理了当时暴露出来的依赖解析、runtime 生命周期、scan/write 问题，所以 CI 能阶段性恢复。但这次 integration cancellation 又把问题收窄到了另一条路径：打开 Vortex 文件、读取 footer、构建 layout。&lt;/p>
&lt;h2 id="先确认现象">先确认现象&lt;/h2>
&lt;p>我把问题先拆成两类：&lt;/p>
&lt;ol>
&lt;li>Vortex 读写路径是否会 hang。&lt;/li>
&lt;li>integration tests 是否只是缺少外部 fixture。&lt;/li>
&lt;/ol>
&lt;p>复现时聚焦 DataFusion 的 Vortex 表测试，并加上 timeout：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">timeout 150s taskset -c &lt;span class="m">0&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> cargo &lt;span class="nb">test&lt;/span> -p paimon-datafusion --features vortex --test vortex_tables -- --nocapture
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>在 Vortex &lt;code>0.68&lt;/code> 路径上，测试可以卡在 &lt;code>test_vortex_file_format_sql_e2e&lt;/code>，直到 timeout。这个现象和 &lt;a href="https://github.com/apache/paimon-rust/actions/runs/27869393079">CI run 27869393079&lt;/a> 对得上：step 编译完成后运行 1 个测试，&lt;code>test_vortex_file_format_sql_e2e&lt;/code> 超过 60 秒没有结束，之后长时间没有新的输出，最后只看到 &lt;code>The operation was canceled&lt;/code>。&lt;/p>
&lt;p>通过 &lt;code>taskset -c 0&lt;/code> 只是让本地更接近受限 runner 的调度环境，然后可以高概率的复现出 hang 的现象，在后面的排查中发现并不是根因哦。&lt;/p>
&lt;p>CI 里对应的 step 跑在 &lt;code>ubuntu-latest&lt;/code> 上：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cargo &lt;span class="nb">test&lt;/span> -p paimon-datafusion --features vortex --test vortex_tables
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>另一个干扰项也要排掉：有些 integration tests 如果缺少预置 Paimon 表 fixture，会以 &lt;code>TableNotExist&lt;/code> 失败，例如 &lt;code>default.partitioned_log_table&lt;/code>、&lt;code>default.multi_partitioned_log_table&lt;/code>。这类失败会明确 panic，和测试进程无输出卡住不是一回事。&lt;/p>
&lt;h2 id="paimon-rust-是怎么触发的">paimon-rust 是怎么触发的&lt;/h2>
&lt;p>paimon-rust 的 Vortex reader 会在 blocking task 中创建 Vortex session，然后打开 Vortex 文件：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">runtime&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">CurrentThreadRuntime&lt;/span>::&lt;span class="n">new&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">session&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">VortexSession&lt;/span>::&lt;span class="n">default&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">with_handle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">runtime&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">handle&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">read_vortex_batches&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">runtime&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">session&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ByteBuffer&lt;/span>::&lt;span class="n">from&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">bytes&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">plan&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>后面的 &lt;code>read_vortex_batches&lt;/code> 会通过 Vortex open/scan API 读取文件。DataFusion SQL 层、paimon-rust 的 projection/filter 逻辑都只是入口；真正卡住的位置在 Vortex 打开文件并读取 footer 的过程中。&lt;/p>
&lt;h2 id="回到-vortex-068-源码">回到 Vortex 0.68 源码&lt;/h2>
&lt;p>有了上述的排查过程，根据 Vortex 0.68 的源码来一探究竟，可以发现 footer 反序列化代码在 &lt;code>vortex-file/src/footer/mod.rs&lt;/code>。关键调用长这样：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">root_layout&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">layout_from_flatbuffer_with_options&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">layout_bytes&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">dtype&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">layout_read_ctx&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">array_read_ctx&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">session&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">layouts&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">registry&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">session&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">allows_unknown&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这里有两个参数看起来都只是读 session：&lt;/p>
&lt;ul>
&lt;li>&lt;code>session.layouts().registry()&lt;/code>&lt;/li>
&lt;li>&lt;code>session.allows_unknown()&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>问题就藏在这两个参数的求值过程中。&lt;/p>
&lt;p>先看 &lt;code>session.layouts()&lt;/code>。它来自 &lt;code>vortex-layout/src/session.rs&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fn&lt;/span> &lt;span class="nf">layouts&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="nc">Ref&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">&amp;#39;_&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LayoutSession&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">get&lt;/span>::&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">LayoutSession&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>registry()&lt;/code> 返回的是 &lt;code>&amp;amp;LayoutRegistry&lt;/code>。为了把这个引用传给 &lt;code>layout_from_flatbuffer_with_options(...)&lt;/code>，&lt;code>session.layouts()&lt;/code> 返回的 &lt;code>Ref&amp;lt;'_, LayoutSession&amp;gt;&lt;/code> 需要至少活到这次函数调用结束。这个 &lt;code>Ref&lt;/code> 内部包着 DashMap 的 read guard。&lt;/p>
&lt;p>再补一层 Rust 的求值语义：函数调用参数会按源码顺序求值。也就是说，&lt;code>session.layouts().registry()&lt;/code> 先拿到 &lt;code>LayoutSession&lt;/code> 的 guard；等右侧 &lt;code>session.allows_unknown()&lt;/code> 开始执行时，前面的 guard 还没释放。&lt;/p>
&lt;p>接着看 &lt;code>session.allows_unknown()&lt;/code>。Vortex &lt;code>0.68&lt;/code> 里的实现是：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">fn&lt;/span> &lt;span class="nf">allows_unknown&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="kt">bool&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="bp">Self&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">as&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SessionExt&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>::&lt;span class="n">get&lt;/span>::&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">UnknownPluginPolicy&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="n">allow_unknown&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这里的 &lt;code>get::&amp;lt;V&amp;gt;()&lt;/code> 不是纯读。session var 不存在时，它会懒插入默认值：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Some&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">v&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mf">0.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">TypeId&lt;/span>::&lt;span class="n">of&lt;/span>::&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">V&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">())&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Ref&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">Ref&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">entry&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">TypeId&lt;/span>::&lt;span class="n">of&lt;/span>::&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">V&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">())&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">or_insert_with&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Box&lt;/span>::&lt;span class="n">new&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">V&lt;/span>::&lt;span class="n">default&lt;/span>&lt;span class="p">()))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">downgrade&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">.))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>self.0&lt;/code> 是 &lt;code>DashMap&amp;lt;TypeId, Box&amp;lt;dyn SessionVar&amp;gt;, ...&amp;gt;&lt;/code>。所以 &lt;code>allows_unknown()&lt;/code> 这个名字虽然像读操作，但当 &lt;code>UnknownPluginPolicy&lt;/code> 还没初始化时，它会走到 &lt;code>entry(...).or_insert_with(...)&lt;/code>，需要拿对应 shard 的写锁。&lt;/p>
&lt;p>这也解释了为什么受限 runner 或 &lt;code>taskset -c 0&lt;/code> 更容易复现。DashMap 的默认 shard 数量来自 &lt;code>(available_parallelism * 4).next_power_of_two()&lt;/code>；可用 CPU 越少，默认 shard 越少，&lt;code>LayoutSession&lt;/code> 和 &lt;code>UnknownPluginPolicy&lt;/code> 落到同一个 shard 的概率越高。资源约束放大的是概率，不是根因。根因仍然是 footer 解析期间，一个看似只读的 session 查询走进了写路径。&lt;/p>
&lt;p>整个链路可以画成这样：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-mermaid" data-lang="mermaid">flowchart TD
A[&amp;#34;paimon-rust opens a Vortex file&amp;#34;] --&amp;gt; B[&amp;#34;Vortex 0.68 reads the footer&amp;#34;]
B --&amp;gt; C[&amp;#34;Build root_layout arguments&amp;#34;]
C --&amp;gt; D[&amp;#34;Evaluate session.layouts().registry() first&amp;#34;]
D --&amp;gt; E[&amp;#34;SessionExt::get::&amp;lt;LayoutSession&amp;gt;()&amp;#34;]
E --&amp;gt; F[&amp;#34;Hold a DashMap shard read guard&amp;#34;]
C --&amp;gt; G[&amp;#34;Then evaluate session.allows_unknown()&amp;#34;]
G --&amp;gt; H[&amp;#34;SessionExt::get::&amp;lt;UnknownPluginPolicy&amp;gt;()&amp;#34;]
H --&amp;gt; I{&amp;#34;UnknownPluginPolicy exists?&amp;#34;}
I -- &amp;#34;No&amp;#34; --&amp;gt; J[&amp;#34;entry(...).or_insert_with(default)&amp;#34;]
J --&amp;gt; K[&amp;#34;Needs a write guard for the shard&amp;#34;]
F --&amp;gt; L{&amp;#34;Same shard?&amp;#34;}
K --&amp;gt; L
L -- &amp;#34;Yes&amp;#34; --&amp;gt; M[&amp;#34;Same thread waits while holding the read guard&amp;#34;]
M --&amp;gt; N[&amp;#34;Self-deadlock&amp;#34;]
L -- &amp;#34;No&amp;#34; --&amp;gt; O[&amp;#34;This run does not hang&amp;#34;]
&lt;/code>&lt;/pre>&lt;p>所以这里不是普通的多线程竞态。一条线程就够了：它先持有一个 shard 的 read guard，又在同一个调用语句里尝试拿同一个 shard 的 write guard，于是把自己等死。表现出来就是没有 panic、没有错误返回，测试进程安静地卡在那里。&lt;/p>
&lt;h2 id="上游代码的印证">上游代码的印证&lt;/h2>
&lt;p>Vortex 的历史 PR 可以把这条链路串起来。&lt;/p>
&lt;p>先看 &lt;a href="https://github.com/vortex-data/vortex/pull/7347">vortex-data/vortex#7347&lt;/a>（&lt;code>Allow loading &amp;quot;foreign&amp;quot; plugins for UI/TUI/Serde&lt;/code>）。这个 PR 在 2026-04-09 合入，做了两件和本问题相关的事：&lt;/p>
&lt;ol>
&lt;li>在 &lt;code>vortex-session/src/lib.rs&lt;/code> 中新增 &lt;code>UnknownPluginPolicy&lt;/code>、&lt;code>allow_unknown()&lt;/code> 和 &lt;code>allows_unknown()&lt;/code>。&lt;/li>
&lt;li>在 &lt;code>vortex-file/src/footer/mod.rs&lt;/code> 中把 footer layout 解析改成 &lt;code>layout_from_flatbuffer_with_options(...)&lt;/code>，并传入 &lt;code>session.layouts().registry()&lt;/code> 和 &lt;code>session.allows_unknown()&lt;/code>。&lt;/li>
&lt;/ol>
&lt;p>当时 &lt;code>allows_unknown()&lt;/code> 用的是 &lt;code>get::&amp;lt;UnknownPluginPolicy&amp;gt;()&lt;/code>，因此默认 policy 不存在时会懒初始化，也就是会写 session map。&lt;/p>
&lt;p>随后 &lt;a href="https://github.com/vortex-data/vortex/pull/7606">vortex-data/vortex#7606&lt;/a>（&lt;code>fix deadlock on unknown policy request&lt;/code>）改了 &lt;code>allows_unknown()&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl"> pub fn allows_unknown(&amp;amp;self) -&amp;gt; bool {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">- &amp;lt;Self as SessionExt&amp;gt;::get::&amp;lt;UnknownPluginPolicy&amp;gt;(self).allow_unknown
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+ &amp;lt;Self as SessionExt&amp;gt;::get_opt::&amp;lt;UnknownPluginPolicy&amp;gt;(self)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ .map(|p| p.allow_unknown)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">+ .unwrap_or(false)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gi">&lt;/span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这里要把证据边界说清楚：#7606 的触发场景来自 Vortex 自己的 FFI tests，不是 paimon-rust 的复现。但它改掉的正是同一个机制：&lt;code>allows_unknown()&lt;/code> 从懒写入变成只读查询；policy 不存在时直接返回 &lt;code>false&lt;/code>。这样 footer 解析期间就不会一边持有 &lt;code>LayoutSession&lt;/code> borrow，一边再去写 session map。&lt;/p>
&lt;p>再看当前 &lt;code>upstream/develop&lt;/code>。同一块代码后来继续演进，footer 读取仍然会把 &lt;code>session.allows_unknown()&lt;/code> 的结果传给 layout 反序列化：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">root_layout&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">layout_from_flatbuffer_with_options&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">layout_bytes&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">dtype&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">layout_read_ctx&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">array_read_ctx&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">session&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">session&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">allows_unknown&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>但 session 实现已经变成 immutable map：session variables 在构造后固定，读取时返回普通引用，不再通过 DashMap guard 管理生命周期。当前 &lt;code>allows_unknown()&lt;/code> 也是纯读：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">fn&lt;/span> &lt;span class="nf">allows_unknown&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="kt">bool&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">get_opt&lt;/span>::&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">UnknownPluginPolicy&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">is_some_and&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">allow_unknown&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这里也要区分版本：paimon-rust 最终升级到的 Vortex &lt;code>0.75.0&lt;/code> 已经包含 #7606，因此 &lt;code>allows_unknown()&lt;/code> 不再懒写入；但 &lt;code>0.75.0&lt;/code> 的 session 还没有变成 immutable map。immutable map 是当前 &lt;code>develop&lt;/code> 的进一步演进，它说明上游后来从更底层把这类读写锁生命周期问题也收掉了，但不是 paimon-rust 修复这次 CI hang 的必要条件。&lt;/p>
&lt;h2 id="解决方案">解决方案&lt;/h2>
&lt;p>对 paimon-rust 来说，合适的修复边界在依赖升级和 API 适配上。也可以在 paimon-rust 侧针对 Vortex &lt;code>0.68&lt;/code> 写 workaround，比如提前初始化 &lt;code>UnknownPluginPolicy&lt;/code>，但这会把 Vortex 内部实现细节漏到 paimon-rust 里，后续维护成本也会留在自己这边。和之信老师讨论之后，决定还是升级依赖更合适。&lt;/p>
&lt;p>Vortex &lt;code>0.75.0&lt;/code> 已经包含 #7606。同样的 footer 读取路径下，&lt;code>session.allows_unknown()&lt;/code> 不再写 session map，也就不会在持有 &lt;code>LayoutSession&lt;/code> borrow 的同时再申请写锁。&lt;/p>
&lt;p>对应 paimon-rust 的修复收敛成三部分：&lt;/p>
&lt;ol>
&lt;li>将 &lt;code>crates/paimon/Cargo.toml&lt;/code> 中的 Vortex 依赖从 &lt;code>0.68&lt;/code> 升级到 &lt;code>0.75.0&lt;/code>。&lt;/li>
&lt;li>适配新版 Vortex Arrow conversion API，用 session execution API 替代 deprecated &lt;code>IntoArrowArray::into_arrow&lt;/code>。&lt;/li>
&lt;li>将 workspace &lt;code>rust-version&lt;/code> 从 &lt;code>1.86.0&lt;/code> 提到 &lt;code>1.91.0&lt;/code>，同时把 &lt;code>constant_time_eq&lt;/code> 从 &lt;code>&amp;lt;0.5.1&lt;/code> 收紧到 &lt;code>&amp;lt;0.5.0&lt;/code>。原因是 &lt;code>constant_time_eq 0.5.0&lt;/code> 要求 Rust &lt;code>1.95.0&lt;/code>，fresh resolver 选到它以后会超过当前 workspace MSRV。&lt;/li>
&lt;/ol>
&lt;p>对应修复已经通过 &lt;a href="https://github.com/apache/paimon-rust/pull/402">&lt;code>fix(vortex): avoid footer read deadlock&lt;/code>&lt;/a> 合并。&lt;/p>
&lt;h2 id="验证">验证&lt;/h2>
&lt;p>验证分两步：先证明 hang 消失，再确认依赖升级没有破坏常规检查。&lt;/p>
&lt;p>关键回归测试还是同一个：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">timeout 120s taskset -c &lt;span class="m">0&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> cargo &lt;span class="nb">test&lt;/span> -p paimon-datafusion --features vortex --test vortex_tables -- --nocapture
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>升级到 Vortex &lt;code>0.75.0&lt;/code> 并适配 API 后，这个测试可以稳定完成，&lt;code>test_vortex_file_format_sql_e2e&lt;/code> 通过。&lt;/p>
&lt;p>paimon-rust 的修复 PR &lt;a href="https://github.com/apache/paimon-rust/pull/402">apache/paimon-rust#402&lt;/a> 于 2026-06-22 合入。对应 &lt;a href="https://github.com/apache/paimon-rust/actions/runs/27923915415">CI run 27923915415&lt;/a> 全部通过，其中 &lt;code>integration&lt;/code> job 和 &lt;code>DataFusion Vortex Integration Test&lt;/code> step 都是 &lt;code>success&lt;/code>。&lt;/p>
&lt;p>常规检查包括：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cargo fmt --all -- --check
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cargo clippy --all-targets --workspace --features fulltext,vortex,mosaic -- -D warnings
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cargo build --features fulltext,vortex,mosaic
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cargo &lt;span class="nb">test&lt;/span> -p paimon --all-targets --features fulltext,vortex,mosaic
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>其中 &lt;code>paimon&lt;/code> crate 的测试覆盖了 Vortex reader/writer 单元测试、predicate 读取、empty projection、row selection 和并发 predicate read 等路径。&lt;/p>
&lt;p>最后再强调一下 fixture 问题：依赖外部 fixture 的 integration read tests 如果缺少预置表，会以 &lt;code>TableNotExist&lt;/code> 失败。这类失败和本次 Vortex hang 是两条线，不能混成一个根因。&lt;/p>
&lt;h2 id="最新状态">最新状态&lt;/h2>
&lt;p>这篇文章写完时，相关修复已经进入 main，PR &lt;a href="https://github.com/apache/paimon-rust/pull/402">apache/paimon-rust#402&lt;/a>。合入后的 main push CI 是 &lt;a href="https://github.com/apache/paimon-rust/actions/runs/27925299942">run 27925299942&lt;/a>，结果为 &lt;code>success&lt;/code>；其中 &lt;code>DataFusion Vortex Integration Test&lt;/code> 也能够正常跑通。&lt;/p>
&lt;p>目前来看 paimon-rust 的 PR 可以发现已经可以正常运行，没有再出现 Vortex test 夯住的情况了。想要参与的同学可以一起来愉快地玩耍啦~&lt;/p>
&lt;h2 id="结论">结论&lt;/h2>
&lt;p>啰里八唆写了一堆，简单总结下这次 CI hang 的根因，是 Vortex &lt;code>0.68&lt;/code> 的 session 读取接口里藏了一条写路径：&lt;/p>
&lt;ul>
&lt;li>footer 解析时，先通过 &lt;code>session.layouts().registry()&lt;/code> 保留了 &lt;code>LayoutSession&lt;/code> 的 session borrow；&lt;/li>
&lt;li>同一个函数调用里，又继续执行 &lt;code>session.allows_unknown()&lt;/code>；&lt;/li>
&lt;li>Vortex &lt;code>0.68&lt;/code> 的 &lt;code>allows_unknown()&lt;/code> 会通过 &lt;code>get::&amp;lt;UnknownPluginPolicy&amp;gt;()&lt;/code> 懒插入默认 policy；&lt;/li>
&lt;li>如果这次写入和前面尚未释放的 &lt;code>LayoutSession&lt;/code> borrow 落到同一个 DashMap shard，同一线程就会在持有 read guard 的情况下等待 write guard，形成自死锁。&lt;/li>
&lt;/ul>
&lt;p>Vortex 上游已经通过 &lt;a href="https://github.com/vortex-data/vortex/pull/7606">vortex-data/vortex#7606&lt;/a> 把 &lt;code>allows_unknown()&lt;/code> 改成 &lt;code>get_opt()&lt;/code>，让默认 &lt;code>false&lt;/code> 变成真正的只读路径。paimon-rust 侧升级到包含这个修复的 Vortex 版本，并完成必要 API 适配，是边界最清楚的修法。&lt;/p></content></entry><entry><title type="text">浅谈 Paimon 的 BinaryRow 设计哲学</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/paimon/paimon001_binaryrow/"/><id>https://quakewang.github.io/tech/paimon/paimon001_binaryrow/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2026-05-22T22:30:00+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h2 id="分区表读取任务">分区表读取任务&lt;/h2>
&lt;p>好久没有写技术博客了，尤其是 AI 泛滥之后，总觉得写技术博客的价值越来越少。不过前段时间，重新回到开源社区，做了让 paimon-rust 支持读取分区表（&lt;a href="https://github.com/apache/paimon-rust/issues/124">Issue #124&lt;/a>）的任务。在完成这个 issue 的过程中，还是学到不少知识的，就随手写写吧。&lt;/p>……</summary><content type="html">&lt;h2 id="分区表读取任务">分区表读取任务&lt;/h2>
&lt;p>好久没有写技术博客了，尤其是 AI 泛滥之后，总觉得写技术博客的价值越来越少。不过前段时间，重新回到开源社区，做了让 paimon-rust 支持读取分区表（&lt;a href="https://github.com/apache/paimon-rust/issues/124">Issue #124&lt;/a>）的任务。在完成这个 issue 的过程中，还是学到不少知识的，就随手写写吧。&lt;/p>
&lt;p>这个任务有一条清晰的依赖链：要读分区表，首先要能解析 Manifest 中的分区信息；而分区信息在 Manifest 的 &lt;code>_PARTITION&lt;/code> 字段中以 Avro &lt;code>bytes&lt;/code> 存储。对 paimon-rust 来说，这段 bytes 是 Paimon 的 serialized BinaryRow：前 4 个字节是 big-endian 的 arity，后面才是 BinaryRow 的原始 payload。也就是说，不搞懂 BinaryRow，分区表就读不了。&lt;/p>
&lt;p>在大模型的帮助下，读源码的门槛在大幅度降低。看了 BinaryRow 相关的代码之后，觉得这个设计值得单独聊聊——它不只是一个序列化格式，更是 Paimon 在性能和工程实践之间做出的一系列精巧 trade-off 的缩影。&lt;/p>
&lt;h2 id="binaryrow-到底要解决什么问题">BinaryRow 到底要解决什么问题&lt;/h2>
&lt;p>要理解 BinaryRow，得先知道它替代了什么。&lt;/p>
&lt;p>Paimon 内部有一个 &lt;code>InternalRow&lt;/code> 接口，用来表示&amp;quot;一行数据&amp;quot;。最直觉的实现是 &lt;code>GenericRow&lt;/code>，本质上就是一个 Java Object 数组：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">GenericRow&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">implements&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">InternalRow&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Object&lt;/span>&lt;span class="o">[]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fields&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// [Integer(42), BinaryString(&amp;#34;hello&amp;#34;), null, ...]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这种实现在功能上完全够用，可以通过 &lt;code>fields[0]&lt;/code> 拿到第一个字段，&lt;code>fields[1]&lt;/code> 拿到第二个，什么类型都能存。但在数据密集的场景下——比如 Flink 每秒处理几百万行，或者一次 Compaction 要归并上亿行——GenericRow 会栽在三个地方。&lt;/p>
&lt;p>&lt;strong>第一是序列化。&lt;/strong> 数据在 shuffle、落盘、跨进程传递的时候，需要把 Object 数组变成 bytes。这意味着要遍历每个字段，判断类型，逐一编码。反过来读的时候再逐一解码。每行都要走一遍这个逻辑，CPU 开销是 O(N) 的。&lt;/p>
&lt;p>&lt;strong>第二是 GC 压力。&lt;/strong> 一行 10 个字段，就是 10 个 Java Object。如果字段是 int 或 long，还会被装箱成 Integer 和 Long。几百万行就是几千万个小对象散布在堆上，Full GC 一来整个 JVM 都要抖一下。&lt;/p>
&lt;p>&lt;strong>第三是 Cache 不友好。&lt;/strong> Object 数组存的是引用（指针），实际数据散落在堆内存的各个角落。访问一个字段需要先跳到引用地址，再读取数据——这两次内存访问大概率不在同一个 Cache Line 里，CPU 的 L1/L2 缓存几乎帮不上忙。&lt;/p>
&lt;p>BinaryRow 的思路非常直接：&lt;strong>把一行数据编码成一段连续的 bytes view&lt;/strong>。不要 Object 数组，不要装箱，不要指针跳转。概念上一行数据就是一段平坦的内存——在 Java 实现中底层是 &lt;code>MemorySegment&lt;/code>（可以指向堆内或堆外内存），不一定真的是单个 &lt;code>byte[]&lt;/code>。&lt;/p>
&lt;p>这样做的好处是一目了然的：当数据已经处在 BinaryRow 形态时，传输和落盘可以按连续 bytes 复制，避免重复做逐字段类型分派和编码；对象数量从 N 个字段的 N 个 Object 降到极少数几个引用，GC 压力显著降低；逻辑布局紧凑，相较散布在堆上的对象图更 cache-friendly，CPU prefetch 可以高效预取。&lt;/p>
&lt;p>把两者的核心差异放在一起对比：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">维度&lt;/th>
&lt;th style="text-align: left">GenericRow&lt;/th>
&lt;th style="text-align: left">BinaryRow&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">序列化&lt;/td>
&lt;td style="text-align: left">逐字段编码，O(N fields)&lt;/td>
&lt;td style="text-align: left">已编码数据可按 bytes 复制，O(byte length)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">GC 压力&lt;/td>
&lt;td style="text-align: left">N 个 Object + 装箱&lt;/td>
&lt;td style="text-align: left">极少数引用&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">Cache 友好性&lt;/td>
&lt;td style="text-align: left">指针跳转，数据散布&lt;/td>
&lt;td style="text-align: left">连续内存，可预取&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">随机访问&lt;/td>
&lt;td style="text-align: left">O(1)（数组下标）&lt;/td>
&lt;td style="text-align: left">O(1)（偏移计算）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">适用场景&lt;/td>
&lt;td style="text-align: left">构造 / 临时转换&lt;/td>
&lt;td style="text-align: left">内部高性能路径&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>从数据流的角度看，两者在 Paimon 中各有分工：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-mermaid" data-lang="mermaid">graph LR
A[&amp;#34;外部数据\n(Parquet/Avro/CSV)&amp;#34;] --&amp;gt; B[&amp;#34;GenericRow\n便捷构造 / 临时转换&amp;#34;]
B --&amp;gt; C[&amp;#34;BinaryRow\n高性能内部格式&amp;#34;]
C --&amp;gt; D[&amp;#34;Shuffle / Sort&amp;#34;]
C --&amp;gt; E[&amp;#34;Manifest 持久化\n(_PARTITION 字段)&amp;#34;]
C --&amp;gt; F[&amp;#34;比较 / Hash / 去重&amp;#34;]
style B fill:#f9f3e3,stroke:#d4a017
style C fill:#e3f2fd,stroke:#1565c0
&lt;/code>&lt;/pre>&lt;p>简单来说，GenericRow 是&amp;quot;方便的入口&amp;quot;，BinaryRow 是&amp;quot;高效的引擎&amp;quot;。在 Flink Sink 这种高吞吐路径上，数据从 Flink 侧进来时通常已经是 BinaryRow 格式（Flink 内部用同源的 BinaryRowData），不会先建一遍 GenericRow 再转。&lt;/p>
&lt;p>这个思路并不是 Paimon 首创——它直接继承自 Apache Flink 的 &lt;code>BinaryRowData&lt;/code>，而 Flink 的设计又受到了数据库系统中 &amp;quot;tuple-at-a-time&amp;quot; vs &amp;quot;column-at-a-time&amp;quot; 这类经典讨论的影响。Paimon 从 Flink 独立出来之后保留了这套格式，因为它确实好用。&lt;/p>
&lt;h2 id="内存布局">内存布局&lt;/h2>
&lt;p>说了这么多好处，那 BinaryRow 的 bytes 里面到底是什么样的？一段连续内存里怎么找到第 N 个字段？&lt;/p>
&lt;p>先看去掉 arity 前缀之后的 raw BinaryRow payload。它的内存分为两大块：固定长度部分和变长部分。&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-mermaid" data-lang="mermaid">flowchart TB
subgraph Fixed[&amp;#34;Fixed-Length Part&amp;#34;]
direction LR
HN[&amp;#34;Header + Null Bits\n(8-byte aligned)\nRowKind + 1 bit per field&amp;#34;]
F[&amp;#34;Field Values\n(8 bytes × arity)\n定长类型 inline\n变长类型存 offset|length&amp;#34;]
HN --- F
end
subgraph Var[&amp;#34;Variable-Length Part&amp;#34;]
direction LR
V0[&amp;#34;var data 0&amp;#34;] --- V1[&amp;#34;var data 1&amp;#34;] --- V2[&amp;#34;var data 2&amp;#34;] --- VN[&amp;#34;...&amp;#34;]
end
Fixed --&amp;gt; Var
style HN fill:#fff3e0,stroke:#e65100
style F fill:#e3f2fd,stroke:#1565c0
style Var fill:#fce4ec,stroke:#c62828
&lt;/code>&lt;/pre>&lt;p>&lt;strong>Header + Null Bits&lt;/strong> 共同占据 raw payload 的开头区域，并按 8 字节对齐。逻辑上的 Header 只用第 0 个字节存 RowKind——这行数据是新增（INSERT）、更新前（UPDATE_BEFORE）、更新后（UPDATE_AFTER）还是删除（DELETE）。这个信息在 changelog 语义和 Compaction 归并时至关重要。&lt;/p>
&lt;p>&lt;strong>Null Bits&lt;/strong> 从 bit 8 开始，用 1 个 bit 表示一个字段是否为 null。bit 为 1 就是 null，为 0 就是有值。这个开头区域的宽度计算公式是 &lt;code>((arity + 63 + 8) / 64) * 8&lt;/code>。拆解一下：&lt;code>+8&lt;/code> 是因为 RowKind 占了前 8 个 bit，null 位从第 8 个 bit 开始计数；&lt;code>+63&lt;/code> 配合整除 64 实现&lt;strong>向上取整到 8 字节边界&lt;/strong>。在当前 Rust 实现里，源码常量名是 &lt;code>HEADER_SIZE_IN_BYTES = 8&lt;/code>，它参与 bit offset 和 bitset width 的计算，最终得到这个对齐后的 header/null-bits 区域宽度。&lt;/p>
&lt;p>&lt;strong>Field Values&lt;/strong> 是最关键的部分。不管字段实际是什么类型，&lt;strong>每个字段固定占 8 个字节&lt;/strong>。int 占 4 字节？也给它分 8 字节。boolean 只要 1 字节？也是 8 字节。这看起来有点浪费，但换来了一个极其重要的性质：&lt;strong>第 N 个字段的偏移位置可以用一次乘法算出来&lt;/strong>（&lt;code>nullBitSetSize + N × 8&lt;/code>），不需要遍历前面的字段。O(1) 的随机访问能力，在排序、比较、hash 等操作中价值巨大。&lt;/p>
&lt;p>&lt;strong>Variable-Length Part&lt;/strong> 放在最后面，存那些在 8 个字节里装不下的数据——比如超过 7 字节的字符串、大 DECIMAL、嵌套的 Array/Map/Row 等。固定长度部分里对应的 8 字节 slot 存的是一个&amp;quot;指针&amp;quot;：高 32 位是 offset（数据在 variable part 中的起始位置），低 32 位是 length（数据长度）。&lt;/p>
&lt;h3 id="举个">举个🌰&lt;/h3>
&lt;p>假设有一个分区表的分区 schema 是 &lt;code>(region STRING, year INT, city STRING)&lt;/code>，写入一行 &lt;code>(&amp;quot;cn&amp;quot;, 2024, &amp;quot;shanghai&amp;quot;)&lt;/code>。arity = 3，RowKind = INSERT。&lt;/p>
&lt;p>先算各区域大小。Header + null bits 宽度 = &lt;code>((3 + 63 + 8) / 64) × 8 = 8&lt;/code> 字节。固定长度部分 = 8（header + null bits）+ 8 × 3（三个字段）= 32 字节。Variable part 需要存 &amp;quot;shanghai&amp;quot;（8 字节，超过了 7 字节阈值），而 &amp;quot;cn&amp;quot; 只有 2 字节，不需要放到 variable part。&lt;/p>
&lt;p>完整的内存布局如下（小端序，按逐字节 dump 展示）：&lt;/p>
&lt;pre tabindex="0">&lt;code>地址 字节值 说明
────── ────────────── ────────────────────────────────────
── Header + Null Bit Set（8 bytes）──
[0] 00 Header: RowKind = INSERT
[1] 00 Null bits：全 0，三个字段都非 null
[2-7] 00 00 00 00 padding 至 8 字节对齐
00 00
── Field 0: region = &amp;#34;cn&amp;#34;（inline）──
[8] 63 &amp;#39;c&amp;#39;
[9] 6E &amp;#39;n&amp;#39;
[10-14] 00 00 00 00 00 padding
[15] 82 0x80 | 2 = 最高 bit 标记 inline，长度 = 2
── Field 1: year = 2024（i32）──
[16-19] E8 07 00 00 2024 的小端序表示（i32 只占 4 字节）
[20-23] 00 00 00 00 高位补零
── Field 2: city = &amp;#34;shanghai&amp;#34;（引用 variable part）──
[24-27] 08 00 00 00 length = 8（低 32 位，小端 i32）
[28-31] 20 00 00 00 offset = 32（高 32 位，小端 i32，variable part 起始位置）
── Variable-Length Part ──
[32-39] 73 68 61 6E &amp;#34;shanghai&amp;#34; 实际字符串数据
67 68 61 69
Total: 40 bytes
&lt;/code>&lt;/pre>&lt;p>整行数据，40 个字节，连续排列在一段内存中。读取第 1 个字段（year）的逻辑是：算出偏移 &lt;code>8 + 1 × 8 = 16&lt;/code>，读 &lt;code>data[16..20]&lt;/code> 转成 i32 = 2024。没有任何指针跳转，没有任何类型判断，一次内存访问就拿到了值。&lt;/p>
&lt;h2 id="设计上的-trade-off">设计上的 trade-off&lt;/h2>
&lt;p>上面的例子中，&amp;quot;cn&amp;quot; 和 &amp;quot;shanghai&amp;quot; 虽然都是 STRING 类型，但编码方式完全不同。这是 BinaryRow 最精巧的设计之一——对变长字段有两种编码策略。&lt;/p>
&lt;p>&lt;strong>策略 A：短数据直接塞进 8 字节 slot（inline）。&lt;/strong> 如果一个字符串或 binary 数据不超过 7 个字节，它可以直接存在固定长度部分的 8 字节 slot 里。编码规则：把 8 字节 slot 视为一个 64-bit word，&lt;strong>最高字节&lt;/strong>（big-endian 意义上的最高位字节）用于存储标记和长度——最高 bit 置 1 表示“这是 inline 数据”，低 7 bits 存数据长度（0~127）；剩下的 7 个字节存实际数据。&lt;/p>
&lt;p>在 little-endian 内存中，这个“最高字节”落在 8-byte slot 的最后一个地址——上面示例中 &lt;code>[15]&lt;/code> 位置的 &lt;code>0x82&lt;/code> 就是它。源码在读取时会显式处理字节序（参见 &lt;code>MemorySegmentUtils&lt;/code> 中的 byte order 逻辑）。以 &lt;code>&amp;quot;cn&amp;quot;&lt;/code> 为例：数据只有 2 字节，标记字节 = &lt;code>0x80 | 2 = 0x82&lt;/code>，slot 的低 2 字节存了 &lt;code>'c'&lt;/code> 和 &lt;code>'n'&lt;/code>。&lt;/p>
&lt;p>&lt;strong>策略 B：长数据放到 variable part，slot 里存&amp;quot;指针&amp;quot;（引用）。&lt;/strong> 超过 7 字节的数据放不进 slot，就存到 variable-length part 里，slot 中存 offset 和 length。最高 bit 为 0，表示这是一个引用而不是 inline 数据。&lt;/p>
&lt;p>读取的时候怎么区分？看最高 bit。如果是 1，数据就在 slot 里，直接读；如果是 0，按 offset+length 去 variable part 取。当前 Rust 实现里，按 &lt;code>Datum&lt;/code> 写入的路径会对 &lt;code>len &amp;lt;= 7&lt;/code> 的 string/binary 自动选择 inline；底层的 &lt;code>write_string&lt;/code> / &lt;code>write_binary&lt;/code> 仍然是显式写 variable part 的 API。整个决策过程如下：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-mermaid" data-lang="mermaid">flowchart TD
A[&amp;#34;读取变长字段的 8B slot&amp;#34;] --&amp;gt; B{&amp;#34;最高 bit = ?&amp;#34;}
B --&amp;gt;|&amp;#34;1 (inline)&amp;#34;| C[&amp;#34;从 slot 低 7 字节\n直接读取数据&amp;#34;]
B --&amp;gt;|&amp;#34;0 (引用)&amp;#34;| D[&amp;#34;取高 32 位 = offset\n取低 32 位 = length&amp;#34;]
D --&amp;gt; E[&amp;#34;跳到 Variable Part\n读取 data 中 offset..offset+length&amp;#34;]
C --&amp;gt; F[&amp;#34;返回数据&amp;#34;]
E --&amp;gt; F
style C fill:#e8f5e9,stroke:#2e7d32
style E fill:#fff3e0,stroke:#e65100
&lt;/code>&lt;/pre>&lt;p>只需要一次位运算就能决定走哪条路径。&lt;/p>
&lt;p>这个设计背后的 trade-off 很有意思。inline 策略牺牲了 1 bit 的表示空间（最高 bit 被占用了），但换来的是：&lt;strong>对于短字符串，一次内存访问就能拿到数据，不需要跳到 variable part&lt;/strong>。在实际业务中，分区字段的值往往很短——日期 &amp;quot;2024-01-15&amp;quot; 是 10 字节（刚好超过 7 字节，走引用），但如果分区是按小时 &amp;quot;00&amp;quot;~&amp;quot;23&amp;quot; 只有 2 字节，或者地区缩写 &amp;quot;us&amp;quot;、&amp;quot;cn&amp;quot; 也只有 2 字节，都能享受 inline 的收益。&lt;/p>
&lt;p>这 7 字节的阈值也不是随便选的。一个 slot 8 字节，最高字节的 1 bit 做标记、7 bit 存长度（0~127），剩下正好 7 个字节放数据。刚好把 8 字节用满，没有任何浪费。&lt;/p>
&lt;h2 id="设计哲学的几个提炼">设计哲学的几个提炼&lt;/h2>
&lt;p>回过头看 BinaryRow 的整体设计，有几个反复出现的思路值得提炼。&lt;/p>
&lt;h3 id="用空间换-o1-随机访问">用空间换 O(1) 随机访问&lt;/h3>
&lt;p>每字段固定 8 字节是最典型的例子。boolean 只需要 1 字节，给它分 8 字节确实浪费了 7 字节。但换来的 O(1) 随机访问能力让后续所有操作——排序时比较第 K 列、hash 时定位到主键列——都变成了常数时间的偏移计算，不需要像 Protocol Buffer 那样逐字段解析 varint 来找到目标字段。&lt;/p>
&lt;p>举个具体的数字来感受：假设一行有 20 个字段，你要读取第 19 个字段。BinaryRow 的计算是 &lt;code>8 + 19 × 8 = 160&lt;/code>，一次乘法一次加法，直接跳到目标偏移。而 Protobuf 需要从第 1 个字段开始，逐个解析 tag 和 varint length，跳过前 18 个字段之后才能到达第 19 个。当你在 Compaction 中对上亿行做归并排序，每行都要反复访问主键列时，在频繁随机访问场景下两者会有显著的性能差距。&lt;/p>
&lt;p>在数据密集的系统中，可预测的性能往往比极致的压缩率更重要。宁可多占一些内存，也不希望某个字段因为前面有一个超长字符串，访问延迟突然翻了好几倍。&lt;/p>
&lt;h3 id="让热路径做最少的事">让热路径做最少的事&lt;/h3>
&lt;p>&lt;strong>inline 编码就是这个思路的体现。&lt;/strong> 分区值、主键值是被读取频率最高的字段——每次 scan 都要读分区值来决定路径，每次 compaction 都要读主键来做归并。把这些高频访问的短数据直接放在 fixed part 里，省掉一次间接寻址，积累起来就是可观的性能差异。&lt;/p>
&lt;p>算一笔账就能感受到这个&amp;quot;积累&amp;quot;有多大：一个表有 1000 个分区、每个分区 100 个数据文件，一次全表 scan 要读 10 万条 ManifestEntry，每条都要取 &lt;code>_PARTITION&lt;/code> 中的分区值。如果分区字段是 &lt;code>hr='08'&lt;/code>（2 字节，走 inline），这 10 万次取值每次都是直接从 fixed part 的 slot 里读取，省掉了 10 万次跳到 variable part 的间接寻址。在 Manifest 解码、分区提取、排序比较这类 CPU 热路径里，少一次内存间接访问的收益会被放大到非常可观的程度。&lt;/p>
&lt;h3 id="二进制层面的可比较性">二进制层面的可比较性&lt;/h3>
&lt;p>BinaryRow 的 &lt;code>equals()&lt;/code> 和 &lt;code>hashCode()&lt;/code> 直接在 bytes 层面操作——比较两行是否相等就是 memcmp，计算 hash 就是对 bytes 做 hash。不需要逐字段拆箱、比较、再装箱。这在 hash join、hash aggregate、去重等场景中的收益是巨大的。&lt;/p>
&lt;p>但这也带来了一个隐含的约束：&lt;strong>如果两个 BinaryRow 表示相同的数据，它们的 bytes 必须完全一致&lt;/strong>。这个约束在处理 null 字段时尤为关键。如果一个字段被设为 null，仅仅设置 null bit 是不够的——slot 里可能还残留着之前的脏数据。虽然业务逻辑上这个值不会被读取，但 memcmp 会比较到它。所以 Java 源码里 &lt;code>setNullAt&lt;/code> 不仅设置 null bit，还会把对应的 8 字节 slot 清零。逻辑上可以概括为：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">setNullAt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">bitSet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HEADER_SIZE_IN_BITS&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// 设置 null bit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">segment&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">putLong&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">getFieldOffset&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">0&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// 清零 slot，保证 memcmp 一致性&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>看起来多写了一行代码，但保住了整个 equals/hash 语义的正确性。&lt;/p>
&lt;h2 id="从-java-到-rust跨语言实现的思考">从 Java 到 Rust：跨语言实现的思考&lt;/h2>
&lt;p>参与 paimon-rust 的开发过程中，我对 BinaryRow 做了 Java → Rust 的移植（&lt;a href="https://github.com/apache/paimon-rust/pull/133">PR #133&lt;/a>）。这个过程中有一些有趣的发现。&lt;/p>
&lt;p>&lt;strong>有些 Java 层面的复杂性在 Rust 中可以直接消失。&lt;/strong> 最明显的是 &lt;code>MemorySegment&lt;/code>。Java Paimon 的 BinaryRow 不直接操作 &lt;code>byte[]&lt;/code>，而是通过 &lt;code>MemorySegment&lt;/code> 抽象访问——这层抽象是为了支持堆外内存（off-heap），规避 GC。&lt;code>BinarySection&lt;/code>（BinaryRow 的基类）实际上持有 &lt;code>MemorySegment[]&lt;/code> 引用，一个 BinaryRow 可以跨多个 segment 存储。但在当前的 Rust 实现里，不需要引入 Java 那样的 MemorySegment 抽象——没有 GC，&lt;code>Vec&amp;lt;u8&amp;gt;&lt;/code> 或 &lt;code>&amp;amp;[u8]&lt;/code> 本身就是高效的连续内存，直接用 slice 操作就够了。&lt;/p>
&lt;p>&lt;strong>二进制格式本身必须 bit-for-bit 一致。&lt;/strong> Paimon 的 Manifest 文件可能由 Java 端写入，Rust 端要读取。&lt;code>_PARTITION&lt;/code> 字段里的 bytes 是 serialized BinaryRow，Rust 端会先解析 4 字节 big-endian arity 前缀，再用完全相同的规则解码 raw payload——小端序、null bit 从第 8 位开始、inline 标记在最高 bit。格式兼容性是跨语言实现的生命线。&lt;/p>
&lt;p>&lt;strong>Rust 的所有权模型天然解决了 Java 的一个隐患。&lt;/strong> Java 的 BinaryRow 通过 &lt;code>pointTo(segment, offset, size)&lt;/code> 指向外部内存。它本身并不拥有底层数据，只是保存了对 &lt;code>MemorySegment[]&lt;/code> 的引用，本质上相当于一次没有类型系统约束的隐式 borrow。风险出现在底层 buffer 被复用或覆写的时候，比如流式处理中，同一段 segment 内存被下一个 batch 的数据覆盖，BinaryRow 后续读到的内容也会随之改变。对 GC 来说，这个引用依然合法，程序通常不会因为内存安全问题直接失败；真正失效的是对象和底层数据之间原本约定的生命周期关系。在 Java 里，这类风险需要通过清晰的接口约定、文档说明，以及必要时的主动拷贝来控制。&lt;/p>
&lt;p>在 Rust 中，这个问题在编译期就被解决了：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Owned：数据被 move 进 BinaryRow，生命周期没有任何歧义
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">BinaryRow&lt;/span>::&lt;span class="n">from_bytes&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">arity&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// data 的所有权转移给 row
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// 如果未来需要零拷贝读取，可以用带生命周期约束的引用：
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">row_ref&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">BinaryRowRef&lt;/span>::&lt;span class="n">from_slice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">arity&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">buffer&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">offset&lt;/span>&lt;span class="o">..&lt;/span>&lt;span class="p">]);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// 编译器保证 buffer 的生命周期 &amp;gt;= row_ref，不存在悬垂引用
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>Vec&amp;lt;u8&amp;gt;&lt;/code> 做 owned 时数据跟着 BinaryRow 走，&lt;code>&amp;amp;'a [u8]&lt;/code> 做 borrow 时编译器强制检查引用有效性。这种安全性不需要额外的运行时开销，是 Rust 类型系统带来的“免费午餐”。&lt;/p>
&lt;h2 id="binaryrow-在分区表链路中的角色">BinaryRow 在分区表链路中的角色&lt;/h2>
&lt;p>啰里八唆扯了一堆，现在回到最初的那个任务——有了 BinaryRow 的完整实现之后，分区表读取链路的最后一块拼图也就到位了。来看一下实际的数据流：&lt;/p>
&lt;p>在 Paimon 中，每个数据文件的元数据都记录在 Manifest 文件中，而 Manifest 中有一个关键字段 &lt;code>_PARTITION&lt;/code>，它的类型是 &lt;code>bytes&lt;/code>。这段 bytes 是分区值经过 BinaryRow 编码后的 serialized 结果：它包含 arity 前缀和 raw BinaryRow payload。&lt;/p>
&lt;p>比如一个按 &lt;code>(dt STRING, hr INT)&lt;/code> 分区的表，分区值 &lt;code>(&amp;quot;2024-01-15&amp;quot;, 12)&lt;/code> 会被编码成 serialized BinaryRow bytes，然后存进 Manifest 的 Avro 文件中。整条读取链路如下：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-mermaid" data-lang="mermaid">flowchart LR
S[&amp;#34;Snapshot\n(JSON)&amp;#34;] --&amp;gt; ML[&amp;#34;ManifestList\n(Avro)&amp;#34;]
ML --&amp;gt; MF[&amp;#34;ManifestFile\n(Avro)&amp;#34;]
MF --&amp;gt; ME[&amp;#34;ManifestEntry\n_PARTITION: bytes&amp;#34;]
ME --&amp;gt; BR[&amp;#34;BinaryRow::from_serialized_bytes\nget_string(0) → 2024-01-15\nget_int(1) → 12&amp;#34;]
BR --&amp;gt; PP[&amp;#34;PartitionComputer / PartitionPathUtils\ndt=2024-01-15/hr=12&amp;#34;]
PP --&amp;gt; DF[&amp;#34;读取 Parquet\n数据文件&amp;#34;]
style BR fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
style ME fill:#fff3e0,stroke:#e65100
&lt;/code>&lt;/pre>&lt;p>读取时，Rust 端需要：&lt;/p>
&lt;ol>
&lt;li>从 Avro 中取出这段 bytes&lt;/li>
&lt;li>用 &lt;code>BinaryRow::from_serialized_bytes(data)&lt;/code> 读取 arity 前缀，并还原出 BinaryRow&lt;/li>
&lt;li>调用 &lt;code>row.get_string(0)&lt;/code> 得到 &lt;code>&amp;quot;2024-01-15&amp;quot;&lt;/code>，&lt;code>row.get_int(1)&lt;/code> 得到 &lt;code>12&lt;/code>&lt;/li>
&lt;li>拼出分区路径 &lt;code>dt=2024-01-15/hr=12&lt;/code>&lt;/li>
&lt;li>用这个路径去对象存储上找到实际的 Parquet 数据文件&lt;/li>
&lt;/ol>
&lt;p>这条链路把前面提到的很多概念串了起来：Snapshot 是读取入口，ManifestList 列出所有 Manifest 文件，ManifestFile 中的 ManifestEntry 包含 &lt;code>_PARTITION&lt;/code> 的 serialized BinaryRow bytes，解码后生成分区路径，最终定位到 Parquet 数据文件。BinaryRow 就像一颗不起眼但关键的螺丝钉，拧在整个链路的中间位置。&lt;/p>
&lt;h2 id="结语">结语&lt;/h2>
&lt;p>BinaryRow 的设计其实不复杂——把一行数据编码成连续 bytes，固定宽度的 slot 保证 O(1) 访问，短数据 inline 减少间接寻址，长数据用 offset+length 引用。没有什么魔法，每个决策背后都是朴素的 trade-off 分析。&lt;/p>
&lt;p>但正是这种“每个字节都不浪费、每个 bit 都有用途”的精细设计，让 Paimon 在处理海量数据时能保持高吞吐和低延迟。在分区、主键、统计信息、bucket 分配、比较和 hash 等内部路径中，BinaryRow 承载着紧凑、可随机访问的行式表示；而在数据文件主体读取这类分析链路里，Parquet / Arrow 这样的列式格式仍然各司其职。&lt;/p>
&lt;p>不过，BinaryRow 毕竟是一个行式格式（row format）。在列式存储主导的分析型世界里——Parquet 用列式编码，Arrow 用列式内存布局——并非所有链路都适合行式表达。一个更有针对性的问题是：Paimon 内部的哪些链路应该继续保持 row-oriented（比如主键去重、changelog 生成这类天然按行操作的逻辑），而哪些链路值得做 vectorized / columnar 化（比如大批量的分析型扫描和聚合）？有机会的话，继续在下一篇展开聊聊。（bushi 又挖坑了。&lt;/p>
&lt;hr>
&lt;h2 id="延伸阅读">延伸阅读&lt;/h2>
&lt;ul>
&lt;li>Java 源码：&lt;a href="https://github.com/apache/paimon/blob/master/paimon-common/src/main/java/org/apache/paimon/data/BinaryRow.java">BinaryRow.java&lt;/a>、&lt;a href="https://github.com/apache/paimon/blob/master/paimon-common/src/main/java/org/apache/paimon/data/BinarySection.java">BinarySection.java&lt;/a>、&lt;a href="https://github.com/apache/paimon/blob/master/paimon-common/src/main/java/org/apache/paimon/data/BinaryRowWriter.java">BinaryRowWriter.java&lt;/a>&lt;/li>
&lt;li>Flink 上游同源实现：&lt;a href="https://github.com/apache/flink/blob/master/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/binary/BinaryRowData.java">BinaryRowData.java&lt;/a>（Paimon BinaryRow 直接继承自此设计）&lt;/li>
&lt;li>Rust 实现：&lt;a href="https://github.com/apache/paimon-rust/pull/133">PR #133 — BinaryRow Deserialization&lt;/a>、&lt;a href="https://github.com/apache/paimon-rust/pull/127">PR #127 — PartitionPathUtils&lt;/a>&lt;/li>
&lt;li>上游 Issue：&lt;a href="https://github.com/apache/paimon-rust/issues/124">#124 — Support reading partitioned tables&lt;/a>、&lt;a href="https://github.com/apache/paimon-rust/issues/126">#126 — BinaryRow getters&lt;/a>&lt;/li>
&lt;/ul></content></entry><entry><title type="text">Doris03 存算分离导入数据的一些杂记</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/doris/doris003_loaddata/"/><id>https://quakewang.github.io/tech/doris/doris003_loaddata/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2025-07-11T10:47:11+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="cluade-code-食用指北存算分离写入的一点杂记">Cluade Code 食用指北：存算分离写入的一点杂记&lt;/h1>
&lt;h2 id="背景">背景&lt;/h2>
&lt;p>最近接触 Doris 存算分离的场景比较多，在存算分离架构中，计算与存储资源不再是耦合在一起，通常是把数据持久化在&lt;strong>对象存储&lt;/strong>中，而非 BE 节点本地。BE 节点专注于查询计算，仅&lt;strong>缓存&lt;/strong>热数据，无需管理底层存储。这种架构实现了存储和计算资源的独立扩缩容，降低了总拥有成本，同时通过多层缓存机制保证了查询性能。&lt;/p>……</summary><content type="html">&lt;h1 id="cluade-code-食用指北存算分离写入的一点杂记">Cluade Code 食用指北：存算分离写入的一点杂记&lt;/h1>
&lt;h2 id="背景">背景&lt;/h2>
&lt;p>最近接触 Doris 存算分离的场景比较多，在存算分离架构中，计算与存储资源不再是耦合在一起，通常是把数据持久化在&lt;strong>对象存储&lt;/strong>中，而非 BE 节点本地。BE 节点专注于查询计算，仅&lt;strong>缓存&lt;/strong>热数据，无需管理底层存储。这种架构实现了存储和计算资源的独立扩缩容，降低了总拥有成本，同时通过多层缓存机制保证了查询性能。&lt;/p>
&lt;p>这么一看存算分离的优点很明确，但最近在使用的过程中，遇见一个问题，那就是数据既然是直接存在对象存储中，但为什么当有大量数据写入的时候，BE 节点的 IO 会很高，甚至达到瓶颈。大概率猜到是写入本地缓存的原因导致，但具体是先写入本地缓存，还是先写入对象存储，一直比较模糊，所以想借助 Claude Code 分析源码来一探究竟。&lt;/p>
&lt;h2 id="doris-存算分离写入流程源码分析">Doris 存算分离写入流程源码分析&lt;/h2>
&lt;p>Doris 这里我们选用最新的 3.0.6.2 版本，进行分析，打开 Terminal 输入：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 克隆 Doris 源码&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git clone https://github.com/apache/doris.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 进入源码目录&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ &lt;span class="nb">cd&lt;/span> doris
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 切换到 3.0.6.2 版本&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git checkout 3.0.6.2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># claude code 分析&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ claude
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>启动 Claude Code 之后，输入 &lt;code>/help&lt;/code> 查看帮助文档，如下图所示：&lt;/p>
&lt;p>&lt;img src="https://files.mdnice.com/user/4229/c961d05c-e5ed-469d-b50f-3ff2e24e7da2.png" alt="">&lt;/p>
&lt;p>发现还有 &lt;code>/vim&lt;/code> 命令，可以进入 vim 模式，狂喜！&lt;/p>
&lt;p>Doris 的导入方式有很多，这里我们选用 &lt;code>Stream Load&lt;/code> 的方式来看看一个完整的写入流程，输入 prompt 如下：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-prompt" data-lang="prompt">结合源码，分析 doris 存算分离模式下，完整的写入流程。务必详细准确，你要做的就是分析源码，不用过多展开延伸。使用自然段通顺连贯地表达。必要时可以给出核心代码进行说明，并展示代码来源。
&lt;/code>&lt;/pre>&lt;p>会发现 Claude Code 会把输入的“人话”解析为 TODOS，进行逐步执行，可以很直观的看见拆分的任务是否合理。&lt;/p>
&lt;p>&lt;img src="https://files.mdnice.com/user/4229/fe15ec51-2677-4c58-91ca-dafc96744326.png" alt="">&lt;/p>
&lt;blockquote>
&lt;p>下文基于 Claude Code 输出，进行整理得到。&lt;/p>
&lt;/blockquote>
&lt;h3 id="写入流程">写入流程&lt;/h3>
&lt;p>Doris 存算分离模式完整写入流程：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl"> ┌────────────────────────────────────────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ FE &lt;span class="o">(&lt;/span>Frontend&lt;span class="o">)&lt;/span> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ Stream Load │────▶ │ Transaction │────▶ │ Meta Service │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ Request │ │ Manager │ │ &lt;span class="o">(&lt;/span>Cloud&lt;span class="o">)&lt;/span> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └────────────────────────────────────────────────────────────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌────────────────────────────────────────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ BE &lt;span class="o">(&lt;/span>Backend&lt;span class="o">)&lt;/span> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │CloudStreamLoad │────▶ │StreamLoadContext│────▶ │CloudDeltaWriter │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │Executor │ │ │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ▼ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │MemTableWriter │◀─────│ MemTable │◀─────│ Data Block │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │ │ &lt;span class="o">(&lt;/span>Memory Buffer&lt;span class="o">)&lt;/span> │ │ &lt;span class="o">(&lt;/span>Incoming&lt;span class="o">)&lt;/span> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ▼ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ CloudRowset │────▶ │ CloudRowset │────▶│ Segment Writer │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ Builder │ │ Writer │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ▼ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ S3FileWriter │◀─────│ File Writer │◀─────│ Segment Data │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │ │ Creator │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └───────────│────────────────────────────────────────────────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌────────────────────────────────────────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Object Storage &lt;span class="o">(&lt;/span>S3/OSS/COS&lt;span class="o">)&lt;/span> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ .dat Files │ │ .idx Files │ │ .hdr Files │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &lt;span class="o">(&lt;/span>Data Segments&lt;span class="o">)&lt;/span> │ │ &lt;span class="o">(&lt;/span>Index Files&lt;span class="o">)&lt;/span> │ │ &lt;span class="o">(&lt;/span>Headers&lt;span class="o">)&lt;/span> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └────────────────────────────────────────────────────────────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼ &lt;span class="o">(&lt;/span>Async&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌────────────────────────────────────────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Local File Cache │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ Cache Blocks │ │ Warm Up Manager │ │ Cache Metadata │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &lt;span class="o">(&lt;/span>Hot Data&lt;span class="o">)&lt;/span> │ │ &lt;span class="o">(&lt;/span>Preload&lt;span class="o">)&lt;/span> │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └────────────────────────────────────────────────────────────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="1-写入请求处理阶段">1. 写入请求处理阶段&lt;/h4>
&lt;p>Stream Load 请求首先到达 FE，FE 分配事务 ID 并选择适当的 BE 节点。BE 端的 CloudStreamLoadExecutor 作为存算分离模式的专用执行器接管了请求，处理写入逻辑。它继承自通用的 StreamLoadExecutor，但针对云环境进行了特殊优化。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Status&lt;/span> &lt;span class="n">CloudStreamLoadExecutor&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">pre_commit_txn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">StreamLoadContext&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">ctx&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">auto&lt;/span> &lt;span class="n">st&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_exec_env&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">storage_engine&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">to_cloud&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">meta_mgr&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">precommit_txn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="n">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ok&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">LOG&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">WARNING&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="s">&amp;#34;Failed to precommit txn: &amp;#34;&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">st&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="s">&amp;#34;, &amp;#34;&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">ctx&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">brief&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">st&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ctx&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">need_rollback&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">false&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">st&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="2-数据写入内存阶段">2. 数据写入内存阶段&lt;/h4>
&lt;p>数据写入的核心环节由 CloudDeltaWriter 负责，它管理整个数据从内存到持久化的流程。数据首先进入 MemtableWriter 管理的内存缓冲区。这么设计的好处是，写入操作能够快速响应，不受磁盘 I/O 的限制。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Status&lt;/span> &lt;span class="n">CloudDeltaWriter&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">const&lt;/span> &lt;span class="n">vectorized&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">Block&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">block&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">const&lt;/span> &lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">vector&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="kt">uint32_t&lt;/span>&lt;span class="o">&amp;gt;&amp;amp;&lt;/span> &lt;span class="n">row_idxs&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">row_idxs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">empty&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="na">[[unlikely]]&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">Status&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">OK&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">lock_guard&lt;/span> &lt;span class="n">lock&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_mtx&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CHECK&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_is_init&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="n">_is_cancelled&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">SCOPED_TIMER&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_wait_flush_limit_timer&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">_memtable_writer&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">flush_running_count&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">config&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">memtable_flush_running_count_limit&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">this_thread&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">sleep_for&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">chrono&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">milliseconds&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">_memtable_writer&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">block&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">row_idxs&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>MemtableWriter 负责将数据写入 MemTable，当 MemTable 的数据达到预设阈值时，系统会触发异步 flush 操作，避免内存占用过高。可以通过在 be.conf 中配置 &lt;a href="https://github.com/apache/doris/blob/branch-3.0.6/be/src/common/config.h#L717">memtable_flush_running_count_limit&lt;/a>，&lt;code>memtable_flush_running_count_limit&lt;/code> 是控制每个 MemtableWriter 可以同时运行的最大并行 flush 任务数。根据生产经验，一般来说保持默认值就好。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Status&lt;/span> &lt;span class="n">MemTableWriter&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">const&lt;/span> &lt;span class="n">vectorized&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">Block&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">block&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">const&lt;/span> &lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">vector&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="kt">uint32_t&lt;/span>&lt;span class="o">&amp;gt;&amp;amp;&lt;/span> &lt;span class="n">row_idxs&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ... 锁定和状态检查 ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">_total_received_rows&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">row_idxs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">size&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">auto&lt;/span> &lt;span class="n">st&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_mem_table&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">insert&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">block&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">row_idxs&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="n">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ok&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="na">[[unlikely]]&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_reset_mem_table&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">st&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">UNLIKELY&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_mem_table&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">need_flush&lt;/span>&lt;span class="p">()))&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">auto&lt;/span> &lt;span class="n">s&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_flush_memtable_async&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_reset_mem_table&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">UNLIKELY&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ok&lt;/span>&lt;span class="p">()))&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">s&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">Status&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">OK&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="3-rowset-构建阶段">3. Rowset 构建阶段&lt;/h4>
&lt;p>CloudRowsetBuilder 负责初始化和构建 rowset，它会同步 tablet 的 rowset 信息，并为 MoW（Merge-on-Write）表初始化相应的上下文。这一步骤为后续的持久化操作做好准备，确保元数据的一致性。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Status&lt;/span> &lt;span class="n">CloudRowsetBuilder&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">init&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_tablet&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">DORIS_TRY&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_engine&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">get_tablet&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tablet_id&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">shared_ptr&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">MowContext&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">mow_context&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">_tablet&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">enable_unique_key_merge_on_write&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">auto&lt;/span> &lt;span class="n">st&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">static_pointer_cast&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">CloudTablet&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_tablet&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">sync_rowsets&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="n">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ok&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="n">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">is&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">ErrorCode&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">INVALID_TABLET_STATE&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">st&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">RETURN_IF_ERROR&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">init_mow_context&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mow_context&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ... 构建 rowset writer context ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">storage_resource&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_engine&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">get_storage_resource&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">storage_vault_id&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">write_file_cache&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">write_file_cache&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">file_cache_ttl_sec&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_tablet&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">ttl_seconds&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_rowset_writer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">DORIS_TRY&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_tablet&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">create_rowset_writer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">false&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">Status&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">OK&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="4-数据持久化阶段">4. 数据持久化阶段&lt;/h4>
&lt;p>CloudRowsetWriter 负责将数据写入持久化存储，它会设置远程存储资源信息，并根据是否为部分更新决定 rowset 状态。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Status&lt;/span> &lt;span class="n">CloudRowsetWriter&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">init&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">const&lt;/span> &lt;span class="n">RowsetWriterContext&lt;/span>&lt;span class="o">&amp;amp;&lt;/span> &lt;span class="n">rowset_writer_context&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_context&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">rowset_writer_context&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_rowset_meta&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">make_shared&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">RowsetMeta&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">_context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">is_local_rowset&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tablet_path&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">io&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">FileCacheFactory&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">pick_one_cache_path&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_rowset_meta&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">set_remote_storage_resource&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="n">_context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">storage_resource&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ... 设置 rowset meta 信息 ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">_context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">segment_collector&lt;/span> &lt;span class="o">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">make_shared&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">SegmentCollectorT&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">BaseBetaRowsetWriter&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">file_writer_creator&lt;/span> &lt;span class="o">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">make_shared&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">FileWriterCreatorT&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">BaseBetaRowsetWriter&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">Status&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">OK&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="5-对象存储写入阶段">5. 对象存储写入阶段&lt;/h4>
&lt;p>在对象存储写入阶段，S3FileWriter 是整个写入链路的终点，负责将数据实际写入对象存储。它支持多种写入模式，针对不同大小的文件采用不同的上传策略 — 小文件直接上传，大文件则采用分片上传。这种灵活的机制确保了数据写入的效率和可靠性。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Status&lt;/span> &lt;span class="n">S3FileWriter&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">appendv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">const&lt;/span> &lt;span class="n">Slice&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">size_t&lt;/span> &lt;span class="n">data_cnt&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="n">State&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">OPENED&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="na">[[unlikely]]&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">Status&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">InternalError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;append to closed file: {}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_obj_storage_path_opts&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">path&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">native&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size_t&lt;/span> &lt;span class="n">buffer_size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">s3_write_buffer_size&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">size_t&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">data_cnt&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">i&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ... 数据写入逻辑 ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">_pending_buf&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">get_size&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">buffer_size&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">_cur_part_num&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">RETURN_IF_ERROR&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_create_multi_upload_request&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_cur_part_num&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_countdown_event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">add_count&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">RETURN_IF_ERROR&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">FileBuffer&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">submit&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">move&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_pending_buf&lt;/span>&lt;span class="p">)));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_pending_buf&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">nullptr&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_bytes_appended&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">data_size_to_append&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">Status&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">OK&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="6-本地缓存机制">6. 本地缓存机制&lt;/h4>
&lt;p>存算分离模式的一个关键特性是支持本地缓存，通过 FileCacheAllocatorBuilder 管理缓存分配。系统会将热数据异步写入本地缓存，提高后续查询的性能。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">enable_file_cache&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">opts&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="k">nullptr&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">opts&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">write_file_cache&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_cache_builder&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">make_unique&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">FileCacheAllocatorBuilder&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">FileCacheAllocatorBuilder&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">opts&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="n">opts&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="nl">is_cold_data&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="nb">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">opts&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="n">opts&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="nl">file_cache_expiration&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">BlockFileCache&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">hash&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_obj_storage_path_opts&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">path&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">filename&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">native&lt;/span>&lt;span class="p">()),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">FileCacheFactory&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">get_by_path&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">BlockFileCache&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">hash&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_obj_storage_path_opts&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">path&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">filename&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">native&lt;/span>&lt;span class="p">()))});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="7-事务提交阶段">7. 事务提交阶段&lt;/h4>
&lt;p>最后，CloudStreamLoadExecutor 负责事务的提交。根据表类型和系统配置，它会决定是在 BE 直接提交还是通过 FE 提交。对于 MoW 表，系统采用了支持重试机制的提交策略，提高了事务的成功率。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Status&lt;/span> &lt;span class="n">CloudStreamLoadExecutor&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">commit_txn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">StreamLoadContext&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">ctx&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">is_mow_table&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">enable_stream_load_commit_txn_on_be&lt;/span> &lt;span class="o">||&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ctx&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">load_type&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">TLoadType&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">ROUTINE_LOAD&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 对于 MoW 表，通过 FE 提交，支持重试机制
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">Status&lt;/span> &lt;span class="n">st&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">int&lt;/span> &lt;span class="n">retry_times&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">retry_times&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">mow_stream_load_commit_retry_times&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">st&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">StreamLoadExecutor&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">commit_txn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ok&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="n">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">is&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">ErrorCode&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">DELETE_BITMAP_LOCK_ERROR&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">break&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">retry_times&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">stream_load_commit_retry_counter&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">st&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 直接在 BE 提交
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">auto&lt;/span> &lt;span class="n">st&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_exec_env&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">storage_engine&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">to_cloud&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">meta_mgr&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">commit_txn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">false&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="n">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ok&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">LOG&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">WARNING&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="s">&amp;#34;Failed to commit txn: &amp;#34;&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">st&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="s">&amp;#34;, &amp;#34;&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">ctx&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">brief&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">st&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ctx&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">need_rollback&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">false&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">st&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>总的来说，Doris 存算分离模式的写入流程主要是以下几个阶段：&lt;/p>
&lt;p>首先，Stream Load 请求由 FE 接收并分配事务 ID，随后交由 BE 节点的 CloudStreamLoadExecutor 处理；其次，数据通过 CloudDeltaWriter 写入 MemTable 内存缓冲区，当缓冲区达到阈值时触发异步 flush；接着，数据由 CloudRowsetBuilder 和 CloudRowsetWriter 构建并组织成持久化格式；最终通过 S3FileWriter 直接写入对象存储，然后异步写入本地缓存。&lt;/p>
&lt;p>本来写到这里以为可以结束，毕竟得到了一个比较笼统的结论：&lt;em>Doris 在写入数据的时候，会直接写入对象存储，然后异步写入本地缓存&lt;/em>。比较巧的是，这周和同事们讨论到 TSDB 相关的内容，其中就有提到“数据高吞吐实时写入”。然后就找 WayneXia（AKA 大师）聊聊，在 GreptimeDB 存算分离的模式下数据的写入流程。他们是先本地缓存，然后再写入对象存储中。&lt;/p>
&lt;h2 id="greptimedb-的写入方式">GreptimeDB 的写入方式&lt;/h2>
&lt;p>GreptimeDB 的存储引擎是基于 LSM Tree 做的优化，整个写入过程严格遵循 WAL-first 原则。数据首先被写入预写日志（WAL），确保即使在系统崩溃的情况下也能恢复数据。WAL 写入完成后，数据会被写入 Memtable，此时用户的写入请求就可以返回成功了。&lt;/p>
&lt;p>WAL 的设计基于 Raft Engine，支持批量写入以提高效率。在 &lt;code>mito2/src/worker/handle_write.rs&lt;/code> 中，可以看到系统如何处理多个区域的并发写入。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 批量写入 WAL 的核心逻辑
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">mut&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">wal_writer&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">wal&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">for&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">region_ctx&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">in&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">region_ctxs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">values_mut&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">WalOptions&lt;/span>::&lt;span class="n">Noop&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">region_ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">version&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">options&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">wal_options&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">continue&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">region_ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">add_wal_entry&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="k">mut&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">wal_writer&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这种批量处理机制大大提高了写入效率，特别是在高并发场景下。系统会收集同一时间窗口内的多个写入请求，然后批量提交给 WAL，减少了磁盘 I/O 的开销。WAL 写入成功后，数据会被并行写入到各个区域的 Memtable 中。MemTable 采用时间序列优化的数据结构，能够高效地处理按时间顺序的插入操作。当 MemTable 达到一定大小时，会触发 flush 操作，将数据持久化为 SST 文件，然后再将临时文件写入对象存储中。上传完成之后，如果这个文件要留作 cache 就会 rename 到文件系统的 cache 里面，如果不需要就会被删除。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 原子写入目录的设计
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">const&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="no">ATOMIC_WRITE_DIR&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="kt">str&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;tmp/&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">const&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="no">OLD_ATOMIC_WRITE_DIR&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="kt">str&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;.tmp/&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// WriteCachePathProvider 负责管理临时文件路径
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">impl&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FilePathProvider&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">for&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">WriteCachePathProvider&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">fn&lt;/span> &lt;span class="nf">build_sst_file_path&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">file_id&lt;/span>: &lt;span class="nc">FileId&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="nb">String&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">parquet_file_key&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IndexKey&lt;/span>::&lt;span class="n">new&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">region_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">file_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FileType&lt;/span>::&lt;span class="n">Parquet&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">file_cache&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">cache_file_path&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">parquet_file_key&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>顺便去看了 GreptimeDB 的 cache 相关的内容，他们采用了多层缓存架构来应对时序数据库的特殊需求。从源码分析来看，这套缓存系统的核心在于将读写路径完全分离，避免了传统数据库中常见的缓存污染问题。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// src/mito2/src/cache/mod.rs 中的 CacheManager 统一管理
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">struct&lt;/span> &lt;span class="nc">CacheManager&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sst_meta_cache&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">SstMetaCache&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// SST 元数据缓存
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">vector_cache&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">VectorCache&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// 向量数据缓存
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">page_cache&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">PageCache&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// 页面缓存 (读优化)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">write_cache&lt;/span>: &lt;span class="nb">Option&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">WriteCacheRef&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// 写入缓存 (写优化)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>PageCache 专门服务于读取场景，缓存频繁访问的数据页；WriteCache 则专注于写入场景，提供低延迟的写入响应。这种分离设计的背后，是对时序数据库访问模式的深刻理解。&lt;/p>
&lt;p>WriteCache 实现了一种改进的 Write-Through 模式。不同于传统的 Write-Through Cache 会立即写入后端存储，GreptimeDB 的设计更加巧妙：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// src/mito2/src/cache/write_cache.rs 中的核心逻辑
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">pub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">crate&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">async&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">fn&lt;/span> &lt;span class="nf">write_and_upload_sst&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">write_request&lt;/span>: &lt;span class="nc">SstWriteRequest&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">upload_request&lt;/span>: &lt;span class="nc">SstUploadRequest&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">write_opts&lt;/span>: &lt;span class="kp">&amp;amp;&lt;/span>&lt;span class="nc">WriteOptions&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>-&amp;gt; &lt;span class="nb">Result&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">SstInfoArray&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// 阶段一：立即写入本地缓存
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sst_info&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">write_all&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">write_request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">source&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">write_request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">max_sequence&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">write_opts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="k">await&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// 阶段二：异步上传到远程存储
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">mut&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">upload_tracker&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">UploadTracker&lt;/span>::&lt;span class="n">new&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">region_id&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">for&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sst&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">in&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">sst_info&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// 并行上传 Parquet 文件和 Puffin 索引文件
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">upload&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">parquet_key&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">parquet_path&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">remote_store&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="k">await&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sst&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">index_metadata&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">file_size&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">upload&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">puffin_key&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">puffin_path&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">remote_store&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="k">await&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nb">Ok&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sst_info&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>用户的写入请求在本地写入完成后立即返回，而后台的上传过程不会影响后续的写入操作。这样既保证了写入的低延迟，又确保了数据的最终持久性。更重要的是，WriteCache 中的数据有明确的生命周期管理。一旦数据成功上传到对象存储，本地的缓存条目就可以根据 LRU 策略被安全淘汰，而不会影响系统的数据完整性。&lt;/p>
&lt;h2 id="doris-的编码">Doris 的编码&lt;/h2>
&lt;p>后面又继续和大师聊了一会，基于上面的结论，当时想的是 Doris &lt;em>写入缓存的数据是从对象存储中拉取的&lt;/em>。如果这样的话岂不是需要在上传对象存储和异步写入 cache 都需要进行编码呢？这样设计从直觉来说有点不太合理。就又回到 Doris 这里，继续深入分析。&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-prompt" data-lang="prompt">&amp;gt; Doris 存算分离模式采用&amp;#34;先写入对象存储，再异步写入本地缓存&amp;#34;的策略，其中分别需要编码几次？是不是在上传对象存储和异步写入 cache 都需要呢？如果不是的话，cache 是从哪里写入的数据？结合 Doris 源码进行详细分析。不要脱离源码进行展开胡编乱造。
&lt;/code>&lt;/pre>&lt;p>先说结论：Doris 存算分离模式只需要编码一次，并且本地缓存写入的数据直接来源于&lt;strong>内存缓冲区&lt;/strong>，而不是重新编码。&lt;/p>
&lt;p>整个流程如下所示：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-mermaid" data-lang="mermaid">%%{
init: {
&amp;#39;theme&amp;#39;: &amp;#39;base&amp;#39;,
&amp;#39;themeVariables&amp;#39;: {
&amp;#39;primaryColor&amp;#39;: &amp;#39;#1e1e2e&amp;#39;,
&amp;#39;primaryTextColor&amp;#39;: &amp;#39;#cdd6f4&amp;#39;,
&amp;#39;primaryBorderColor&amp;#39;: &amp;#39;#89b4fa&amp;#39;,
&amp;#39;lineColor&amp;#39;: &amp;#39;#cba6f7&amp;#39;,
&amp;#39;secondaryColor&amp;#39;: &amp;#39;#313244&amp;#39;,
&amp;#39;tertiaryColor&amp;#39;: &amp;#39;#45475a&amp;#39;,
&amp;#39;background&amp;#39;: &amp;#39;#1e1e2e&amp;#39;,
&amp;#39;mainBkg&amp;#39;: &amp;#39;#181825&amp;#39;,
&amp;#39;secondBkg&amp;#39;: &amp;#39;#313244&amp;#39;,
&amp;#39;nodeBorder&amp;#39;: &amp;#39;#89b4fa&amp;#39;,
&amp;#39;clusterBkg&amp;#39;: &amp;#39;#1e1e2e&amp;#39;,
&amp;#39;clusterBorder&amp;#39;: &amp;#39;#cba6f7&amp;#39;,
&amp;#39;titleColor&amp;#39;: &amp;#39;#f5c2e7&amp;#39;,
&amp;#39;edgeLabelBackground&amp;#39;: &amp;#39;#313244&amp;#39;,
&amp;#39;textColor&amp;#39;: &amp;#39;#cdd6f4&amp;#39;,
&amp;#39;nodeBkg&amp;#39;: &amp;#39;#313244&amp;#39;
}
}
}%%
flowchart TD
subgraph SingleEncoding[&amp;#34;单次编码，双路写入&amp;#34;]
direction TB
RawData[&amp;#34;Raw Data(Original)&amp;#34;] --&amp;gt; UploadBuffer[&amp;#34;UploadFileBuffer _inner_data(One Encoding)&amp;#34;]
UploadBuffer --&amp;gt; |&amp;#34;共享内存&amp;#34;| S3Stream[&amp;#34;StringViewStream(S3 Upload)&amp;#34;]
UploadBuffer --&amp;gt; |&amp;#34;共享内存&amp;#34;| LocalCache[&amp;#34;Local Cache(Secondary)&amp;#34;]
S3Stream --&amp;gt; ObjectStorage[&amp;#34;Object Storage(Primary)&amp;#34;]
S3Stream -.-&amp;gt; |&amp;#34;指向相同内存地址&amp;#34;| MemoryBuffer[&amp;#34;_inner_data-&amp;gt;data().get_data() (Single Memory Buffer)&amp;#34;]
LocalCache -.-&amp;gt; |&amp;#34;指向相同内存地址&amp;#34;| MemoryBuffer
ObjectStorage --&amp;gt; MemoryBuffer
end
classDef default fill:#313244,stroke:#89b4fa,color:#cdd6f4
classDef memory fill:#45475a,stroke:#cba6f7,color:#f5c2e7
classDef title fill:#1e1e2e,stroke:#f5c2e7,color:#f5c2e7,font-weight:bold
class MemoryBuffer memory
class SingleEncoding title
&lt;/code>&lt;/pre>&lt;h3 id="1-单一内存缓冲区设计">1. 单一内存缓冲区设计&lt;/h3>
&lt;p>数据首先写入 UploadFileBuffer 的内存缓冲区，这是唯一的数据编码和存储位置：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Status&lt;/span> &lt;span class="n">UploadFileBuffer&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">append_data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">const&lt;/span> &lt;span class="n">Slice&lt;/span>&lt;span class="o">&amp;amp;&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">TEST_SYNC_POINT_RETURN_WITH_VALUE&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;UploadFileBuffer::append_data&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Status&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">OK&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">get_size&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 数据一次性写入内存缓冲区 _inner_data
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">memcpy&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)(&lt;/span>&lt;span class="n">_inner_data&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">get_data&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">_size&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">get_data&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">get_size&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_size&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">get_size&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_crc_value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">crc32c&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">Extend&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_crc_value&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">get_data&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">get_size&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">Status&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">OK&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="2-对象存储上传直接使用内存缓冲区">2. 对象存储上传：直接使用内存缓冲区&lt;/h3>
&lt;p>对象存储上传时，直接从内存缓冲区创建流，无需重新编码：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">void&lt;/span> &lt;span class="n">UploadFileBuffer&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">on_upload&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 直接从内存缓冲区创建 StringViewStream，无需重新编码
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">_stream_ptr&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">make_shared&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">StringViewStream&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_inner_data&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">get_data&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">_size&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">_crc_value&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="n">crc32c&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_inner_data&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">get_data&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">_size&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">DCHECK&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">false&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">set_status&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Status&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">IOError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Buffer checksum not match&amp;#34;&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 使用已编码的数据上传到 S3
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">_upload_to_remote&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="3-本地缓存写入共享内存缓冲区">3. 本地缓存写入：共享内存缓冲区&lt;/h3>
&lt;p>本地缓存写入同样直接使用内存缓冲区的数据，无需重新编码：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">void&lt;/span> &lt;span class="n">UploadFileBuffer&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">upload_to_local_file_cache&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">bool&lt;/span> &lt;span class="n">is_cancelled&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ... 检查和初始化 ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// the data is already written to S3 in this situation
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// so i didn&amp;#39;t handle the file cache write error
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">_holder&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_alloc_holder&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size_t&lt;/span> &lt;span class="n">pos&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size_t&lt;/span> &lt;span class="n">data_remain_size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_size&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">auto&lt;/span>&lt;span class="o">&amp;amp;&lt;/span> &lt;span class="nl">block&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">_holder&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">file_blocks&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ... 循环处理每个缓存块 ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">block&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">FileBlock&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">State&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">EMPTY&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">block&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">get_or_set_downloader&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">block&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">is_downloader&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 直接从内存缓冲区获取数据写入缓存，无需重新编码
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">Slice&lt;/span> &lt;span class="nf">s&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_inner_data&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">get_data&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">pos&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">append_size&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Status&lt;/span> &lt;span class="n">st&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">block&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data_remain_size&lt;/span> &lt;span class="o">-=&lt;/span> &lt;span class="n">append_size&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pos&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">append_size&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>此外，在源码的注释中，也明确说明了数据共享的设计：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// this control flow means the buf and the stream shares one memory
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// so we can directly use buf here
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">upload_to_local_file_cache&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cancelled&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>综合源码分析可见，Doris 存算分离模式采用了高效的编码和写入策略：数据仅需一次编码就写入 &lt;code>UploadFileBuffer&lt;/code> 的 &lt;code>_inner_data&lt;/code> 内存缓冲区；对象存储写入通过 &lt;code>StringViewStream&lt;/code> 零拷贝引用内存缓冲区；本地缓存写入则通过 &lt;code>Slice&lt;/code> 机制直接引用相同的内存区域。&lt;/p>
&lt;p>这种设计带来了显著的性能优化。零拷贝设计通过内存共享避免了数据拷贝，对象存储上传和缓存写入都直接引用同一块内存，大幅提升了处理效率。数据只在首次写入 UploadFileBuffer 时进行一次编码，后续所有操作都复用这份编码后的数据，减少了重复计算。这种统一数据来源的设计既保证了数据一致性，又通过内存共享和零拷贝机制最大化了性能，无需额外的编码或解码操作，为高吞吐量的数据写入提供了有力支持。&lt;/p>
&lt;h2 id="人话解读doris-写入流程">人话解读：Doris 写入流程&lt;/h2>
&lt;p>当数据进入 Doris 存算分离模式的写入组件时，首先会提交给 &lt;code>CloudDeltaWriter&lt;/code> 进行处理。数据经过 &lt;code>MemtableWriter&lt;/code> 写入 MemTable 后，当 MemTable 达到刷写条件时，会触发 &lt;code>CloudRowsetBuilder&lt;/code> 构建 rowset，进而调用 &lt;code>CloudRowsetWriter&lt;/code> 开始数据持久化过程。在这个阶段，系统会创建 &lt;code>S3FileWriter&lt;/code> 来处理实际的对象存储写入操作。&lt;/p>
&lt;p>数据在 &lt;code>S3FileWriter&lt;/code> 中首先被写入 &lt;code>UploadFileBuffer&lt;/code> 的内存缓冲区。具体来说，通过 &lt;code>append_data&lt;/code> 方法，原始数据被一次性拷贝到 &lt;code>_inner_data&lt;/code> 指向的内存空间中，这是整个流程中唯一的一次数据编码和存储操作。在这个过程中，系统会同时计算 CRC32 校验值以确保数据完整性。当缓冲区大小达到配置的阈值（通常是 &lt;code>s3_write_buffer_size&lt;/code>）时，会触发异步上传机制，如果是大文件则会进行分片上传。&lt;/p>
&lt;p>对象存储的写入过程通过 &lt;code>FileBuffer::submit&lt;/code> 方法启动，该方法会将任务提交到专门的 S3 上传线程池。在异步执行的 &lt;code>on_upload&lt;/code> 方法中，系统首先创建一个 &lt;code>StringViewStream&lt;/code> 对象，这个流对象并不拷贝数据，而是直接引用内存缓冲区中已编码的数据。接着，系统会验证 CRC32 校验值确保数据完整性，然后调用 &lt;code>_upload_to_remote&lt;/code> 方法将数据上传到 S3 等对象存储服务。&lt;/p>
&lt;p>在对象存储写入完成后，系统会继续执行本地缓存的写入操作。关键的是，缓存写入过程完全复用了内存缓冲区中的数据，通过 &lt;code>upload_to_local_file_cache&lt;/code> 方法实现。在这个方法中，系统会分配缓存块持有者（&lt;code>FileBlocksHolderPtr&lt;/code>），然后遍历每个文件缓存块。对于每个空闲的缓存块，系统会创建一个 Slice 对象，这个 Slice 直接指向内存缓冲区中的相应数据段，而不进行任何数据拷贝或重新编码。&lt;/p>
&lt;p>具体的缓存写入过程是通过计算偏移量和数据块大小来实现的。系统从内存缓冲区的起始位置开始，根据每个缓存块的容量，依次创建指向不同数据段的 Slice 对象。每个 Slice 对象都通过 &lt;code>_inner_data-&amp;gt;data().get_data() + pos&lt;/code> 的方式直接引用内存缓冲区中的特定位置，其中 pos 是当前的偏移量，append_size 是当前块需要写入的数据大小。这些 Slice 对象随后被传递给文件缓存块的 append 方法，完成实际的缓存写入操作。&lt;/p>
&lt;p>通过单次编码、内存共享和零拷贝机制，系统避免了重复的数据处理和内存分配开销。对象存储写入和缓存写入都直接访问同一块内存区域，这不仅减少了内存占用，还提高了数据一致性。同时，异步写入缓存的设计确保了主写入路径不会被缓存操作阻塞，从而保证了整体的写入性能。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>经过这两天比较深度使用 Claude Code 后，能够明显感觉到现在 AI 的强大之处。而且在 Terminal 的交互更加方便快捷。在这个时代，要做的是善用工具。虽然社会上很多声音都在说 AI 的引入，导致很多人失业，但从另一个方面来说，AI 也会促进社会的进步，让更多的时间解放出来，去做更多有意义的事情。学会与之共处，积极拥抱变化～&lt;/p>
&lt;p>通过 Claude Code 对 Doris 存算分离模式源码的深入分析，我们不仅解答了最初的疑问，还获得了对不同数据库系统架构设计的深刻理解。我们分别探索了 Doris 和 GreptimeDB 在存算分离架构下的不同写入策略，这二者分别都是 OLAP 和 TSDB 场景下的佼佼者。Doris 采用&amp;quot;先写入对象存储，再异步写入本地缓存&amp;quot;的模式，OLAP 系统更关注&lt;strong>数据可靠性和查询性能&lt;/strong>，因此优先保障数据持久化到对象存储。而且通过巧妙的内存共享和零拷贝机制，实现了数据的一次编码、双路写入，既保证了数据持久性，又提升了查询性能。而 GreptimeDB 则采用&amp;quot;先写入本地缓存，再异步上传对象存储&amp;quot;的策略，更加注重写入延迟的优化。而 TSDB 系统面对的是&lt;strong>高频时序数据写入场景&lt;/strong>，因此优先保障写入的低延迟响应。这种设计上的差异恰恰体现了系统架构设计上的 trade-off。&lt;/p>
&lt;p>通过这次源码探索，在使用的时候，有点惊艳于 Claude Code 作为源码阅读工具展现出了卓越的能力：它能够快速定位关键代码路径，理解复杂的调用关系，并将技术细节转化为清晰的流程图和自然语言描述。这种能力让我们能够在短时间内掌握陌生代码库的核心设计理念，极大提升了技术探索的效率。这也再次证明，善用工具（如 Claude Code）进行源码分析，是理解复杂系统、提升技术深度的有效途径。&lt;/p></content></entry><entry><title type="text">macOS 折腾指南——Terminal</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/macos/001-terminal/"/><id>https://quakewang.github.io/tech/macos/001-terminal/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2025-07-02T17:07:02+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="如何打造一个更顺手的-terminal">如何打造一个更顺手的 Terminal？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>记得刚入手 Mac 的时候，想的是终于能够脱离 Windows 的苦海，可以像操作 Linux 一样用 Terminal 操作电脑，毕竟直接用&lt;strong>命令行&lt;/strong>的效率要远远大于在可视化界面鼠标点击的操作。在刚接触 Linux 系统的时候，我的内心就有了 &amp;quot;&lt;em>Terminal is all&lt;/em>&amp;quot; 的念头，不过折腾一番之后，也就不了了之。最近 Claude Code 和 Gemini Cli 的爆火，让技术圈又掀起了一波文艺复兴的潮流，于是就想着重新折腾下 macOS 的工具，&lt;strong>看看在 AI 时代，如何打造一个更顺手的 Terminal&lt;/strong>，并把这个过程写成博客分享给大家。&lt;/p>……</summary><content type="html">&lt;h1 id="如何打造一个更顺手的-terminal">如何打造一个更顺手的 Terminal？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>记得刚入手 Mac 的时候，想的是终于能够脱离 Windows 的苦海，可以像操作 Linux 一样用 Terminal 操作电脑，毕竟直接用&lt;strong>命令行&lt;/strong>的效率要远远大于在可视化界面鼠标点击的操作。在刚接触 Linux 系统的时候，我的内心就有了 &amp;quot;&lt;em>Terminal is all&lt;/em>&amp;quot; 的念头，不过折腾一番之后，也就不了了之。最近 Claude Code 和 Gemini Cli 的爆火，让技术圈又掀起了一波文艺复兴的潮流，于是就想着重新折腾下 macOS 的工具，&lt;strong>看看在 AI 时代，如何打造一个更顺手的 Terminal&lt;/strong>，并把这个过程写成博客分享给大家。&lt;/p>
&lt;p>如果上网搜：&amp;quot;macOS&amp;quot;、&amp;quot;Terminal&amp;quot;这几个关键词，会发现很多文章都是推荐使用 &lt;code>iTerm2 + oh-my-zsh&lt;/code> 的组合，优点是配置比较简单，跟着教程稍微弄一下，就能够有一个不错的终端。起初我刚接触 macOS 的时候，也选择了这一套配置。但使用一段时间，但总感觉有些地方很别扭，而且启动速度不够快，外观样式也不够好看。&lt;/p>
&lt;p>于是就想换个终端，当时考虑的是 &lt;a href="https://github.com/alacritty/alacritty">Alacritty&lt;/a> 和 &lt;a href="https://github.com/kovidgoyal/kitty">Kitty&lt;/a>。这两个都是基于 GPU 加速的终端，总体来说都很不错，根据网上用户讨论，这两款终端各有优劣，总结如下：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>延迟性能&lt;/strong>：尽管 Alacritty 宣称自己是&amp;quot;最快的终端模拟器&amp;quot;（毕竟是 Rust 写的，23333🤣），但实际测试表明 Kitty 的输入延迟也很低。Alacritty 擅长的是吞吐量（处理大量文本输出的能力），而不是输入延迟。&lt;/li>
&lt;li>&lt;strong>功能丰富度&lt;/strong>：Kitty 提供了更多功能，包括标签页、窗口分割、会话管理等，而 Alacritty 则更加简洁，专注于核心功能。&lt;/li>
&lt;li>&lt;strong>字体渲染&lt;/strong>：在 Linux 上，Alacritty 的字体渲染可能更好，但在 macOS 上，Kitty 的表现通常更佳。特别是对于 Unicode 字符和双宽字符（如中日韩文字）的支持，Kitty 处理得更为优雅。&lt;/li>
&lt;li>&lt;strong>可配置性&lt;/strong>：两者都使用配置文件而非 GUI 界面进行设置。Kitty 的文档更为完善，配置选项也更多，而 Alacritty 的配置相对简单但也更受限。&lt;/li>
&lt;li>&lt;strong>特色功能&lt;/strong>：Kitty 有一些独特功能，如能够在终端中显示图片、内置的会话管理、以及 &amp;quot;kittens&amp;quot;（小型辅助程序）。&lt;/li>
&lt;/ul>
&lt;p>个人使用感受是，如果你喜欢简单、干净的终端体验，Alacritty 可能是更好的选择；而如果你需要更丰富的功能和更好的配置选项，尤其是在 macOS 上，Kitty 可能会是更合适的选择。我最终的选择是 Kitty，谁又能拒绝电脑了多一只猫咪呢。另外比较有意思的一点就是，Kitty 的维护者是一位印度开发者，这个三哥脾气比较大，有的时候提的 issue 不是他想要的，直接就给 close 了，这可能就是个人项目的偏执吧。&lt;/p>
&lt;h2 id="kitty-的美化之旅">Kitty 的美化之旅&lt;/h2>
&lt;h3 id="kitty-简介">Kitty 简介&lt;/h3>
&lt;p>Kitty 是一款现代化的终端模拟器，它的设计理念是充分利用现代计算机硬件的性能，尤其是 GPU 加速渲染，让你在日常工作中获得流畅的终端体验。与许多传统终端不同，Kitty 的每一个细节都经过精心设计，以确保&lt;strong>高性能&lt;/strong>和&lt;strong>高度可定制性&lt;/strong>共存。&lt;/p>
&lt;p>Kitty 的窗口管理采用了一种&lt;strong>层级结构&lt;/strong>，理解这一点很重要。在最顶层是操作系统窗口，而在每个操作系统窗口中，可以有多个标签页（tabs）。每个标签页又可以包含多个 Kitty 窗口（windows），它们按照你选择的布局方式排列。这种结构让你能够根据工作需要组织终端空间，无论是并排比较文件，还是监控多个进程。&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-mermaid" data-lang="mermaid">graph TD
A[Kitty Terminal] --&amp;gt; B1[tab1]
A --&amp;gt; B2[tab2]
A --&amp;gt; B3[tab3]
B1 --&amp;gt; C1[window1]
B1 --&amp;gt; C2[window2]
B1 --&amp;gt; C3[window3]
B2 --&amp;gt; D1[window1]
B2 --&amp;gt; D2[window2]
B3 --&amp;gt; E1[window1]
C1 --&amp;gt; F1[process1]
C2 --&amp;gt; F2[process2]
C3 --&amp;gt; F3[process3]
D1 --&amp;gt; F4[process4]
D2 --&amp;gt; F5[process5]
E1 --&amp;gt; F6[process6]
style A fill:#f5c2e7,stroke:#000000,stroke-width:2px,color:#000000
style B1 fill:#b4befe,stroke:#000000,stroke-width:2px,color:#000000
style B2 fill:#b4befe,stroke:#000000,stroke-width:2px,color:#000000
style B3 fill:#b4befe,stroke:#000000,stroke-width:2px,color:#000000
style C1 fill:#a6e3a1,stroke:#000000,stroke-width:2px,color:#000000
style C2 fill:#a6e3a1,stroke:#000000,stroke-width:2px,color:#000000
style C3 fill:#a6e3a1,stroke:#000000,stroke-width:2px,color:#000000
style D1 fill:#a6e3a1,stroke:#000000,stroke-width:2px,color:#000000
style D2 fill:#a6e3a1,stroke:#000000,stroke-width:2px,color:#000000
style E1 fill:#a6e3a1,stroke:#000000,stroke-width:2px,color:#000000
style F1 fill:#f9e2af,stroke:#000000,stroke-width:2px,color:#000000
style F2 fill:#f9e2af,stroke:#000000,stroke-width:2px,color:#000000
style F3 fill:#f9e2af,stroke:#000000,stroke-width:2px,color:#000000
style F4 fill:#f9e2af,stroke:#000000,stroke-width:2px,color:#000000
style F5 fill:#f9e2af,stroke:#000000,stroke-width:2px,color:#000000
style F6 fill:#f9e2af,stroke:#000000,stroke-width:2px,color:#000000
classDef rounded fill:#f5c2e7,stroke:#000000,stroke-width:2px,color:#000000,rx:10,ry:10
class A,B1,B2,B3,C1,C2,C3,D1,D2,E1,F1,F2,F3,F4,F5,F6 rounded
&lt;/code>&lt;/pre>&lt;p>布局是 Kitty 的另一个亮点，默认提供了不同的布局方案 — 从网格到堆栈，从水平到垂直排列，甚至是分割视图，而且可以在它们之间来回切换。每种布局都有其特定的用途，比如当需要专注于单个任务时可以使用堆栈布局，需要并行处理多项任务时可以使用网格布局。&lt;/p>
&lt;p>Kitty 的扩展性也值得一提。它有一个叫做 &lt;code>kittens&lt;/code> 的脚本框架，能够添加新功能或创建利用 Kitty 特性的小程序。无论是查看图像、比较文件还是输入 Unicode 字符，都可以通过 kittens 实现。如果有特定的需求，甚至可以创建自己的 kitten。&lt;/p>
&lt;p>总的来说，Kitty 不仅仅是一个终端模拟器，它更像是一个为&lt;strong>终端密集型工作&lt;/strong>精心打造的环境。它结合了速度、灵活性和功能性，让终端操作变得既高效又愉悦。&lt;/p>
&lt;h3 id="安装">安装&lt;/h3>
&lt;p>Kitty 的安装非常简单，只需要一行命令即可：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -L https://sw.kovidgoyal.net/kitty/installer.sh &lt;span class="p">|&lt;/span> sh /dev/stdin
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>如果需求想体验一些新功能的话，可以访问 &lt;a href="https://sw.kovidgoyal.net/kitty/binary/">https://sw.kovidgoyal.net/kitty/binary/&lt;/a> 下载 nightly 版本的。下载完打开之后，你会发现怎么好丑，黑不溜秋的，一点都不美观。&lt;/p>
&lt;p>&lt;img src="https://files.mdnice.com/user/4229/6cb83b7f-a936-4626-976b-6dbbdbebd8a5.png" alt="init-kitty">&lt;/p>
&lt;p>别急，我们可以在终端里面输入 &lt;code>kitten themes&lt;/code>，就跳转到 Kitty 的主题选择界面了，可以使用键盘上的 &lt;code>j&lt;/code> 和 &lt;code>k&lt;/code> 来上下移动，如果想要搜索的的话，可以使用 &lt;code>/&lt;/code> 加主题名称的关键字即可。我比较喜欢的是 &lt;code>Catppuccin Mocha&lt;/code> 主题，我的所有配色都是基于这个主题的。&lt;/p>
&lt;p>&lt;img src="https://files.mdnice.com/user/4229/07abd481-33de-4e42-a011-7ee8deafb717.png" alt="kitty-theme">&lt;/p>
&lt;p>如果找到了自己喜欢的主题，可以按下回车键，这个时候会出现对应的提示，我们输入 &lt;code>M&lt;/code> 即可，也就是 &lt;code>Modify kitty.conf to load Catppuccin-Mocha&lt;/code>。这个时候会在 &lt;code>~/.config/kitty/&lt;/code> 目录下生成一个 &lt;code>current-theme.conf&lt;/code> 文件，这个文件里面就记录了主题详细的配色信息。&lt;/p>
&lt;h3 id="配置">配置&lt;/h3>
&lt;p>完成上面的步骤，之后我们已经安装好 Kitty Terminal 并且选择了一个还不错的主题。接下来，我们就可以开始配置 Kitty 的配置文件了。&lt;/p>
&lt;p>Kitty 的配置文件是 &lt;code>~/.config/kitty/kitty.conf&lt;/code>，这个文件里面记录了 Kitty 的配置信息，默认的配置内容可以全部删除，因为接下来我们要添加一些配置，来实现一些个性化的功能。配置之后的目录结构如下：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-zsh" data-lang="zsh">&lt;span class="line">&lt;span class="cl">❯ tree ~/.config/kitty/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── keymap.py &lt;span class="c1"># 快捷键配置&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── kitty.app.icns &lt;span class="c1"># 图标&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── kitty.conf &lt;span class="c1"># 配置文件&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── themes &lt;span class="c1"># 主题文件&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── catppuccin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── frappe.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── latte.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── macchiato.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── mocha.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── window.py &lt;span class="c1"># 窗口管理&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>下面我们来逐项讲解如何配置 Kitty。&lt;/p>
&lt;h4 id="窗口管理">窗口管理&lt;/h4>
&lt;p>上文提到 Kitty 有着丰富的窗口管理机制，可与编写的 &lt;code>window.py&lt;/code> 来更好地利用这些特性，让窗口操作变得更加便捷自然。这个脚本本质上是一个 kitten，它扩展了 Kitty 的基本功能，提供了一套优雅的窗口管理解决方案。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;span class="lnt">68
&lt;/span>&lt;span class="lnt">69
&lt;/span>&lt;span class="lnt">70
&lt;/span>&lt;span class="lnt">71
&lt;/span>&lt;span class="lnt">72
&lt;/span>&lt;span class="lnt">73
&lt;/span>&lt;span class="lnt">74
&lt;/span>&lt;span class="lnt">75
&lt;/span>&lt;span class="lnt">76
&lt;/span>&lt;span class="lnt">77
&lt;/span>&lt;span class="lnt">78
&lt;/span>&lt;span class="lnt">79
&lt;/span>&lt;span class="lnt">80
&lt;/span>&lt;span class="lnt">81
&lt;/span>&lt;span class="lnt">82
&lt;/span>&lt;span class="lnt">83
&lt;/span>&lt;span class="lnt">84
&lt;/span>&lt;span class="lnt">85
&lt;/span>&lt;span class="lnt">86
&lt;/span>&lt;span class="lnt">87
&lt;/span>&lt;span class="lnt">88
&lt;/span>&lt;span class="lnt">89
&lt;/span>&lt;span class="lnt">90
&lt;/span>&lt;span class="lnt">91
&lt;/span>&lt;span class="lnt">92
&lt;/span>&lt;span class="lnt">93
&lt;/span>&lt;span class="lnt">94
&lt;/span>&lt;span class="lnt">95
&lt;/span>&lt;span class="lnt">96
&lt;/span>&lt;span class="lnt">97
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">kittens.tui.handler&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">result_handler&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">directions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;top&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;k&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;bottom&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;j&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;left&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;h&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;right&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;l&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">pass&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@result_handler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">no_ui&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">handle_result&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">answer&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">target_window_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">boss&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">window&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_window&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">window&lt;/span> &lt;span class="ow">is&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cmd&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">window&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">child&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">foreground_cmdline&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">act&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">args&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># e.g. -jump&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">act&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;-&amp;#34;&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">cmd&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">:]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;nvim&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">secound&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">directions&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">]]&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">window&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">write_to_child&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\x1b&lt;/span>&lt;span class="s2">[119;8u&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">act&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}{&lt;/span>&lt;span class="n">secound&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">direction&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">direction&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;top&amp;#34;&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">direction&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;bottom&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">launch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;--cwd=current&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--location=hsplit&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">launch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;--cwd=current&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--location=vsplit&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">direction&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;right&amp;#34;&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">direction&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;left&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">move_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">direction&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">close&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">close_window&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">quit&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">quit&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">jump&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">direction&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">neighboring_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">direction&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">resize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">direction&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">neighbors&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">current_layout&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">neighbors_for_window&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">window&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">windows&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">top&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">bottom&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">neighbors&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;top&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">neighbors&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;bottom&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">left&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">right&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">neighbors&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;left&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">neighbors&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;right&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">direction&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;top&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">top&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">bottom&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resize_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;shorter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">top&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resize_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;taller&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">bottom&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resize_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;shorter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">direction&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;bottom&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">top&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">bottom&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resize_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;taller&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">top&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resize_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;shorter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">bottom&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resize_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;taller&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">direction&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;left&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">left&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">right&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resize_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;narrower&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">left&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resize_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;wider&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">right&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resize_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;narrower&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">direction&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;right&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">left&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">right&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resize_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;wider&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">left&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resize_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;narrower&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">right&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">resize_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;wider&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">move&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">direction&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_tab&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">move_window&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">direction&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">act&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">act&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">:]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">act&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;split&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">act&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;close&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">act&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;quit&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">quit&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">act&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;jump&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">jump&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">act&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;resize&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">act&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;move&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">move&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这段代码定义了几个核心功能：窗口拆分、关闭、跳转、调整大小和移动。拆分功能（split）会根据指定的方向创建新窗口 — 如果是上下方向，它会水平分割（hsplit）；如果是左右方向，则会垂直分割（vsplit）。这种直观的设计让可以快速构建出符合当前工作需求的窗口布局，无需记忆复杂的命令。&lt;/p>
&lt;p>&lt;code>jump&lt;/code> 函数则允许在不同方向的窗口之间自如跳转，就像在一张二维网格上移动一样。而 &lt;code>resize&lt;/code> 函数则考虑了窗口的相对位置关系，可以巧妙地调整窗口大小，会检查当前窗口在各个方向上的邻居窗口，然后决定是应该增加还是减少窗口尺寸。&lt;/p>
&lt;p>说来惭愧，自己也是反反复复入坑 Neovim 好几次，所以在 Kitty 里面也考虑到了与 NeoVim 的集成。在代码开头定义了一个 directions 字典，将人类可读的方向名称（如&amp;quot;top&amp;quot;）映射到 Vim 风格的按键（如&amp;quot;k&amp;quot;）。当脚本检测到当前窗口正在运行 Neovim 时，它不会执行普通的窗口操作，而是向 Neovim 发送特殊的转义序列，让编辑器内部处理相应的命令。这种无缝集成意味着可以使用相同的快捷键在 Kitty 窗口和 Neovim 分割之间统一导航，大大减少了上下文切换的认知负担。另外，通过 &lt;code>@result_handler(no_ui=True)&lt;/code> 装饰器，创建了一个无界面的处理程序，它在后台静默工作，不会打断正在使用的工作流。当触发相应的快捷键时，脚本会分析当前环境，执行最合适的操作。&lt;/p>
&lt;p>这个窗口管理系统虽然代码不多，但却大大提升了 Kitty 的使用效率。无论是并排查看多个文件，还是监控多个进程，还是在编辑器内外无缝切换，它都提供了简单而强大的解决方案。作为用户，我们可以根据自己的工作习惯，将这些功能绑定到喜欢的快捷键上，打造真正属于自己的终端工作环境。&lt;/p>
&lt;h4 id="按键映射处理">按键映射处理&lt;/h4>
&lt;p>在终端的世界里，按键映射是个比较头疼的的难题。早期的终端协议设计时并未考虑到现代编辑器和 shell 对丰富键盘输入的需求，这导致许多组合键在传统终端中无法被正确识别或区分。于是编写 &lt;code>keymap.py&lt;/code> 脚本来解决这个历史遗留问题而生，让现代应用能够接收到更丰富的键盘输入信息。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">kittens.tui.handler&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">result_handler&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">kitty.keys&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">keyboard_mode_name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">pass&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@result_handler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">no_ui&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">handle_result&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">answer&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">target_window_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">boss&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">window&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">boss&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">active_window&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">window&lt;/span> &lt;span class="ow">is&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cmd&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">window&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">child&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">foreground_cmdline&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">args&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;C-i&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Move cursor to the end of line, specific to zsh&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">cmd&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">:]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;zsh&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">window&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">write_to_child&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\x1b&lt;/span>&lt;span class="s2">[105;5u&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Other programs that support CSI u&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">keyboard_mode_name&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">window&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">screen&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;kitty&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">window&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">write_to_child&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\x1b&lt;/span>&lt;span class="s2">[105;5u&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Otherwise send a ^I&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">window&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">write_to_child&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\x09&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">args&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;S-s&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">cmd&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">:]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;nvim&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">window&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">write_to_child&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\x1b&lt;/span>&lt;span class="s2">[115;8u&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这个脚本主要处理了两个特殊的按键组合，第一个是 &lt;code>Ctrl+i&lt;/code> 组合键，它在不同的程序中需要有不同的行为。在传统终端中，&lt;code>Ctrl+i&lt;/code> 往往与 Tab 键无法区分，因为它们发送了相同的字符码。我们的脚本通过检测当前运行的程序，为不同环境提供了合适的解决方案 — 在 zsh 中，它发送一个特殊的转义序列，使光标跳到行尾；在支持 Kitty 键盘模式的程序中，它发送现代的 CSI u 序列；而在其他程序中，它则回退到传统的 Tab 行为。这种上下文感知的处理让我们能够在不同程序间保持一致的体验。第二个处理的是 &lt;code>Shift+s&lt;/code> 组合键，专门针对 Neovim 编辑器的需求。当检测到当前窗口运行的是 Neovim 时，脚本会发送一个特殊的转义序列 &lt;code>\x1b[115;8u&lt;/code>，让 Neovim 能够识别并处理这个按键组合。这种处理方式利用了现代终端的 CSI u 协议，该协议允许传递更详细的按键信息，包括各种修饰键的状态。&lt;/p>
&lt;h4 id="终端配置">终端配置&lt;/h4>
&lt;blockquote>
&lt;p>该部分配置采用分段讲解的方式，完整的配置文件参考：&lt;a href="https://github.com/QuakeWang/dotfiles/blob/main/kitty/kitty.conf">kitty-conf&lt;/a>。&lt;/p>
&lt;/blockquote>
&lt;p>在美化终端的时候，一个好看的字体则起到非常重要的作用。在本次的配置中会使用到 &lt;code>SF Mono&lt;/code> 和 &lt;code>Symbols Nerd Font&lt;/code> 字体，可以按照下面的命令进行安装：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">brew tap homebrew/cask-fonts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">brew install --cask font-sf-mono
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">brew install --cask font-symbols-only-nerd-font
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">brew install --cask sf-symbols
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>下面是具体的配置参考，可以根据实际情况按需配置：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf">font_family SF Mono
font_size 13.0
modify_font cell_height 150%
modify_font underline_position 5
modify_font strikethrough_position 5
&lt;/code>&lt;/pre>&lt;ul>
&lt;li>&lt;strong>font_family&lt;/strong>: 使用 SF Mono 字体（macOS 系统字体）&lt;/li>
&lt;li>&lt;strong>font_size&lt;/strong>: 字体大小 13.0&lt;/li>
&lt;li>&lt;strong>cell_height&lt;/strong>: 字符高度调整为 150%，增加行间距&lt;/li>
&lt;li>&lt;strong>underline_position&lt;/strong>: 下划线位置调整&lt;/li>
&lt;li>&lt;strong>strikethrough_position&lt;/strong>: 删除线位置调整&lt;/li>
&lt;/ul>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf"># Seti-UI + Custom
symbol_map U+E5FA-U+E6AC Symbols Nerd Font
# Devicons
symbol_map U+E700-U+E7C5 Symbols Nerd Font
# Font Awesome
symbol_map U+F000-U+F2E0 Symbols Nerd Font
# Font Awesome Extension
symbol_map U+E200-U+E2A9 Symbols Nerd Font
# Material Design Icons
symbol_map U+F0001-U+F1AF0 Symbols Nerd Font
# Weather
symbol_map U+E300-U+E3E3 Symbols Nerd Font
# Octicons
symbol_map U+F400-U+F532,U+2665,U+26A1 Symbols Nerd Font
# [Powerline Symbols]
symbol_map U+E0A0-U+E0A2,U+E0B0-U+E0B3 Symbols Nerd Font
# Powerline Extra Symbols
symbol_map U+E0A3,U+E0B4-U+E0C8,U+E0CA,U+E0CC-U+E0D4 Symbols Nerd Font
# IEC Power Symbols
symbol_map U+23FB-U+23FE,U+2B58 Symbols Nerd Font
# Font Logos (Formerly Font Linux)
symbol_map U+F300-U+F32F Symbols Nerd Font
# Pomicons
symbol_map U+E000-U+E00A Symbols Nerd Font
# Codicons
symbol_map U+EA60-U+EBEB Symbols Nerd Font
# Additional sets
symbol_map U+E276C,U+2771,U+2500-U+259F Symbols Nerd Font
&lt;/code>&lt;/pre>&lt;p>这部分配置了 Nerd Font 符号映射，支持各种图标集（Devicons、Font Awesome、Material Design Icons 等），用于在终端中显示丰富的图标。&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf">map cmd+equal change_font_size all +1.0
map cmd+minus change_font_size all -1.0
map cmd+0 change_font_size all 0
&lt;/code>&lt;/pre>&lt;p>&lt;strong>字体大小&lt;/strong>设置：&lt;/p>
&lt;ul>
&lt;li>&lt;code>Cmd+Equal&lt;/code>：增加字体大小&lt;/li>
&lt;li>&lt;code>Cmd+Minus&lt;/code>：减小字体大小&lt;/li>
&lt;li>&lt;code>Cmd+0&lt;/code>：重置字体大小&lt;/li>
&lt;/ul>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf">scrollback_lines -1
scrollback_fill_enlarged_window yes
&lt;/code>&lt;/pre>&lt;ul>
&lt;li>&lt;strong>scrollback_lines&lt;/strong>: 设置为 -1 表示无限回滚历史&lt;/li>
&lt;li>&lt;strong>scrollback_fill_enlarged_window&lt;/strong>: 窗口放大时用回滚内容填充&lt;/li>
&lt;/ul>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf">url_prefixes file ftp ftps git http https mailto sftp ssh
strip_trailing_spaces always
select_by_word_characters @-./_~?&amp;amp;=%+#
&lt;/code>&lt;/pre>&lt;ul>
&lt;li>&lt;strong>url_prefixes&lt;/strong>: 定义可点击的 URL 前缀类型&lt;/li>
&lt;li>&lt;strong>strip_trailing_spaces&lt;/strong>: 总是删除行尾空格&lt;/li>
&lt;li>&lt;strong>select_by_word_characters&lt;/strong>: 定义单词选择的字符集&lt;/li>
&lt;/ul>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf">remember_window_size no
initial_window_width 1080
initial_window_height 720
enabled_layouts splits, stack
&lt;/code>&lt;/pre>&lt;ul>
&lt;li>&lt;strong>remember_window_size&lt;/strong>: 不记住窗口大小&lt;/li>
&lt;li>&lt;strong>initial_window_width/height&lt;/strong>: 初始窗口尺寸 1080x720&lt;/li>
&lt;li>&lt;strong>enabled_layouts&lt;/strong>: 启用 splits 和 stack 布局&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;ul>
&lt;li>Splits 布局是 Kitty 中最灵活的布局方式，它允许通过水平和垂直分割来创建任意复杂的窗口排列。可以先水平分割创建左右两个区域，然后在左侧区域再次垂直分割，形成上下两个子区域。这种布局特别适合需要同时查看多个相关文件或监控多个进程的场景，比如在左侧编辑代码，右侧运行测试，同时在底部查看日志输出。&lt;/li>
&lt;li>Stack 布局则是一种更专注的工作模式，它一次只显示一个窗口，其他窗口被隐藏起来。当需要全神贯注于某个任务时，这种布局可以消除视觉干扰，让专注于当前的工作。可以通过快捷键在隐藏的窗口之间切换，就像在浏览器标签页间切换一样。这种布局特别适合需要深度思考的编程任务，或者当需要在一个窗口中运行长时间的命令时。&lt;/li>
&lt;/ul>
&lt;/blockquote>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf">tar_bar_edge top
tab_bar_style powerline
tab_powerline_style round
&lt;/code>&lt;/pre>&lt;ul>
&lt;li>&lt;strong>tar_bar_edge&lt;/strong>: 标签栏位于顶部&lt;/li>
&lt;li>&lt;strong>tab_bar_style&lt;/strong>: 使用 powerline 样式&lt;/li>
&lt;li>&lt;strong>tab_powerline_style&lt;/strong>: 圆角样式&lt;/li>
&lt;/ul>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf">include themes/catppuccin/macchiato.conf
background_opacity 0.9
background_blur 30
&lt;/code>&lt;/pre>&lt;p>这里则是对&lt;strong>外观样式&lt;/strong>再优化一点，增加了透明度和毛玻璃的效果，可以把一开始下载选择的主题创建目录并替换即可。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>include&lt;/strong>: Catppuccin Macchiato&lt;/li>
&lt;li>&lt;strong>background_opacity&lt;/strong>: 背景透明度 90%&lt;/li>
&lt;li>&lt;strong>background_blur&lt;/strong>: 背景模糊 30 像素&lt;/li>
&lt;/ul>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf">macos_option_as_alt yes
macos_quit_when_last_window_closed yes
macos_show_window_title_in window
macos_colorspace displayp3
text_composition_strategy 1.2 20
&lt;/code>&lt;/pre>&lt;p>这部分是针对 &lt;strong>macOS 特定的优化&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>macos_option_as_alt&lt;/strong>: Option 键作为 Alt 键&lt;/li>
&lt;li>&lt;strong>macos_quit_when_last_window_closed&lt;/strong>: 关闭最后一个窗口时退出&lt;/li>
&lt;li>&lt;strong>macos_show_window_title_in&lt;/strong>: 在窗口内显示标题&lt;/li>
&lt;li>&lt;strong>macos_colorspace&lt;/strong>: 使用 displayp3 色彩空间&lt;/li>
&lt;li>&lt;strong>text_composition_strategy&lt;/strong>: 文本合成策略&lt;/li>
&lt;/ul>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf">map ctrl+i kitten keymap.py &amp;#34;C-i&amp;#34;
# Edit current command via neovim, specific to zsh
map ctrl+, send_text normal,application \x1b[44;5u
&lt;/code>&lt;/pre>&lt;p>&lt;strong>特殊按键&lt;/strong>处理部分解决了终端输入的历史遗留问题：&lt;/p>
&lt;ul>
&lt;li>&lt;code>ctrl+i&lt;/code> 的映射通过我们之前编写的 keymap.py 脚本处理，确保在不同程序间的一致性&lt;/li>
&lt;li>&lt;code>ctrl+,&lt;/code> 的组合则专门为 zsh 环境下的 Neovim 集成设计，发送特殊的转义序列让编辑器能够识别并处理这个按键&lt;/li>
&lt;/ul>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf">map cmd+c copy_to_clipboard
map cmd+v paste_from_clipboard
map cmd+shift+v paste_from_selection
&lt;/code>&lt;/pre>&lt;p>&lt;strong>剪贴板&lt;/strong>操作采用了 macOS 用户熟悉的快捷键模式，方便使用：&lt;/p>
&lt;ul>
&lt;li>&lt;code>cmd+c&lt;/code> 和 &lt;code>cmd+v&lt;/code>：复制和粘贴&lt;/li>
&lt;li>&lt;code>cmd+shift+v&lt;/code>：提供了从选择缓冲区粘贴的选项，这在处理临时文本时特别有用&lt;/li>
&lt;/ul>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf">map alt+k combine : scroll_line_up : scroll_line_up : scroll_line_up : scroll_line_up : scroll_line_up
map alt+j combine : scroll_line_down : scroll_line_down : scroll_line_down : scroll_line_down : scroll_line_down
map cmd+/ scroll_to_prompt -1
map cmd+shift+/ scroll_to_prompt 1
map cmd+alt+/ show_last_command_output
&lt;/code>&lt;/pre>&lt;p>&lt;strong>滚动控制&lt;/strong>的设计考虑了不同粒度的滚动需求：&lt;/p>
&lt;ul>
&lt;li>&lt;code>alt+j/k&lt;/code>：快速的多行滚动，一次滚动五行的设计让浏览长文本更加高效&lt;/li>
&lt;li>&lt;code>cmd+/&lt;/code> 和 &lt;code>cmd+shift+/&lt;/code>：智能的提示跳转功能，能够在命令历史中快速定位到上一个或下一个提示符&lt;/li>
&lt;li>&lt;code>cmd+alt+/&lt;/code>：查看最后命令输出的便捷方式&lt;/li>
&lt;/ul>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf">map cmd+ctrl+k kitten window.py +split top
map cmd+ctrl+j kitten window.py +split bottom
map cmd+ctrl+h kitten window.py +split left
map cmd+ctrl+l kitten window.py +split right
map cmd+w kitten window.py -close
map cmd+k kitten window.py -jump top
map cmd+j kitten window.py -jump bottom
map cmd+h kitten window.py -jump left
map cmd+l kitten window.py -jump right
map cmd+shift+k kitten window.py -resize top
map cmd+shift+j kitten window.py -resize bottom
map cmd+shift+h kitten window.py -resize left
map cmd+shift+l kitten window.py -resize right
map cmd+alt+k kitten window.py -move top
map cmd+alt+j kitten window.py -move bottom
map cmd+alt+h kitten window.py -move left
map cmd+alt+l kitten window.py -move right
map cmd+enter toggle_layout stack
&lt;/code>&lt;/pre>&lt;p>&lt;strong>窗口管理&lt;/strong>快捷键采用了层次化的设计，使用 &lt;code>hjkl&lt;/code> 四键作为基础导航方向。&lt;/p>
&lt;ul>
&lt;li>&lt;code>cmd+ctrl+{hjkl}&lt;/code>：在上下左右四个方向拆分窗口&lt;/li>
&lt;li>&lt;code>cmd+w&lt;/code>：关闭单个窗口&lt;/li>
&lt;li>&lt;code>cmd+{hjkl}&lt;/code>：在窗口间跳转&lt;/li>
&lt;li>&lt;code>cmd+shift+{hjkl}&lt;/code>：调整窗口大小&lt;/li>
&lt;li>&lt;code>cmd+alt+{hjkl}&lt;/code>：移动窗口位置&lt;/li>
&lt;li>&lt;code>cmd+enter&lt;/code>：切换到堆栈布局，能够在专注模式和分屏模式间自如切换&lt;/li>
&lt;/ul>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf">map cmd+t new_tab
map cmd+shift+w close_tab
map cmd+shift+t set_tab_title
map cmd+[ previous_tab
map cmd+] next_tab
map cmd+alt+[ move_tab_forward
map cmd+alt+] move_tab_backward
map cmd+1 goto_tab 1
map cmd+2 goto_tab 2
map cmd+3 goto_tab 3
map cmd+4 goto_tab 4
map cmd+5 goto_tab 5
map cmd+6 goto_tab 6
&lt;/code>&lt;/pre>&lt;p>&lt;strong>标签页管理&lt;/strong>快捷键采用了类似浏览器的操作模式：&lt;/p>
&lt;ul>
&lt;li>&lt;code>cmd+t&lt;/code>：创建新标签页&lt;/li>
&lt;li>&lt;code>cmd+shift+w&lt;/code>：关闭当前标签页&lt;/li>
&lt;li>&lt;code>cmd+shift+t&lt;/code>：设置标签页标题&lt;/li>
&lt;li>&lt;code>cmd+[&lt;/code> 和 &lt;code>cmd+]&lt;/code>：在标签页间前后导航&lt;/li>
&lt;li>&lt;code>cmd+alt+[&lt;/code> 和 &lt;code>cmd+alt+]&lt;/code>：移动标签页位置&lt;/li>
&lt;li>&lt;code>cmd+1-6&lt;/code>：直接跳转到特定标签页的快速方式&lt;/li>
&lt;/ul>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf">map cmd+o open_url_with_hints
map cmd+q kitten window.py -quit
map cmd+p kitten hints --type path --program -
map cmd+shift+p kitten hints --type path
&lt;/code>&lt;/pre>&lt;p>&lt;strong>文本交互功能&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;code>cmd+o&lt;/code>：URL 提示功能，能够快速打开终端中显示的链接&lt;/li>
&lt;li>&lt;code>cmd+q&lt;/code>：通过窗口管理脚本提供快速退出功能&lt;/li>
&lt;li>&lt;code>cmd+p&lt;/code> 和 &lt;code>cmd+shift+p&lt;/code>：路径提示功能，前者会尝试用默认程序打开选中的路径，后者则仅显示路径提示供选择&lt;/li>
&lt;/ul>
&lt;pre tabindex="0">&lt;code class="language-kitty.conf" data-lang="kitty.conf">map cmd+backspace send_text all \u17
map cmd+s kitten keymap.py &amp;#34;S-s&amp;#34;
map cmd+r clear_terminal to_cursor active
map cmd+f combine : show_scrollback : send_text normal,application /
map cmd+shift+f toggle_maximized
&lt;/code>&lt;/pre>&lt;p>&lt;strong>其他功能快捷键&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;code>cmd+backspace&lt;/code>：发送特殊的退格字符&lt;/li>
&lt;li>&lt;code>cmd+s&lt;/code>：通过按键映射脚本处理特殊按键组合&lt;/li>
&lt;li>&lt;code>cmd+r&lt;/code>：清除到光标位置的功能&lt;/li>
&lt;li>&lt;code>cmd+f&lt;/code>：结合了显示滚动缓冲区和发送搜索命令，可以快速查该 window 的内容&lt;/li>
&lt;li>&lt;code>cmd+shift+f&lt;/code>：最大化切换功能&lt;/li>
&lt;/ul>
&lt;p>终于说完了这个配置文件，如果能够耐心看完的话，相信你已经对 Kitty 的配置有了一个大致的了解。当然如果初次使用，一次性加了那么多快捷键肯定是记不住的，所以建议按需选择，比如标签页和窗口管理就很推荐。其余的配置，在使用过程可以根据需求进行调整。而且这一部分可以参考官方文档，&lt;a href="https://sw.kovidgoyal.net/kitty/conf.html">Kitty 配置&lt;/a>，进行更深入的优化。&lt;/p>
&lt;h4 id="ssh-踩坑">SSH 踩坑&lt;/h4>
&lt;p>在配置完 Kitty 之后，我尝试连接了下服务器，能够成功登录服务器，但是如果遇到命令输入错误，想要删除的时候，发现按下 &lt;code>backspace&lt;/code> 键，并没有删除而是直接变成了空格。此外在服务器上使用 &lt;code>top&lt;/code> 命令查看进程，会发现报错如下：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="s1">&amp;#39;xterm-kitty&amp;#39;&lt;/span>: unknown terminal type.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这是因为 Kitty 的终端类型没有被正确配置，导致在服务器上无法正常使用。在查阅了 Kitty 的文档后，发现 Kitty 引入了自己的 TERM 变量，而远程会话，确切地说是 terminfo 无法正确识别，即导致了上述报错。&lt;/p>
&lt;p>&lt;strong>解决方案&lt;/strong>：可以把 TERM 设置成比较通用的，比如 &lt;code>xterm-256color&lt;/code>，把它放到 &lt;code>~/.zshrc&lt;/code> 或者 &lt;code>~/.bashrc&lt;/code> 里，根据使用的 shell 而定。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-zsh" data-lang="zsh">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">TERM&lt;/span>&lt;span class="o">=&lt;/span>xterm-256color
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>另外，在 Kitty 的 &lt;a href="https://sw.kovidgoyal.net/kitty/kittens/ssh/">ssh&lt;/a> 文档中还提到：&lt;/p>
&lt;blockquote>
&lt;p>You should end up at a shell prompt on the remote host, with shell integration enabled. If you like it you can add an alias to it in your shell’s rc files:&lt;/p>
&lt;/blockquote>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-zsh" data-lang="zsh">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">alias&lt;/span> &lt;span class="nv">s&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;kitten ssh&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这么做的好处是可以增强 SSH 的体验，可以保证远程服务器和本地终端的一致性。&lt;/p>
&lt;h2 id="演示">演示&lt;/h2>
&lt;p>当完成上述配置之后，就可以打开 Kitty 多多练习熟悉这些快捷键，相信你一定会爱上这个终端的。下面是一个简单的演示图片：&lt;/p>
&lt;p>&lt;img src="https://files.mdnice.com/user/4229/3884456f-7b90-473d-957c-b1b8efd9a8ad.png" alt="">&lt;/p>
&lt;p>在终端最下面，可以观察到使用 &lt;code>cmd+t&lt;/code> 创建了五个标签页，使用 &lt;code>cmd+1&lt;/code> 跳转到了第一个标签页，使用 &lt;code>cmd+shift+t&lt;/code> 设置标签页标题为“example”。其中这个页面又被分成了三个窗口，左边部分是 &lt;code>btop&lt;/code>，右上是 &lt;code>fastfetch&lt;/code>，右下是 &lt;code>fzf&lt;/code>。并使用 &lt;code>cmd+shift+k&lt;/code> 和 &lt;code>cmd+shift+j&lt;/code> 调整了窗口大小。如果想关闭窗口，使用 &lt;code>cmd+w&lt;/code> 即可。&lt;/p>
&lt;p>诶，本篇博客重点讲解了 Kitty 的配置，但上图演示的内容又是如何实现的呢？以及开篇吐槽的 oh-my-zsh 又该如何优化呢？在后续的文章会一步一步介绍，完整实现一个好用的 macOS，敬请期待。&lt;/p>
&lt;h2 id="结尾">结尾&lt;/h2>
&lt;p>其实，这个系列的文章是我一直想写但又不太敢去写的，毕竟互联网上关于 macOS 的教程已经很多了，而且我对于 macOS 也只是停留在使用阶段，并没有太深入去学习，所以难免会有很多不足之处。在写这篇博客的时候，我也是参考了很多其他大佬的教程，并结合自己的需求进行了优化。希望不会烂尾，能够持续更新下去吧。&lt;/p>
&lt;p>但话又说回来，电脑最开始的交互方式就是没有鼠标，而是通过键盘来操作的。想想那些穿着白大褂，面对黑底绿字终端敲击键盘的工程师们，他们用最原始的方式与计算机对话，却创造了改变世界的技术。随着图形界面的普及，我们似乎渐渐忘记了这种极致高效的交互方式，沉浸在了五彩斑斓的窗口和图标中。在我们享受这些现代化工具的同时，也应该向那些早期计算机先驱致敬。是他们在没有鼠标、没有图形界面、甚至连基本的文本编辑器都十分简陋的年代，用键盘与机器对话，奠定了今天计算机科学的基础。他们用纯粹的文本命令构建了操作系统、编程语言和网络协议，这些至今仍是我们数字世界的骨架。&lt;/p>
&lt;p>而今天，在 AI 时代的浪潮下，命令行界面（CLI）又一次焕发出新的生机。大型语言模型的交互，云服务的管理，甚至是日常的开发工作，都在回归到基于文本的交互模式。这不仅仅是一种怀旧，更是对效率的追求 — 纯文本的表达往往比点击和拖拽更精准，更直接，也更易于自动化。&lt;/p>
&lt;p>读到这里，你可能会问既然是打造 AI 时代更顺手的终端，但似乎并没有太体现 AI 在哪里？哈哈哈，我承认确实有点标题党博眼球，这一点之后的配置文章会逐步引出的。现在谈到 AI 终端，大家可能第一反应就是 &lt;a href="https://www.warp.dev/">Warp&lt;/a>，但这个工具已经改变了传统终端的使用方式，而且有点重，使用一段时间会发现在磁盘中占用 1GB 多，作为一个终端工具，这个体积有点大了。相比 Kitty 则是一个轻量级的终端，配置简单，功能强大，而且支持多种主题，非常适合我这种懒人。&lt;/p>
&lt;p>&lt;img src="https://files.mdnice.com/user/4229/16f601eb-d8e8-4118-9cbc-450021af68a1.png" alt="">&lt;/p>
&lt;p>最后，我想说的是在技术发展的螺旋中，AI CLI 的出现并非是在简单地回归终端，而是在更高的层次上重新发现文本交互的力量。有一次饭后和同事散步聊天就有提到，每一次 AI 的发展都会沉淀下一些东西，这几年 LLM 的爆火，已经让传统的文字交互发生改变。在这个 AI 加持的新时代，终端或许是更好的交互方式，使得我们能够比以往任何时候都更高效、更创造性地与计算机协作，共同面对未来的挑战。&lt;/p>
&lt;blockquote>
&lt;p>参考：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.reddit.com/r/archlinux/comments/n9noje/alacritty_vs_kitty/">Alacritty vs Kitty: Which Terminal Should You Choose?&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/chancez/dotfiles/blob/master/kitty/.config/kitty/relative_resize.py">Kitty 窗口管理&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://sw.kovidgoyal.net/kitty/conf.html">Kitty 配置&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://sxyz.blog/macos-setup/">macOS Setup&lt;/a>&lt;/li>
&lt;/ul>
&lt;/blockquote></content></entry><entry><title type="text">Doris02——Oracle 数据同步</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/doris/doris002_sync-oracle2doris/"/><id>https://quakewang.github.io/tech/doris/doris002_sync-oracle2doris/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2024-10-12T18:44:33+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="oracle-同步数据至-doris">Oracle 同步数据至 Doris&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>在上篇博客中，我们掌握了如何使用 Flink-CDC 和 Doris-Flink-Connector 将 MySQL 的数据同步至 Doris，本篇博客一起来探索如何将 Oracle 的数据同步至 Doris。本次使用的主要技术栈如下：&lt;/p>……</summary><content type="html">&lt;h1 id="oracle-同步数据至-doris">Oracle 同步数据至 Doris&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>在上篇博客中，我们掌握了如何使用 Flink-CDC 和 Doris-Flink-Connector 将 MySQL 的数据同步至 Doris，本篇博客一起来探索如何将 Oracle 的数据同步至 Doris。本次使用的主要技术栈如下：&lt;/p>
&lt;ul>
&lt;li>Doris 2.1.6&lt;/li>
&lt;li>Oracle_11g&lt;/li>
&lt;li>Flink 1.18.0&lt;/li>
&lt;li>FlinkCDC 3.1.0&lt;/li>
&lt;li>Doris-Flink-Connector 24.0.0&lt;/li>
&lt;/ul>
&lt;h2 id="准备工作">准备工作&lt;/h2>
&lt;h3 id="oracle-安装并配置">Oracle 安装并配置&lt;/h3>
&lt;p>Oracle 的配置相较于 MySQL 来说会复杂一些，这里逐步来演示一下。采用的方式是使用 Docker 部署 Oracle，然后开启归档模式。&lt;/p>
&lt;h4 id="安装-oracle-11g">安装 Oracle 11g&lt;/h4>
&lt;p>拉取 Oracle 11g 的镜像：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker pull registry.cn-hangzhou.aliyuncs.com/helowin/oracle_11g
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>执行以下命令以创建并运行 Oracle 11g 容器（其中：1521 为映射主机端口，8071 为管理界面端口，helowin 为 Oracle 数据库唯一实例 ID，端口号可以根据服务器实际情况进行映射，防止冲突即可）：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -d -p 3051:1521 -p 4891:8080 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>--name oracle_cdc &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>-e &lt;span class="nv">ORACLE_HOME&lt;/span>&lt;span class="o">=&lt;/span>/home/oracle/app/oracle/product/11.2.0/dbhome_2 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>-e &lt;span class="nv">ORACLE_SID&lt;/span>&lt;span class="o">=&lt;/span>helowin &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>registry.cn-hangzhou.aliyuncs.com/helowin/oracle_11g
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这样我们就有了一个 Oracle 11g 的容器，可以使用 &lt;code>docker exec -it oracle_cdc bash&lt;/code> 进入容器内部。&lt;/p>
&lt;h4 id="配置-oracle-环境">配置 Oracle 环境&lt;/h4>
&lt;ol>
&lt;li>切换至 root 用户（默认密码为 helowin）：&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">su root
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ol start="2">
&lt;li>修改环境变量：&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">vi /etc/profile
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">## 下面的内容贴到文件末尾&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ORACLE_HOME&lt;/span>&lt;span class="o">=&lt;/span>/home/oracle/app/oracle/product/11.2.0/dbhome_2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ORACLE_SID&lt;/span>&lt;span class="o">=&lt;/span>helowin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$ORACLE_HOME&lt;/span>/bin:&lt;span class="nv">$PATH&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">## 退出保存，source 让其生效&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">source&lt;/span> /etc/profile
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ol start="3">
&lt;li>创建 sqlplus 软链接：&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ln -s &lt;span class="nv">$ORACLE_HOME&lt;/span>/bin/sqlplus /usr/bin
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>通过在 &lt;code>/usr/bin&lt;/code> 目录下创建软链接，使得 &lt;code>sqlplus&lt;/code> 命令可以在系统的任何位置被直接调用，而不需要指定完整路径，方便后续操作。&lt;/p>
&lt;h4 id="配置数据库恢复和归档日志">配置数据库恢复和归档日志&lt;/h4>
&lt;p>登录 SQL*Plus 并执行以下命令：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="n">sqlplus&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">nolog&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 以 DBA 身份登录
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">CONN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SYSDBA&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 设置数据库恢复
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SYSTEM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SET&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">DB_RECOVERY_FILE_DEST_SIZE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="k">G&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SYSTEM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SET&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">DB_RECOVERY_FILE_DEST&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;/home/oracle/app/oracle/product/11.2.0&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SCOPE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">SPFILE&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 启用归档日志模式
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">SHUTDOWN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IMMEDIATE&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">STARTUP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">MOUNT&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DATABASE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ARCHIVELOG&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DATABASE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">OPEN&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 查看归档日志状态
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">ARCHIVE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOG&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LIST&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">Database&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">mode&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Archive&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">Mode&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">Automatic&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">archival&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Enabled&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">Archive&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">destination&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">USE_DB_RECOVERY_FILE_DEST&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">Oldest&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">online&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sequence&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">60&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">Next&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sequence&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">to&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">archive&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">62&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">Current&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sequence&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">62&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 为特定表启用增强日志记录
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FLINKUSER&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">CUSTOMERS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ADD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SUPPLEMENTAL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOG&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DATA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">ALL&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">COLUMNS&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 为整个数据库启用增强日志记录
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DATABASE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ADD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SUPPLEMENTAL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOG&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DATA&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 创建用户并赋予 dba 角色
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">USER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">admin&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IDENTIFIED&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">admin123&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">DBA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">admin&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- 为该用户赋予 dba 角色
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="创建表空间和用户">创建表空间和用户&lt;/h4>
&lt;ol>
&lt;li>创建表空间：&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TABLESPACE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">logminer_tbs&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">DATAFILE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;/home/oracle/app/oracle/product/11.2.0/logminer_tbs.dbf&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SIZE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">25&lt;/span>&lt;span class="n">M&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">REUSE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AUTOEXTEND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">MAXSIZE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">UNLIMITED&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ol start="2">
&lt;li>创建用户并授权：&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">USER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IDENTIFIED&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkpw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TABLESPACE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOGMINER_TBS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">QUOTA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">UNLIMITED&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOGMINER_TBS&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 授予必要权限
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SESSION&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LOCK&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ANY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ANY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SEQUENCE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">V_$DATABASE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FLASHBACK&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ANY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ANY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SELECT_CATALOG_ROLE&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">EXECUTE_CATALOG_ROLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ANY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TRANSACTION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">EXECUTE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">DBMS_LOGMNR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">EXECUTE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">DBMS_LOGMNR_D&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">V_$LOG&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">V_$LOG_HISTORY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">V_$LOGMNR_LOGS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">V_$LOGMNR_CONTENTS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">V_$LOGMNR_PARAMETERS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">V_$LOGFILE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">V_$ARCHIVED_LOG&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">V_$ARCHIVE_DEST_STATUS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="创建示例表">创建示例表&lt;/h4>
&lt;p>切换到 flinkuser 并创建表：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="n">sqlplus&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flinkuser&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">flinkpw&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 创建 customers 表
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">customer_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">NUMBER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">customer_name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">VARCHAR2&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">50&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">email&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">VARCHAR2&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">phone&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">VARCHAR2&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">20&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TABLESPACE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOGMINER_TBS&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 创建 product 表
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">product&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">product_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">NUMBER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">product_name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">VARCHAR2&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">50&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">price&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">NUMBER&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TABLESPACE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOGMINER_TBS&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 查看表空间中的表
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">tablespace_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">table_name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">user_tables&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">tablespace_name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;LOGMINER_TBS&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 插入 10 条模拟数据
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ALL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">customer_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customer_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">email&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">phone&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;John Doe&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;john.doe@example.com&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;123-456-7890&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">customer_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customer_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">email&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">phone&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Jane Smith&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;jane.smith@example.com&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;234-567-8901&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">customer_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customer_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">email&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">phone&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Bob Johnson&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;bob.johnson@example.com&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;345-678-9012&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">customer_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customer_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">email&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">phone&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Alice Brown&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;alice.brown@example.com&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;456-789-0123&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">customer_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customer_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">email&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">phone&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Charlie Davis&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;charlie.davis@example.com&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;567-890-1234&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">customer_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customer_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">email&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">phone&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Eva Wilson&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;eva.wilson@example.com&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;678-901-2345&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">customer_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customer_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">email&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">phone&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Frank Miller&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;frank.miller@example.com&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;789-012-3456&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">customer_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customer_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">email&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">phone&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Grace Lee&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;grace.lee@example.com&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;890-123-4567&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">customer_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customer_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">email&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">phone&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">9&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Henry Taylor&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;henry.taylor@example.com&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;901-234-5678&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">customer_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customer_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">email&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">phone&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Ivy Chen&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;ivy.chen@example.com&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;012-345-6789&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">dual&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 验证插入的数据
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">customers&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 插入 5 条模拟数据
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ALL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">product&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">product_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">product_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">price&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Product A&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">product&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">product_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">product_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">price&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Product B&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">20&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">product&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">product_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">product_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">price&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Product C&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">30&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">product&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">product_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">product_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">price&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Product D&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">40&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">product&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">product_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">product_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">price&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Product E&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">50&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">dual&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 验证插入的数据
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">product&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 提交事务
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">COMMIT&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="flink-配置">Flink 配置&lt;/h3>
&lt;p>FLink 仍旧采用的是单机部署，直接解压即可，可以根据自己的服务器情况进行调整 &lt;code>flink-conf.yaml&lt;/code> 配置文件。然后最关键的是依赖的配置，由于 License 的不同，这些需要用户自己去手动配置。主要需要的有：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://repository.apache.org/content/repositories/releases/org/apache/doris/flink-doris-connector-1.18/24.0.0/flink-doris-connector-1.18-24.0.0.jar">&lt;code>doris-flink-connector-1.18-24.0.0.jar&lt;/code>&lt;/a>：Doris 的 Flink 连接器；&lt;/li>
&lt;li>&lt;a href="https://repo1.maven.org/maven2/org/apache/flink/flink-sql-connector-oracle-cdc/3.1.0/flink-sql-connector-oracle-cdc-3.1.0.jar">&lt;code>flink-sql-connector-oracle-cdc-3.1.0.jar&lt;/code>&lt;/a>：Flink 的 Oracle CDC 连接器；&lt;/li>
&lt;li>&lt;a href="https://repo1.maven.org/maven2/com/oracle/ojdbc/ojdbc8/19.3.0.0/ojdbc8-19.3.0.0.jar">&lt;code>ojdbc8-19.3.0.0.jar&lt;/code>&lt;/a>：Oracle 的 JDBC 驱动。&lt;/li>
&lt;/ul>
&lt;p>上述配置文件可以下载放在 &lt;code>flink/lib&lt;/code> 目录下，可以参考下图（还包含了上一篇 MySQL 同步数据的配置文件）：&lt;/p>
&lt;p>&lt;img src="https://github.com/QuakeWang/quakewang.github.io/blob/master/content/imag/tech/bigdata/doris/doris002_flink-lib.png?raw=true" alt="flink-lib">&lt;/p>
&lt;p>完成上述配置之后，使用 &lt;code>start-cluster.sh&lt;/code> 启动 Flink 集群。&lt;/p>
&lt;!-- ## Flink CDC 同步
完成上述配置之后，我们可以先尝试使用 Flink CDC 读取 Oracle 并同步至 Doris 中。可以按照下述步骤来进行操作：
启动 Flink SQL CLI：
```bash
$FLINK_HOME/bin/sql-client.sh
```
```sql
-- 创建 Oracle CDC 源表 oracle_source_customers，从 Oracle 数据库中读取数据
CREATE TABLE oracle_source_customers (
CUSTOMER_ID INT,
CUSTOMER_NAME STRING,
EMAIL STRING,
PHONE STRING,
PRIMARY KEY (CUSTOMER_ID) NOT ENFORCED
)
WITH (
'connector' = 'oracle-cdc',
'hostname' = '127.0.0.1',
'port' = '3051', -- Oracle 所在服务器端口，这里冲突做了更改
'username' = 'flinkuser',
'password' = 'flinkpw',
'database-name' = 'helowin',
'schema-name' = 'FLINKUSER',
'table-name' = 'CUSTOMERS'
);
-- 查看数据
SELECT * FROM oracle_source_customers;
```
![oracle-cdc-customers](https://github.com/QuakeWang/quakewang.github.io/blob/master/content/imag/tech/bigdata/doris/doris002_flink-sql.png?raw=true)
重新开一个终端，创建 Doris 中对应的库表：
```bash
mysql -h 127.0.0.1 -P 9030 -u root
```
```sql
CREATE DATABASE IF NOT EXISTS test_db;
USE test_db;
CREATE TABLE `CUSTOMERS` (
`CUSTOMER_ID` varchar(65533) NULL,
`CUSTOMER_NAME` varchar(150) NULL,
`EMAIL` varchar(300) NULL,
`PHONE` varchar(60) NULL
) ENGINE=OLAP
UNIQUE KEY(`CUSTOMER_ID`)
DISTRIBUTED BY HASH(`CUSTOMER_ID`) BUCKETS AUTO
PROPERTIES (
"replication_allocation" = "tag.location.default: 1",
"min_load_replica_num" = "-1",
"is_being_synced" = "false",
"storage_medium" = "hdd",
"storage_format" = "V2",
"inverted_index_storage_format" = "V1",
"enable_unique_key_merge_on_write" = "true",
"light_schema_change" = "true",
"disable_auto_compaction" = "false",
"enable_single_replica_compaction" = "false",
"group_commit_interval_ms" = "10000",
"group_commit_data_bytes" = "134217728",
"enable_mow_light_delete" = "false"
);
```
```bash
Flink SQL> INSERT INTO doris_sink_customers SELECT * FROM oracle_source_customers;
>
[ERROR] Could not execute SQL statement. Reason:
org.apache.doris.shaded.com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `int` from String "errCode=7,detailMessage=unknowntable,tableName=customers": not a valid `int` value at [Source: (String)""errCode=7,detailMessage=unknowntable,tableName=customers""; line: 1, column: 1]
```
回到 Flink SQL CLI 中，查看 Doris 中数据同步情况：
```sql
-- 创建 Doris 的 sink 表
CREATE TABLE doris_sink_customers (
CUSTOMER_ID int,
CUSTOMER_NAME STRING,
EMAIL STRING,
PHONE STRING
)
WITH (
'connector' = 'doris',
'fenodes' = '168.43.0.1:6280',
'table.identifier' = 'test_db.customers',
'username' = 'root',
'password' = 'Doris@123',
'sink.label-prefix'='doris_sink_001'
);
-- 执行数据同步
INSERT INTO doris_sink_customers SELECT * FROM oracle_source_customers;
-- 查看 Doris 中数据同步情况
SELECT * FROM doris_sink_customers;
``` -->
&lt;h2 id="代码同步">代码同步&lt;/h2>
&lt;p>可以创建 Maven 项目，并添加所需依赖如下所示：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-xml" data-lang="xml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;groupId&amp;gt;&lt;/span>org.apache.flink&lt;span class="nt">&amp;lt;/groupId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;artifactId&amp;gt;&lt;/span>flink-streaming-java&lt;span class="nt">&amp;lt;/artifactId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;version&amp;gt;&lt;/span>1.18.0&lt;span class="nt">&amp;lt;/version&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;/dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;groupId&amp;gt;&lt;/span>org.apache.doris&lt;span class="nt">&amp;lt;/groupId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;artifactId&amp;gt;&lt;/span>flink-doris-connector-1.18&lt;span class="nt">&amp;lt;/artifactId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;version&amp;gt;&lt;/span>24.0.0&lt;span class="nt">&amp;lt;/version&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;/dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;groupId&amp;gt;&lt;/span>org.apache.flink&lt;span class="nt">&amp;lt;/groupId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;artifactId&amp;gt;&lt;/span>flink-connector-oracle-cdc&lt;span class="nt">&amp;lt;/artifactId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;version&amp;gt;&lt;/span>3.1.0&lt;span class="nt">&amp;lt;/version&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;/dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>创建一个 Java 类命名为 &lt;code>OracleToDoris&lt;/code>，并编写代码如下：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.apache.doris.flink.table.DorisConfigOptions&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.apache.doris.flink.tools.cdc.DatabaseSync&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.apache.doris.flink.tools.cdc.oracle.OracleDatabaseSync&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.apache.flink.configuration.Configuration&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.apache.flink.cdc.connectors.oracle.source.config.OracleSourceOptions&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.apache.flink.streaming.api.environment.StreamExecutionEnvironment&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">java.util.HashMap&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">java.util.UUID&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">OracleToDoris&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">main&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">[]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">throws&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Exception&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">StreamExecutionEnvironment&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">env&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">StreamExecutionEnvironment&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getExecutionEnvironment&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setParallelism&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">1&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">disableOperatorChaining&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">enableCheckpointing&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">1000&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">database&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;test_db&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">tablePrefix&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">tableSuffix&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">HashMap&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sourceConfig&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HashMap&lt;/span>&lt;span class="o">&amp;lt;&amp;gt;&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sourceConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OracleSourceOptions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">DATABASE_NAME&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">key&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;helowin&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sourceConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OracleSourceOptions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">SCHEMA_NAME&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">key&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;FLINKUSER&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sourceConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OracleSourceOptions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">HOSTNAME&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">key&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;127.0.0.1&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sourceConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OracleSourceOptions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">PORT&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">key&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;3051&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sourceConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OracleSourceOptions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">USERNAME&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">key&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;flinkuser&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sourceConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OracleSourceOptions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">PASSWORD&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">key&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;flinkpw&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sourceConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;debezium.log.mining.strategy&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;online_catalog&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sourceConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;debezium.log.mining.continuous.mine&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sourceConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;debezium.database.history.store.only.captured.tables.ddl&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Configuration&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sourceConf&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Configuration&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">fromMap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sourceConfig&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">HashMap&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sinkConfig&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HashMap&lt;/span>&lt;span class="o">&amp;lt;&amp;gt;&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sinkConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">DorisConfigOptions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">FENODES&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">key&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;127.0.0.1:8030&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sinkConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">DorisConfigOptions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">USERNAME&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">key&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;root&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sinkConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">DorisConfigOptions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">PASSWORD&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">key&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;123456&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sinkConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">DorisConfigOptions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">JDBC_URL&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">key&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;jdbc:mysql://127.0.0.1:9030&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">sinkConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">DorisConfigOptions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">SINK_LABEL_PREFIX&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">key&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">UUID&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">randomUUID&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">toString&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Configuration&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sinkConf&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Configuration&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">fromMap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sinkConfig&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">boolean&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ignoreDefaultValue&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">boolean&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">useNewSchemaChange&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kt">boolean&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ignoreIncompatible&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">DatabaseSync&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">databaseSync&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">OracleDatabaseSync&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">databaseSync&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setEnv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">env&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setDatabase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">database&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setConfig&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sourceConf&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setTablePrefix&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tablePrefix&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setTableSuffix&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tableSuffix&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setIgnoreDefaultValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ignoreDefaultValue&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setSinkConfig&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sinkConf&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setCreateTableOnly&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setNewSchemaChange&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">useNewSchemaChange&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setIgnoreIncompatible&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ignoreIncompatible&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">create&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">databaseSync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">build&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">execute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">format&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Oracle-Doris Database Sync: %s&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">database&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这段代码展示了如何使用 Java 来配置和执行 Oracle 到 Doris 的数据同步任务。它首先设置了 Flink 的执行环境，包括并行度、算子链和检查点等基本参数。然后，代码分别配置了 Oracle 源和 Doris 接收端的详细信息，如数据库名称、主机地址、端口号、用户名和密码等。接着，它创建了一个 OracleDatabaseSync 实例，并通过一系列方法调用设置了同步任务的各种参数，包括数据库、表前缀后缀、是否忽略默认值等。最后，代码调用 create() 和 build() 方法来创建和构建同步任务，并执行这个任务。这种通过 Java 代码配置数据同步的方法比使用命令行更加灵活。它允许开发者根据需求动态调整参数，更容易集成到现有的 Java 应用中。&lt;/p>
&lt;p>编写完成代码之后，可以使用 &lt;code>mvn clean package&lt;/code> 命令来打包，然后在可以在 Flink Web UI 中提交任务。如下图所示：&lt;/p>
&lt;p>&lt;img src="https://github.com/QuakeWang/quakewang.github.io/blob/master/content/imag/tech/bigdata/doris/doris002_flink-submit.png?raw=true" alt="flink-submit-task">&lt;/p>
&lt;p>当提交完成之后，可以在 Web UI 的 Jobs Running Jobs 看见提交的任务，当任务状态变为 RUNNING 之后，说明数据同步任务正在运行。&lt;/p>
&lt;p>&lt;img src="https://github.com/QuakeWang/quakewang.github.io/blob/master/content/imag/tech/bigdata/doris/doris002_flink-web01.png?raw=true" alt="flink-running-task">&lt;/p>
&lt;p>这个时候可以在 Doris 中查看数据同步情况：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="n">mysql&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">test_db&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">PRODUCT&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="c1">------------+--------------+-------+
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PRODUCT_ID&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PRODUCT_NAME&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PRICE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="c1">------------+--------------+-------+
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Product&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">E&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">50&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Product&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">B&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">20&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Product&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">A&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Product&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">C&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">30&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Product&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">D&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">40&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="c1">------------+--------------+-------+
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">rows&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">in&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">set&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">01&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sec&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这个时候如果插入新的数据，Doris 中也会同步更新数据，则说明数据同步成功。&lt;/p>
&lt;h2 id="doris-flink-connector-同步">Doris-Flink-Connector 同步&lt;/h2>
&lt;p>如果感觉使用代码同步数据比较复杂，则可以使用 Doris-Flink-Connector 同步数据，在 MySQL 数据同步已经用过了，总体来说就是添加依赖至 &lt;code>$FLINK_HOME/lib&lt;/code> 下，然后启动命令即可。&lt;/p>
&lt;p>依赖在上面已经添加了，这里就不再赘述了，直接启动命令如下：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">bin/flink run &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -Dexecution.checkpointing.interval&lt;span class="o">=&lt;/span>10s &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -Dparallelism.default&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -c org.apache.doris.flink.tools.cdc.CdcTools &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> lib/flink-doris-connector-1.18-24.0.0.jar &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oracle-sync-database &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --database test_db &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --oracle-conf &lt;span class="nv">hostname&lt;/span>&lt;span class="o">=&lt;/span>127.0.0.1 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --oracle-conf &lt;span class="nv">port&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">3051&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --oracle-conf &lt;span class="nv">username&lt;/span>&lt;span class="o">=&lt;/span>flinkuser &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --oracle-conf &lt;span class="nv">password&lt;/span>&lt;span class="o">=&lt;/span>flinkpw &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --oracle-conf database-name&lt;span class="o">=&lt;/span>helowin &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --oracle-conf schema-name&lt;span class="o">=&lt;/span>FLINKUSER &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --oracle-conf debezium.log.mining.strategy&lt;span class="o">=&lt;/span>online_catalog &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --oracle-conf debezium.log.mining.continuous.mine&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --oracle-conf debezium.database.history.store.only.captured.tables.ddl&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sink-conf &lt;span class="nv">fenodes&lt;/span>&lt;span class="o">=&lt;/span>168.43.0.1:8030 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sink-conf &lt;span class="nv">username&lt;/span>&lt;span class="o">=&lt;/span>root &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sink-conf &lt;span class="nv">password&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">123456&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sink-conf jdbc-url&lt;span class="o">=&lt;/span>jdbc:mysql://168.43.0.1:9030 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sink-conf sink.label-prefix&lt;span class="o">=&lt;/span>label &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --table-conf &lt;span class="nv">replication_num&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这个命令使用 Flink 运行 Doris-Flink-Connector 来同步 Oracle 数据库到 Doris。解释一下主要参数：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>Flink 执行参数：&lt;/p>
&lt;ul>
&lt;li>设置检查点间隔为 10 秒&lt;/li>
&lt;li>默认并行度为 1&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>主类和 JAR 文件：&lt;/p>
&lt;ul>
&lt;li>使用 &lt;code>org.apache.doris.flink.tools.cdc.CdcTools&lt;/code> 类&lt;/li>
&lt;li>从 &lt;code>lib/flink-doris-connector-1.18-24.0.0.jar&lt;/code> 运行&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>同步模式：&lt;code>oracle-sync-database&lt;/code>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>目标数据库：&lt;code>test_db&lt;/code>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Oracle 配置：&lt;/p>
&lt;ul>
&lt;li>主机名、端口、用户名、密码&lt;/li>
&lt;li>数据库名和 schema 名&lt;/li>
&lt;li>日志挖掘策略和连续挖掘设置&lt;/li>
&lt;li>仅捕获指定表的 DDL 历史&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>Doris 接收端配置：&lt;/p>
&lt;ul>
&lt;li>FE 节点地址&lt;/li>
&lt;li>用户名和密码&lt;/li>
&lt;li>JDBC URL&lt;/li>
&lt;li>导入标签前缀&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>表配置：&lt;/p>
&lt;ul>
&lt;li>复制数量设为 1&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>补充&lt;/strong>：debezium 相关的三行参数的作用。&lt;/p>
&lt;p>这三行参数是针对 Oracle CDC (Change Data Capture) 的特定配置，它们对于 Oracle 数据同步有重要作用：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;code>--oracle-conf debezium.log.mining.strategy=online_catalog&lt;/code>&lt;/p>
&lt;ul>
&lt;li>这个参数设置了日志挖掘策略为 &amp;quot;online_catalog&amp;quot;。&lt;/li>
&lt;li>在这种模式下，Debezium（用于 CDC 的开源框架）使用 Oracle 的在线目录视图来获取必要的元数据信息。&lt;/li>
&lt;li>这种方法通常比其他策略更快，因为它不需要解析重做日志来获取元数据。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;code>--oracle-conf debezium.log.mining.continuous.mine=true&lt;/code>&lt;/p>
&lt;ul>
&lt;li>启用连续挖掘模式。&lt;/li>
&lt;li>当设置为 true 时，Oracle LogMiner 会持续不断地处理重做日志，而不是在每次查询时重新启动。&lt;/li>
&lt;li>这可以显著提高性能，特别是在处理大量变更时。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;code>--oracle-conf debezium.database.history.store.only.captured.tables.ddl=true&lt;/code>&lt;/p>
&lt;ul>
&lt;li>这个配置指示 Debezium 只存储被捕获表的 DDL（数据定义语言）历史。&lt;/li>
&lt;li>当设置为 true 时，它会减少存储的 DDL 历史量，只保留与被监控表相关的 DDL 语句。&lt;/li>
&lt;li>这可以减少存储开销，并可能提高性能，特别是在大型数据库环境中。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>总的来说，这些参数旨在优化 Oracle CDC 过程，提高性能，减少资源消耗，并确保更高效的数据捕获。它们特别适用于需要实时或近实时数据同步的场景，同时也考虑了系统资源的有效利用。&lt;/p>
&lt;p>可以在 Flink Web UI 中查看任务执行情况：&lt;/p>
&lt;p>&lt;img src="https://github.com/QuakeWang/quakewang.github.io/blob/master/content/imag/tech/bigdata/doris/doris002_flink-web02.png?raw=true" alt="flink-web-ui">&lt;/p>
&lt;p>可以看见所有任务的 Job State 都是 RUNNING，说明数据同步任务正在运行。可以在 Doris 中查看数据同步情况：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="n">mysql&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">select&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">from&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">test_db&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">CUSTOMERS&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="c1">-------------+---------------+---------------------------+--------------+
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">CUSTOMER_ID&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">CUSTOMER_NAME&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">EMAIL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PHONE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="c1">-------------+---------------+---------------------------+--------------+
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Alice&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Brown&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">alice&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">brown&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="n">example&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">com&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">456&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">789&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">0123&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">9&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Henry&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Taylor&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">henry&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">taylor&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="n">example&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">com&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">901&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">234&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">5678&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Charlie&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Davis&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">charlie&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">davis&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="n">example&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">com&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">567&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">890&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">1234&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Frank&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Miller&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">frank&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">miller&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="n">example&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">com&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">789&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">012&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">3456&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Jane&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Smith&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">jane&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">smith&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="n">example&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">com&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">234&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">567&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">8901&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Ivy&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Chen&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ivy&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">chen&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="n">example&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">com&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">012&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">345&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">6789&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Bob&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Johnson&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">bob&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">johnson&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="n">example&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">com&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">345&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">678&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">9012&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Grace&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Lee&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">grace&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">lee&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="n">example&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">com&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">890&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">123&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">4567&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Eva&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Wilson&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">eva&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">wilson&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="n">example&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">com&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">678&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">901&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">2345&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">John&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Doe&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">john&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">doe&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="n">example&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">com&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">123&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">456&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">7890&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="c1">-------------+---------------+---------------------------+--------------+
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">rows&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">in&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">set&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">01&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sec&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这个时候如果在 Oracle 中插入新的数据，可以发现 Doris 中也会同步更新数据。可以发现使用 Connector 进行数据同步是高效又方便。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>到此为止我们完成了 Oracle 到 Doris 的数据同步，同样是提供了两种方式供读者选择，第一种选择的是代码实现，代码的总体结构比较简单，也只是设置一些参数。第二种则更方便一点，使用的是 Doris-Flink-Connector 来同步，可以直接使用 &lt;code>flink run&lt;/code> 命令添加对应的参数完成数据的同步。&lt;/p>
&lt;p>我们在创建测试数据的时候设置表包含主键，对应到 Doris 就是&lt;a href="https://doris.apache.org/zh-CN/docs/table-design/data-model/unique">主键数据模型&lt;/a>，读者可以使用 &lt;code>SHOW CREATE TABLE&lt;/code> 语句来进行检测，那如果某张表不包含主键该怎么进行同步呢？以及某些表不需要进行同步，这些参数又该如何设置呢？可以期待下篇内容！&lt;/p>
&lt;blockquote>
&lt;p>参考：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://nightlies.apache.org/flink/flink-cdc-docs-release-3.2/docs/connectors/flink-sources/oracle-cdc/">Oracle CDC Connector&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doris.apache.org/zh-CN/docs/ecosystem/flink-doris-connector">Flink Doris Connector&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/QuakeWang/doris-usercase/blob/main/src/main/java/org/apache/doris/sync/oracle/OracleToDoris.java">OracleToDoris 代码&lt;/a>&lt;/li>
&lt;li>&lt;/li>
&lt;/ul>
&lt;/blockquote></content></entry><entry><title type="text">Doris01——MySQL 整库同步</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/doris/doris001_sync-mysql2doris/"/><id>https://quakewang.github.io/tech/doris/doris001_sync-mysql2doris/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2024-09-21T11:24:00+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="mysql-同步数据至-doris">MySQL 同步数据至 Doris&lt;/h1>
&lt;h2 id="开篇">开篇&lt;/h2>
&lt;p>摸了几个月，好久没写技术类型的博客，打算开新坑来写写关于 Doris 的点点滴滴，同时也是记录自己学习的一个过程。这个系列不会解释 Doris 以及牵涉到的大数据是什么，以及基本的使用技巧，这部分内容完全可以去看看各个项目的官方文档。更想写的是将 Doirs 聚焦于一个个具体的使用场景，比如本篇内容讲的就是如何同步 MySQL 的数据至 Doris，这里提供了两种方案供读者选择，分别是 &lt;a href="https://nightlies.apache.org/flink/flink-cdc-docs-master/docs/get-started/quickstart/mysql-to-doris/">FlinkCDC&lt;/a>、&lt;a href="https://doris.apache.org/docs/3.0/ecosystem/flink-doris-connector/">Doris-Flink-Connector&lt;/a> 。&lt;/p>……</summary><content type="html">&lt;h1 id="mysql-同步数据至-doris">MySQL 同步数据至 Doris&lt;/h1>
&lt;h2 id="开篇">开篇&lt;/h2>
&lt;p>摸了几个月，好久没写技术类型的博客，打算开新坑来写写关于 Doris 的点点滴滴，同时也是记录自己学习的一个过程。这个系列不会解释 Doris 以及牵涉到的大数据是什么，以及基本的使用技巧，这部分内容完全可以去看看各个项目的官方文档。更想写的是将 Doirs 聚焦于一个个具体的使用场景，比如本篇内容讲的就是如何同步 MySQL 的数据至 Doris，这里提供了两种方案供读者选择，分别是 &lt;a href="https://nightlies.apache.org/flink/flink-cdc-docs-master/docs/get-started/quickstart/mysql-to-doris/">FlinkCDC&lt;/a>、&lt;a href="https://doris.apache.org/docs/3.0/ecosystem/flink-doris-connector/">Doris-Flink-Connector&lt;/a> 。&lt;/p>
&lt;p>版本选择：&lt;/p>
&lt;ul>
&lt;li>Doris 2.1.6&lt;/li>
&lt;li>MySQL 8.0&lt;/li>
&lt;li>Flink 1.18.0&lt;/li>
&lt;li>FlinkCDC 3.1.0&lt;/li>
&lt;li>Doris-Flink-Connector 24.0.0&lt;/li>
&lt;/ul>
&lt;h2 id="准备工作">准备工作&lt;/h2>
&lt;h3 id="mysql-开启-binlog-并建立测试库表">MySQL 开启 Binlog 并建立测试库表&lt;/h3>
&lt;p>MySQL 开启 Binlog：&lt;code>sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf &lt;/code>。&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-conf" data-lang="conf">server-id = 1
log_bin = /var/log/mysql/mysql-bin.log
binlog_format = ROW
&lt;/code>&lt;/pre>&lt;p>重启 MySQL 服务：&lt;code>sudo service mysql restart&lt;/code>。&lt;/p>
&lt;p>参考：https://debezium.io/documentation/reference/1.9/tutorial.html&lt;/p>
&lt;p>使用 MySQL 建立对应的库表：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- create database
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DATABASE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">app_db&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">USE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">app_db&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- create orders table
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">price&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">DECIMAL&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- insert records
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">price&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">00&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">price&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">00&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- create shipments table
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">shipments&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">city&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">VARCHAR&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">255&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- insert records
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">shipments&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">city&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;beijing&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">shipments&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">city&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;xian&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- create products table
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">products&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">product&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">VARCHAR&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">255&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- insert records
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">products&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">product&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Beer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">products&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">product&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Cap&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">products&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="n">product&lt;/span>&lt;span class="o">`&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Peanut&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="配置-doris-并建立对应的库">配置 Doris 并建立对应的库&lt;/h3>
&lt;p>Doris 可以通过 &lt;a href="https://docs.selectdb.com/docs/enterprise/cluster-manager-guide/deployment-guide/deployment-guide-24.x">doris-manager&lt;/a> 进行安装配置，可视化页面操作，只要按照说明提供好对应的配置文件即可，还附属监控功能，很推荐👍。这里 Doris 使用的版本为 &lt;a href="https://apache-doris-releases.oss-accelerate.aliyuncs.com/apache-doris-2.1.6-bin-x64.tar.gz">2.1.6&lt;/a>。&lt;/p>
&lt;p>当完成配置之后，可以使用 &lt;code>jps&lt;/code> 命令查看 Doris 的进程，如果启动成功，会看到 &lt;code>DorisFE&lt;/code> 和 &lt;code>DorisBE&lt;/code> 的进程。然后使用 &lt;code>mysql -uroot -P9030 -h127.0.0.1&lt;/code> 命令可以连接 Doris 的 FE 节点。并创建对应的库即可。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DATABASE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">app_db&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="flink-配置">Flink 配置&lt;/h3>
&lt;p>Flink 使用单机模式即可，需要在 lib 目录下导入对应的依赖。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 启动 Flink 集群&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">flink/bin/start-cluster.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="flinkcdc-同步">FlinkCDC 同步&lt;/h2>
&lt;p>FlinkCDC 这里选用的版本为 &lt;a href="https://archive.apache.org/dist/flink/flink-cdc-3.1.0/flink-cdc-3.1.0-bin.tar.gz">3.1.0&lt;/a>，直接解压并在 Flink-CDC 的 lib 目录下添加依赖即可使用，因为我们的作业是同步 MySQL 的数据至 Doris，所以需要导入的依赖为：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://repo1.maven.org/maven2/org/apache/flink/flink-cdc-pipeline-connector-mysql/3.1.0/flink-cdc-pipeline-connector-mysql-3.1.0.jar">Apache Doris pipeline connector 3.1.0&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://repo1.maven.org/maven2/org/apache/flink/flink-cdc-pipeline-connector-doris/3.1.0/flink-cdc-pipeline-connector-doris-3.1.0.jar">MySQL pipeline connector 3.1.0&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>编写 YAML 文件：&lt;code>vim mysql-to-doris.yaml&lt;/code>。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">source&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mysql&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostname&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">localhost&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3306&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">username&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">root&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">123456&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tables&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">app_db.\.*&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">server-id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5400-5404&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">server-time-zone&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Asia/Shanghai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">sink&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">doris&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fenodes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">127.0.0.1&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="m">8030&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">username&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">root&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">table.create.properties.light_schema_change&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">table.create.properties.replication_num&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">pipeline&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Sync MySQL Database to Doris&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parallelism&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>执行 Job&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">flink-cdc/bin/flink-cdc.sh mysql-to-doris.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>可以在 Doris 的 FE 节点上使用 &lt;code>show tables from app_db;&lt;/code> 查看同步的表。因为是使用 Binlog 同步，所以当 MySQL 的表有更新时，Doris 的表也会同步更新。使用 Flink-CDC 可以帮我们自动在 Doris 中创建对应的表格，是不是感觉很方便？别急，下面还有更好用的。&lt;/p>
&lt;h2 id="doris-flink-connector-同步">Doris-Flink-Connector 同步&lt;/h2>
&lt;p>&lt;a href="https://github.com/apache/doris-flink-connector">Doris-Flink-Connector&lt;/a> 是 Doris 官方提供的连接器，可以支持通过 Flink 操作（读取、写入、修改、删除）Doris 中存储的数据。还是以刚才 MySQL 的数据为例，看看使用 connector 是如何同步数据的。&lt;/p>
&lt;p>Connector 整库同步功能底层依赖的是 Flink CDC，因而对于相应关系型数据的支持，请参考 Flink CDC 官方文档 Flink CDC Overview：https://nightlies.apache.org/flink/flink-cdc-docs-release-3.1/docs/connectors/pipeline-connectors/overview/。&lt;/p>
&lt;p>首先我们需要使用官方提供的 build.sh 脚本构建 jar 包，并将其放到 Flink 的 lib 目录下。这一步不是很难，读者可以自行尝试。另外截止目前最新的 &lt;a href="https://github.com/apache/doris-flink-connector/releases/tag/24.0.0">24.0.0&lt;/a> 已经提供好对应的 jar 包，直接下载对应的版本即可。除此之外，还需要再添加 &lt;a href="https://repo1.maven.org/maven2/org/apache/flink/flink-sql-connector-mysql-cdc/3.1.0/flink-sql-connector-mysql-cdc-3.1.0.jar">flink-sql-connector-mysql-cdc&lt;/a> 的 jar 包，否则可能会报类找不到的错误。完成上述配置之后就可以使用 Connector 进行 MySQL 整库同步了。同步命令如下：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">FLINK_HOME/bin/flink run &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -Dexecution.checkpointing.interval&lt;span class="o">=&lt;/span>10s &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -Dparallelism.default&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -c org.apache.doris.flink.tools.cdc.CdcTools &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> lib/flink-doris-connector-1.16-24.0.0.jar &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> mysql-sync-database &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --database app_db &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --mysql-conf &lt;span class="nv">hostname&lt;/span>&lt;span class="o">=&lt;/span>127.0.0.1 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --mysql-conf &lt;span class="nv">port&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">3306&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --mysql-conf &lt;span class="nv">username&lt;/span>&lt;span class="o">=&lt;/span>root &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --mysql-conf &lt;span class="nv">password&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">123456&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --mysql-conf database-name&lt;span class="o">=&lt;/span>app_db &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sink-conf &lt;span class="nv">fenodes&lt;/span>&lt;span class="o">=&lt;/span>127.0.0.1:8030 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sink-conf &lt;span class="nv">username&lt;/span>&lt;span class="o">=&lt;/span>root &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sink-conf &lt;span class="nv">password&lt;/span>&lt;span class="o">=&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sink-conf jdbc-url&lt;span class="o">=&lt;/span>jdbc:mysql://127.0.0.1:9030 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sink-conf sink.label-prefix&lt;span class="o">=&lt;/span>label &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --table-conf &lt;span class="nv">replication_num&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>具体的命令参数可以参考官方文档：https://doris.apache.org/zh-CN/docs/ecosystem/flink-doris-connector&lt;/p>
&lt;p>使用起来是不是很方便？连配置文件都不用写。检测结果的步骤这里同上，这里不做过多赘述。&lt;/p>
&lt;!-- ## SeaTunnel 同步 -->
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>到这里，我们完成了使用 Flink 同步 MySQL 数据至 Doris 的操作，现在的数据集成工具越来越好用，同步的操作也越来越方便，从需要用户建表，到写个配置文件，再到使用 Connector 直接使用命令即可完成整库同步。也给用户提供了更多的选择。不过到这里你是否会好奇，使用工具同步的过程，建立的表是什么类型的呢？&lt;/p>
&lt;p>Apache Doris 主要有 Unique,Aggreate,Duplicate 三种数据模型，对于数据源（MySQL,Oracle,Postgres,SQL Server) 库中含有主键的表，将直接将该表映射成 &lt;strong>Unique&lt;/strong> 表，而其他不包含主键的表，将直接映射成 Doris 中的 &lt;strong>Duplicate&lt;/strong> 表，对于 MySQL 同步非主键表，需要 &lt;code>--mysql-conf scan.incremental.snapshot.chunk.key-column&lt;/code> 来设置非主键表的 chunk key，且只能选择非空类型的一个字段，不同库表列之间使用 &lt;code>,&lt;/code> 隔开。&lt;/p>
&lt;p>关于全量同步和增量同步也是一个比较有意思的话题，Connecor 在利用 Flink-CDC 进行同步的过程分为全量 + 增量两个阶段，其中全量读取阶段可以并发无锁读取，增量阶段则切分为单线程读取 Binlog，以房子重复拉取 Binlog 数据，对于增量同步的过程，可以根据业务能够接受的最低延迟来调整 CheckPoint 参数。&lt;/p>
&lt;p>好了，这篇博客的内容到此为止，简单讲解了如何同步 MySQL 的库表到 Doris 中，总体来说不是很难，按照步骤可以复现，如果有疑问欢迎留言～&lt;/p>
&lt;p>在下篇博客中我们会进一步探索数据集成与同步的更多用法～&lt;/p>
&lt;blockquote>
&lt;p>参考：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://nightlies.apache.org/flink/flink-cdc-docs-release-3.1/docs/get-started/quickstart/mysql-to-doris/">Streaming ELT from MySQL to Doris&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doris.apache.org/zh-CN/docs/ecosystem/flink-doris-connector">Flink Doris Connector&lt;/a>&lt;/li>
&lt;/ul>
&lt;/blockquote></content></entry><entry><title type="text">Redis05——哨兵机制</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/redis/redis05_sentinel/"/><id>https://quakewang.github.io/tech/redis/redis05_sentinel/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2024-06-12T11:17:00+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="redis哨兵机制">Redis——哨兵机制&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>在上一篇博客中，讲解了有关 Redis 的主从模式，在这个模式下，如果从库发生故障了，客户端可以继续向主库或者其他从库发送请求，进行相关操作。但如果是主库挂了呢？就会直接影响从库之间的数据同步，因为从库没有相应的主库可以进行数据复制操作了。&lt;/p>……</summary><content type="html">&lt;h1 id="redis哨兵机制">Redis——哨兵机制&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>在上一篇博客中，讲解了有关 Redis 的主从模式，在这个模式下，如果从库发生故障了，客户端可以继续向主库或者其他从库发送请求，进行相关操作。但如果是主库挂了呢？就会直接影响从库之间的数据同步，因为从库没有相应的主库可以进行数据复制操作了。&lt;/p>
&lt;p>这个时候，如果客户端发送的是读请求，那么集群中的从库还可以继续提供服务，也就是说在纯读的业务场景下还能被接受。可一旦有写请求的操作了，按照主从模式下的读写分离要求，需要由主库来完成写操作。此时，因为主库挂了，没有实例可以来服务客户端的写操作请求了。&lt;/p>
&lt;p>无论是写服务中断，还是从库无法进行数据同步，在实际生产环境中都是不可接受的。所以，当主库挂了之后，需要一个新的主库来保持 Redis 集群的正常运行。通常会选择把集群中的一个从库切换为主库，这个时候就会涉及到以下三个问题：&lt;/p>
&lt;ul>
&lt;li>主库真的挂了吗？&lt;/li>
&lt;li>该选择哪个从库作为主库？&lt;/li>
&lt;li>怎么把新主库的相关信息通知给从库和客户端呢？&lt;/li>
&lt;/ul>
&lt;p>这就是 Redis 的哨兵机制的作用。在 Redis 主从集群中，哨兵机制是实现主从库自动切换的关键机制，它有效地解决了主从复制模式下故障转移的这三个问题。&lt;/p>
&lt;h2 id="基本流程">基本流程&lt;/h2>
&lt;p>哨兵其实就是一个运行在特殊模式下的 Redis 进程，主从库实例运行的同时，它也在运行。哨兵主要负责的就是三个任务：监控、选主和通知。&lt;/p>
&lt;p>监控指的是哨兵在运行时，会周期性地给所有的主从库发送 PING 命令，检测集群中的节点是否在正常运行。如果从库的响应超时，哨兵就会把它标记为“下线状态”；同样，如果主库没有在规定时间内响应哨兵的 PING 命令，哨兵就会判定主库下线，然后开始&lt;strong>自动切换主库&lt;/strong>的流程。&lt;/p>
&lt;p>这也就会用到哨兵的第二个任务，选主。主库挂了之后，哨兵就需要从很多个从库里，按照一定的规则选择一个从库实例，把它作为新的主库。这一步完成后，集群中也有有了新的主库。&lt;/p>
&lt;p>然后，哨兵会执行最后一个任务：&lt;strong>通知&lt;/strong>。在执行通知任务时，哨兵会把新主库的连接信息发送给其他从库，让它们执行 replicaof 命令，和新主库建立连接，并进行数据复制。同时，哨兵会把新主库的连接信息通知给客户端，让它们把请求操作发到新主库上。&lt;/p>
&lt;p>在上述的三个任务中，通知任务相对来说比较简单，哨兵只需要把新主库的信息发送给从库和客户端，让它们重新和新主库建立连接即可，并不涉及到决策的逻辑。但是，在监控和选主这两个任务重，哨兵需要做出两个决策：&lt;/p>
&lt;ul>
&lt;li>在监控任务中，哨兵需要判断主库是否处于下线状态；&lt;/li>
&lt;li>在选择任务中，哨兵也需要决定选择哪一个从库实例作为新的主库。&lt;/li>
&lt;/ul>
&lt;h2 id="主观下线和客观下线">主观下线和客观下线&lt;/h2>
&lt;p>先来看一下什么是“主观下线”。哨兵进程会使用 PING 命令检测它自己和主从库的网络连接情况，用来判断集群中各个实例的状态。如果某个节点的响应超时了，哨兵就会先把它标记为“主观下线”。&lt;/p>
&lt;p>这里对于主从库的后续处理会有所不同。如果检测的是从库，处理比较简单，只需要将其标记为“客观下线”就行了，因为从库的下线影响一般不会太大，集群对外的服务也就不会间断。&lt;/p>
&lt;p>但如果检测到的是主库响应 PING 命令超时，哨兵就不能简单地将其标记为“主观下线”，开启主从切换。因为很有可能存在这么一个情况：哨兵误判了，主库并没有故障。如果直接开始主从切换，后续的选主和通知任务都会带来额外的计算和通信开销。为了避免这些不必要的开销，就需要特别注意误判的情况。&lt;/p>
&lt;p>首先，得搞清楚误判是什么。简单来说，就是主库实际上并没有下线，但是哨兵误以为它挂了。误判通常会发生在集群网络压力较大、网络阻塞，或者是主库本身压力较大的情况下。&lt;/p>
&lt;p>一旦哨兵判断主库下线了，就会开始选择新主库，并让从库和新主库进行数据同步，这过程本身就会有开销，例如，哨兵要花时间选出新主库，从库也要花时间和主库建立连接，然后进行数据同步。而在误判的情况下，主库本身根本就不需要进行切换的，所以这个过程的开销是没有价值的。所以，我们要尽可能地减少误判。&lt;/p>
&lt;p>俗话说，一个好汉三个帮，我们可以采用&lt;strong>多实例组成的哨兵集群&lt;/strong>来解决这个问题。引入多个哨兵实例一起来判断，就可以避免单个哨兵因为自身网络状况不好，而误判主库下线的情况。同时，多个哨兵的网络同时不稳定的概率比较小，通过哨兵集群一起做决策，误判率也会降低。&lt;/p>
&lt;p>在判断主库是否下线时，不能由一个哨兵说了算，只有大多数的哨兵实例，都判断主库已经“客观下线”了，主库才会被标记为“客观下线”。这个判断的原则是：少数服从多数。同时，这会进一步触发哨兵开始主从切换的流程。&lt;/p>
&lt;p>如下图所示，Redis 主从集群有一个主库、三个从库，还有三个哨兵实例。在图片的左边，哨兵 2 判断主库为“主观下线”，但哨兵 1 和 3 却判定主库是上线状态，此时，主库仍然被判断为处于上线状态。在图片的右边，哨兵 1 和 2 都判断主库为“主观下线”，此时，即使哨兵 3 仍然判断主库为上线状态，主库也被标记为“客观下线”了。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/7724ddb68ace039bc06a9bcd7ab7c7232557051e/content/imag/tech/redis/05_Go-Offine.svg" alt="go-offline">&lt;/p>
&lt;p>简单来说，“客观下线”的标准就是，当有 N 个哨兵实例时，最好要有 N/2+1 个哨兵实例都认为主库是“主观下线”了，才能最终将主库标记为“客观下线”。这样一来，就可以减少误判的概率，也能避免误判带来的主从切换。&lt;/p>
&lt;h2 id="如何选主">如何选主？&lt;/h2>
&lt;p>一般来说，可以把哨兵选择新主库的过程称为“筛选 + 打分”。简单来说，在多个从库中，先按照&lt;strong>一定的筛选条件&lt;/strong>，把不符合条件的从库去掉。然后，再按照&lt;strong>一定的规则&lt;/strong>，给剩下的从库逐个打分，将得分最高的从库选为新主库，如下图所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/7724ddb68ace039bc06a9bcd7ab7c7232557051e/content/imag/tech/redis/05_Elect-Master.svg" alt="election">&lt;/p>
&lt;p>一般情况下，我们要先保证所选的从库仍然在线运行。不过，在选主时从库正常在线，这只能表示从库的现状良好，并不代表它就是最适合做主库的。比如，在选主时，一个从库正常运行，我们就把它选为新主库开始使用了。可是，没过一会它的网络出现了故障，这就导致不得不重新选主，是一件很糟糕的事情。&lt;/p>
&lt;p>所以，在选主时，&lt;strong>除了要检查从库的当前在线状态，还要判断它之前的网络连接状态&lt;/strong>。如果从库和主库断连，而且断连的次数超过了一定的阈值，凭借这一点就可以说，这个从库的网络状况并不是很好，从而可以排除这个从库。&lt;/p>
&lt;p>具体怎么判断呢？可以使用配置项 &lt;code>down-after-milliseconds * 10&lt;/code>。其中，down-after-milliseconds 是认定主从库断连的最大连接超时时间。如果在 down-after-milliseconds 毫秒内，主从节点都没有通过网络联系上，就可以认为主从节点断连了。如果发生断连的次数超过了 10 次，就说明这个从库的网络状况不好，不适合作为新主库。&lt;/p>
&lt;p>基于上述内容就可以过滤掉不适合做主库的从库，完成了筛选工作。&lt;/p>
&lt;p>接下来就需要给剩余的从库进行打分，可以分别按照三个规则依次进行三轮打分，这三个规则分别是&lt;strong>从库优先级、从库复制进度以及从库 ID 号&lt;/strong>。只要在某一轮中，有从库得分最高，那么它就是主库了，选主过程到此结束。如果没有出现得分最高的从库，那么就继续进行下一轮。&lt;/p>
&lt;p>&lt;strong>第一轮：优先级最高的从库得分高&lt;/strong>。&lt;/p>
&lt;p>用户可以通过 &lt;code>slave-priority&lt;/code> 配置项，给不同的从库设置优先级。假设现在有两个从库，它们的内存大小不一样，就可以手动给内存大的实例设置一个高优先级。在选主时，哨兵会给优先级高的从库打高分，如果有一个从库优先级最高，那么它就是新主库了。如果从库的优先级相同，则哨兵会开始第二轮打分。&lt;/p>
&lt;p>&lt;strong>第二轮：复制进度最高的从库得分高&lt;/strong>。&lt;/p>
&lt;p>这个规则的依据是，如果选择和旧主库同步最接近的从库作为主库，那么，这个新主库上就有最新的数据。如何判断从库和旧主库间的同步进度呢？&lt;/p>
&lt;p>在上一篇博客中有说到，主从库同步时有个命令传播的过程。在这个过程中，主库会使用 master_repl_offset 记录当前的最新写操作在 repl_backlog_buffer 中的位置，而从库会用 slave_repl_offset 这个值记录当前的复制进度。此时，想要找的从库，它的 slave_repl_offset 需要最接近 master_repl_offset。如果在所有从库中，有从库的 slave_repl_offset 最接近 master_repl_offset，那么它的得分就最高，可以作为新主库。&lt;/p>
&lt;p>就像下图所示，旧主库的 master_repl_offset 是 100，从库 1、2 和 3 的 slave_repl_offset 分别是 95、99 和 90，那么，从库 2 就应该被选为新主库。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/7724ddb68ace039bc06a9bcd7ab7c7232557051e/content/imag/tech/redis/05_Replicate.svg" alt="replicate">&lt;/p>
&lt;p>不过，有时候也会出现两个从库的 salve_repl_offset 的值大小是一样的，这种情况下，哨兵会继续进行第三轮打分。&lt;/p>
&lt;p>&lt;strong>第三轮：ID 号最高的从库得分高&lt;/strong>。&lt;/p>
&lt;p>每个实例都会有一个 ID，这个 ID 类似于这里的从库编号。目前，Redis 在选主从库时，有一个默认的规定：&lt;strong>在优先级和复制进度都相同的情况下，ID 号最小的从库得分最高，会被选为新主库&lt;/strong>。&lt;/p>
&lt;p>到这里，新主库就被选出来了，“选主”这个过程就完成了。&lt;/p>
&lt;p>简单总结如下：&lt;/p>
&lt;p>首先，哨兵会按照在线状态、网络状态，筛选过滤掉一部分不符合要求的从库，然后，依次按照优先级、复制进度、ID 号大小再对剩余的从库进行打分，只要有得分最高的从库出现，就把它选为新主库。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>在这篇博客中，了解了哨兵机制，它是实现 Redis 不间断服务的重要保证。具体来说，主从集群的数据同步是数据可靠的基础保证；而在主库发生故障时，自动的主从切换时服务不间断的关键支撑。&lt;/p>
&lt;p>Redis 的哨兵机制自动完成了以下三大功能，从而实现了主从库的自动切换，可以降低 Redis 集群的运维开销：&lt;/p>
&lt;ul>
&lt;li>监控主库运行状态，并判断主库是否客观下线；&lt;/li>
&lt;li>在主库客观下线后，选取新主库；&lt;/li>
&lt;li>选出新主库后，通知从库和客户端。&lt;/li>
&lt;/ul>
&lt;p>为了降低误判率，在实际应用时，哨兵机制通常采用多实例的方式进行部署，多个哨兵实例通过“少数服从多数”的原则，来判断主库是否客观下线。一般来说，可以部署哨兵集群来进一步提升判断准确率。&lt;/p></content></entry><entry><title type="text">Redis04——主从数据同步</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/redis/redis04_sync/"/><id>https://quakewang.github.io/tech/redis/redis04_sync/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2024-05-23T13:35:33+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="redis数据同步主从库实现数据一致">Redis——数据同步：主从库实现数据一致&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>在前面两篇博客，我们了解了 AOF 和 RDB，如果 Redis 发生了宕机，它们可以分别通过回放日志和重新读入 RDB 文件的方式恢复数据，从而尽可能地减少数据丢失，提升可靠性。&lt;/p>……</summary><content type="html">&lt;h1 id="redis数据同步主从库实现数据一致">Redis——数据同步：主从库实现数据一致&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>在前面两篇博客，我们了解了 AOF 和 RDB，如果 Redis 发生了宕机，它们可以分别通过回放日志和重新读入 RDB 文件的方式恢复数据，从而尽可能地减少数据丢失，提升可靠性。&lt;/p>
&lt;p>不过，之前讨论的范围都是在单机 Redis 的场景下，如果 Redis 集群中存在多个节点，如果某个实例宕机了，它在恢复期间是无法服务新来的数据请求，那么该如何处理呢？&lt;/p>
&lt;p>我们经常会听到 Redis 具有&lt;strong>高可靠性&lt;/strong>，这里指的主要是：&lt;strong>数据尽量少丢失&lt;/strong>和&lt;strong>服务尽量少中断&lt;/strong>。AOF 和 RDB 保证了前者，而对于后者，Redis 的做法是&lt;strong>增加副本冗余量&lt;/strong>，说简单点就是，将一份数据同时保存在多个实例上。这样一来，即使有一个实例出现了故障，需要过一段时间才能恢复，其他实例也可以对外提供服务，不会影响业务使用。&lt;/p>
&lt;p>但这会引发出一个新的问题：这么多的副本，它们之间的数据如何保持一致呢？数据的读写操作可以发送给所有的实例吗？&lt;/p>
&lt;h2 id="主从模式">主从模式&lt;/h2>
&lt;p>Redis 提供了主从库模式，以保证数据副本的一致，主从库之间采用的是读写分离的方式。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>读操作&lt;/strong>：主库、从库都可以接收；&lt;/li>
&lt;li>&lt;strong>写操作&lt;/strong>：首先到主库执行，然后，主库将同步写操作给从库。&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/22073d5a33bc440f222174790c6934e7cb64c31e/content/imag/tech/redis/04_Master_Slave.svg" alt="master-slave">&lt;/p>
&lt;p>那么，为什么要这样设计呢？&lt;/p>
&lt;p>道理很简单，如果在上图中，不管是主库还是从库，都能接收客户端的写操作，就会带来这样一个问题：如果客户端对同一个数据（例如 k1）前后修改了三次，每一次的修改请求都发送到不同的节点上执行，那么，这个数据在这三个节点上的副本就不一致了（分别是 v1、v2 和 v3）。在读取这个数据的时候，就可能读取到旧的值。&lt;/p>
&lt;p>如果我们非要保持数据在这三个节点上是一致的，就要涉及到加锁、实例间协商是否完成修改等一系列操作，但这会带来巨额的开销，对于整个集群来说是较大的负担。&lt;/p>
&lt;p>而主从模式如果采用了读写分离，所有数据的修改只会在主库上进行，不用协调三个节点。主库有了最新的数据后，会同步给从库，这样主从库的数据就是一致的。&lt;/p>
&lt;p>那么，主从库同步是如何完成的呢？主库数据是一次性传给从库，还是分批同步？要是主从库间的网络断连了，数据还能保持一致吗？带着这些思考，继续看下去吧。&lt;/p>
&lt;h2 id="第一次同步">第一次同步&lt;/h2>
&lt;p>首先，我们来看主从模式下的第一次同步是如何进行的，这也是 Redis 实例建立主从模式后的规定动作。&lt;/p>
&lt;p>当我们启动多个 Redis 实例的时候，它们相互之间就可以通过 replicaof 命令形成主库和从库的关系，之后会按照三个阶段完成数据的第一次同步。&lt;/p>
&lt;p>例如，现在有实例 1（IP：192.168.10.102）和实例 2（IP：192.168.10.103），在实例 2 上执行 &lt;code>replicaof 192.168.10.102 6379&lt;/code> 命令，实例 2 就会变成实例 1 的从库，并从实例 1 上复制数据。&lt;/p>
&lt;p>关于主从数据第一次同步的三个阶段，具体可以看下图：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/22073d5a33bc440f222174790c6934e7cb64c31e/content/imag/tech/redis/04_First_Sync.svg" alt="first-sync-process">&lt;/p>
&lt;p>第一阶段是主从之间建立连接、协商同步的过程，主要是为全量复制做准备。在这一步，&lt;strong>从库和主库建立起连接，并告诉主库即将进行同步，主库确认回复后，主从之间就可以开始同步了&lt;/strong>。&lt;/p>
&lt;p>具体来说，从库给主库发送 &lt;code>psync&lt;/code> 命令，表示要进行数据同步，主库根据这个命令的参数来启动复制。&lt;code>psync&lt;/code> 命令包括了&lt;strong>主库的 runID&lt;/strong> 和&lt;strong>复制进度的 offset&lt;/strong>两个参数。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>runID&lt;/strong>：每个 Redis 实例启动时都会自动生成的一个随机 ID，用来唯一标记这个实力。当从库和主库第一次复制时，因为不知主库的 runID，所以将 runID 设为“?”。&lt;/li>
&lt;li>&lt;strong>offset&lt;/strong>：此时设为 -1，表示第一次复制。&lt;/li>
&lt;/ul>
&lt;p>主库收到 &lt;code>psync&lt;/code> 命令后，会用 FULLRESYNC 响应命令并带上两个参数：主库 runID 和主库目前的复制进度 offset，返回给从库。从库接收到响应后，会记录下这两个参数。&lt;/p>
&lt;p>这里有个需要注意的地方，&lt;strong>FULLRESYNC 响应表示第一次复制采用的全量复制，也就是说，主库会把当前所有的数据都复制给从库&lt;/strong>。&lt;/p>
&lt;p>在第二阶段，&lt;strong>主库将所有数据同步给从库。从库接收到数据后，在本地完成数据加载&lt;/strong>。在这个过程依赖于内存快照生成的 RDB 文件。&lt;/p>
&lt;p>具体来说，主库执行完 &lt;code>bgsave&lt;/code> 命令，生成 RDB 文件，接着将文件发给从库。从库接收到 RDB 文件后，会先清空当前数据库，然后加载 RDB 文件。这是因为从库在通过 &lt;code>replicaof&lt;/code> 命令开始和主库同步前，可能保存了其他数据。为了避免之前数据的影响，从库需要先把当前数据库清空。&lt;/p>
&lt;p>在主库将数据同步给从库的过程中，主库不会被阻塞，仍然可以正常接收请求。否则，Redis 的服务就被中断了。但是，这些请求中的写操作并没有记录到刚刚生成的 RDB 文件中。为了保证主从库的数据一致性，就会把此时 replication buffer 中的修改操作发送给从库，从库再重新执行这些操作。这样一来，主从库就实现同步了。&lt;/p>
&lt;h2 id="主从级联模式">主从级联模式&lt;/h2>
&lt;p>通过分析主从库间第一次数据同步的过程，可以看到，一次全量复制中，对于主库来说，需要完成两个好事的操作：生成 RDB 文件和传输 RDB 文件。&lt;/p>
&lt;p>如果从库数据很多，而且都要和主库进行全量复制的话，就会导致主库忙于 fork 子进程生成 RDB 文件，进行数据全量同步。fork 这个操作会阻塞主线程处理正常请求，从而导致主库响应应用程序的请求速度变慢。此外，传输 RDB 文件也会占用主库的网络带宽，同样会给主库的资源使用带来压力。&lt;/p>
&lt;p>针对上述产生的问题，我们就可以选用“主 - 从 - 从”模式来解决。&lt;/p>
&lt;p>在刚才介绍的主从模式中，所有的从库都是和主库连接，所有的全量复制也都是和主库进行的。现在，我们可以通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力，以级联的方式分散到从库上。&lt;/p>
&lt;p>简单来说，在部署主从集群的时候，可以手动选择一个从库，比如内存资源配置较高的从库，用于级联其他的从库。然后，可以再选择一些从库，在这些从库上执行如下命令，让它们和刚才所选的从库，建立起主从关系。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">replicaof 所选从库的 IP &lt;span class="m">6379&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这样一来，这些从库就会知道，在进行同步时，不用再和主库进行交互了，只要和级联的从库进行写操作同步就行，从而减轻了主库上的压力。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/22073d5a33bc440f222174790c6934e7cb64c31e/content/imag/tech/redis/04_Master_Slave_Slave.svg" alt="m-s-s">&lt;/p>
&lt;p>到这里，我们了解了主从库间通过全量复制实现数据同步的过程，以及通过“主 - 从 -从”模式分担主库压力的方式。一旦主从库完成了全量复制，它们之间就会一直维护一个网络连接，主库会通过这个连接将后续收到的命令再同步给从库，这个过程也称为&lt;strong>基于长连接的命令传播&lt;/strong>，可以避免频繁建立连接的开销。&lt;/p>
&lt;h2 id="网络断连">网络断连&lt;/h2>
&lt;p>上面的过程看似一切顺利，但却有这不可忽视的风险点，那就是&lt;strong>网络断连或阻塞&lt;/strong>。如果网络断连，主从库之间就无法进行命令传播了，从库的数据自然也就没办法和主库保持一致，客户端就可能在从库中读取到旧数据，&lt;/p>
&lt;p>Redis 提供的解决办法是，网络断了之后，主从库会采用增量复制的方式继续同步，增量复制只会把主从库网络断连期间主库收到的命令同步给从库。&lt;/p>
&lt;p>那么，增量复制时，主从库之间是怎么保持同步的呢？原因是 repl_backlog_buff 这个缓冲区。我们先来看看它是如何用于增量命令的同步的。&lt;/p>
&lt;p>当主从库断连后，主库会把断连期间收到的写操作命令，写入 replication buffer，同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区。repl_backlog_buffer 是一个环形缓冲区，&lt;strong>主库会记录自己写到的位置，从库则会记录自己已经读到的位置&lt;/strong>。&lt;/p>
&lt;p>刚开始的时候，主库和从库的写读位置在一起，这算是它们的起始位置。随着主库不断接收新的写操作，它在缓冲区中的写位置会逐步偏离起始位置，我们通常用偏移量来衡量这个偏移距离的大小，对主库来说，对应的偏移量就是 master_repl_offset。主库接收的新写操作越多，这个值就会越大。&lt;/p>
&lt;p>同样，从库在复制完写操作命令后，它在缓冲区中的读位置也开始逐步偏移刚才的起始位置，此时，从库已复制的偏移量 slave_repl_offset 也在不断增加。正常情况下，这两个偏移量基本相等。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/22073d5a33bc440f222174790c6934e7cb64c31e/content/imag/tech/redis/04_Repl_Backlog_Buffer.svg" alt="repl-backlog-buffer">&lt;/p>
&lt;p>主从库的连接恢复之后，从库首先会给主库发送 psync 命令，并把自己当前的 slave_repl_offset 发给主库，主库会判断自己的 master_repl_offset 和 slave_repl_offset 之间的差距。&lt;/p>
&lt;p>在网络断连阶段，主库可能会收到新的写操作命令，所以，一般来说，master_repl_offset 会大于 slave_repl_offset。此时，主库只用把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从库就行。&lt;/p>
&lt;p>就像刚刚示意图的中间部分，主库和从库之间相差了 &lt;code>put d e&lt;/code> 和 &lt;code>put d f&lt;/code> 两个操作，在增量复制时，主库只需要把它们同步给从库，就行了。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/22073d5a33bc440f222174790c6934e7cb64c31e/content/imag/tech/redis/04_Network_Error.svg" alt="network-error">&lt;/p>
&lt;p>不过，这里还有一个地方需要注意一下，因为 repl_backlog_buffer 是一个环形缓冲区，所以在缓冲区写满后，主库会继续写入，此时，就会覆盖了之前写入的操作。&lt;strong>如果从库的读取速度比较慢，就有可能导致从库还未读取的操作被主库新写的操作覆盖了，这会导致主从库间的数据不一致&lt;/strong>。&lt;/p>
&lt;p>因此，就需要想办法避免这一情况，通常来说，可以调整 &lt;strong>repl_backlog_size&lt;/strong> 这个参数。这个参数和所需的缓冲空间大小有关。缓冲空间的计算公式是：缓冲空间大小 = 主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度 * 操作大小。在实际应用中，考虑到可能存在一些突发的请求压力，我们通常需要把这个缓冲空间扩大一倍，即 repl_backlog_size = 缓冲空间大小 * 2，这也就是 repl_backlog_size 的最终值。&lt;/p>
&lt;p>举个例子，如果主库每秒写入 2000 个操作，每个操作的大小为 2KB，网络每秒能传输 1000 个操作，那么，有 1000 个操作需要缓冲起来，这就至少需要 2MB 的缓冲空间。否则，新写的命令就会覆盖掉旧操作了。为了应对可能的突发压力，我们最终把 repl_backlog_size 设为 4MB。&lt;/p>
&lt;p>这样一来，增量复制时主从库的数据不一致风险就降低了。不过，如果并发请求量非常大，连两倍的缓冲空间都存不下新操作请求的话，此时，主从库仍然可能会不一致。针对这种情况，就可以根据 Redis 所在服务器的内存资源继续增加 repl_backlog_size 的值。或者使用&lt;strong>切片集群&lt;/strong>来分担单个主库的请求压力（关于切片集群留个坑）。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>在这篇博客中，主要概括了 Redis 主从库同步的基本原理，总结起来，有三种模式：全量复制、基于长连接的命令传播，以及增量复制。&lt;/p>
&lt;p>全量复制虽然耗时，但对于从库来说，如果是第一次同步，全量复制是无法避免的，所以尽可能保证一个 Redis 实例的数据库不要太大，一个实例大小在几 GB 比较合适，这样可以减少 RDB 文件生成、传输和重新加载的开销。另外，为了避免多个从库同时和主库进行全量复制，给主库过大的同步压力，也可以采用“主 - 从 - 从”级联模式。来缓解主库的压力。&lt;/p>
&lt;p>长连接复制是主从库正常运行后的常规同步阶段。在这个阶段中，主从库之间通过命令传播实现同步。不过，这期间如果遇到了网络断连，增量复制就派上用场了。其中需要注意的点事 repl_backlog_size 这个配置参数。如果它配置得过小，在增量复制阶段，可能会导致从库的复制进度赶不上主库，进而导致从库重新进行全量复制。所以，通过调大这个参数，可以减少从库在网络断连时全量复制的风险。&lt;/p>
&lt;h2 id="延伸">延伸&lt;/h2>
&lt;p>AOF 记录的操作命令更全，相比于 RDB 丢失的数据更少。那么，为什么主从库间的复制不使用 AOF 呢？&lt;/p>
&lt;ul>
&lt;li>RDB 文件是二进制文件，无论是要把 RDB 写入磁盘，还是要通过网络传输 RDB，IO 效率都比记录和传输 AOF 的高；&lt;/li>
&lt;li>在从库进行恢复时，用 RDB 的恢复效率要高于 AOF。&lt;/li>
&lt;/ul></content></entry><entry><title type="text">Redis03——RDB 快照</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/redis/redis03_rdb/"/><id>https://quakewang.github.io/tech/redis/redis03_rdb/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2024-05-09T14:29:45+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="redisrdb-内存快照">Redis——RDB 内存快照&lt;/h1>
&lt;h1 id="前言">前言&lt;/h1>
&lt;p>上一篇内容，我们了解了 Redis 避免数据丢失的 AOF 方法。使用 AOF 的好处是，每次执行只需要记录操作命令，需要持久化的数据量不大。通常来说，只要不采用 Always 的持久化策略，都不会对性能造成太大的影响。&lt;/p>……</summary><content type="html">&lt;h1 id="redisrdb-内存快照">Redis——RDB 内存快照&lt;/h1>
&lt;h1 id="前言">前言&lt;/h1>
&lt;p>上一篇内容，我们了解了 Redis 避免数据丢失的 AOF 方法。使用 AOF 的好处是，每次执行只需要记录操作命令，需要持久化的数据量不大。通常来说，只要不采用 Always 的持久化策略，都不会对性能造成太大的影响。&lt;/p>
&lt;p>但，也正因为记录的是操作命令，而不是实际数据，所以，用 AOF 方法恢复数据的时候，需要把记录的日志信息都逐一执行一遍。如果日志信息过多，那么在恢复的过程中也将会非常耗时，有可能会就影响到正常使用。那么，有没有什么办法既可以保证可靠性，还能在宕机时快速恢复的方法呢？&lt;/p>
&lt;p>答案就是——&lt;strong>内存快照&lt;/strong>。按照里面意思来理解就是，指内存中的数据在某一时刻的状态记录。这就类似于照片，当按下快门的时候，就会将那一瞬间给记录下来。&lt;/p>
&lt;p>对 Redis 来说，它实现类似于照片记录效果的方式，就是把某一时刻的状态以文件的形式写到磁盘上，也就是快照。这样一来，即使宕机，快照文件也不会丢失，数据的可靠性也就得到了保证。这个快照文件就称为 RDB 文件，其中，RDB 就是 Redis DataBase 的缩写。&lt;/p>
&lt;p>和 AOF 相比，RDB 记录的是某一时刻的数据，并不是操作，所以，在做恢复数据的时候，可以直接把 RDB 文件读入内存，很快就可以完成恢复。但既然内存快照这么好，是不是就可以完全取代 AOF 了呢？肯定不是这样，我们可以尝试从以下两个方面进行考虑：&lt;/p>
&lt;ul>
&lt;li>对哪些数据做快照？&lt;/li>
&lt;li>做快照时，数据还能增删改吗？&lt;/li>
&lt;/ul>
&lt;p>第一个问题，主要会牵涉到快照的执行效率；后一个则关系到 Redis 是否被阻塞，能否同时正常处理请求。&lt;/p>
&lt;p>这么说可能会感觉有点抽象，换个方式理解，还是以拍照的例子来看。&lt;/p>
&lt;ul>
&lt;li>如何取景构图？也就是说，照片中会有哪些元素；&lt;/li>
&lt;li>按快门的时候，取景框中的元素有变化，对焦失败怎么办？&lt;/li>
&lt;/ul>
&lt;p>下面就针对上面这两个关键问题来进行展开。&lt;/p>
&lt;h2 id="数据快照的方式">数据快照的方式&lt;/h2>
&lt;p>Redis 的数据都在内容中，为了提供所有数据的可靠性保证，它执行的是&lt;strong>全量快照&lt;/strong>，直接把内存中的所有数据记录到磁盘中，这就类似于给一整个班级的人拍合照，把每一个人都拍进照片里。这样做的好处是，一次性记录了所有数据，一个都不少。&lt;/p>
&lt;p>我们知道，当给一个人拍照时，只用注意这一个人就够了，但是，拍多个人合影的时候，就需要协调全部人的位置、状态等等，这也就会更耗时耗力。而且，全量数据越多，RDB 文件就越大，往磁盘上写数据的时间开销就越大。&lt;/p>
&lt;p>对于 Redis 而言，它的单线程模式就决定了要尽量避免所有会阻塞主线程的操作。所以，针对任何操作都要考虑：&lt;strong>是否会阻塞主线程&lt;/strong>。这也就牵涉到 RDB 文件的生成是否会降低 Redis 的性能。&lt;/p>
&lt;p>Redis 提供了两个命令来生成 RDB 文件，分别是 save 和 bgsave。&lt;/p>
&lt;ul>
&lt;li>save：在主线程中执行，会导致阻塞；&lt;/li>
&lt;li>bgsave：创建一个子进程，专门用于写入 RDB 文件，避免了主线程的阻塞，这也是 Redis RDB 文件生成的默认配置。&lt;/li>
&lt;/ul>
&lt;p>看见 &lt;code>bgsave&lt;/code> 的时候，心里就有底了。我们就可以通过 &lt;code>bgsave&lt;/code> 命令来执行全量快照，这既保证了数据的可靠性，又避免了对 Redis 的性能造成影响。&lt;/p>
&lt;p>接下来，我们要关注的问题是，在对内存数据做快照时，这些数据还能被修改吗？这个问题非常重要，因为如果数据能被修改，那就意味着 Redis 还能正常处理写操作。否则所有写操作都得等到快照完了才能执行，性能会大打折扣。&lt;/p>
&lt;h2 id="快照时数据修改">快照时数据修改？&lt;/h2>
&lt;p>还是来看拍照这个例子，在给别人拍照的时候，如果对方在按快门的时候动了，那么这张照片也就拍糊，属于不可用的，就需要重拍。所以，我们当然希望对方保持不动，对于内存快照而言也是同理。&lt;/p>
&lt;p>举个例子。我们在时刻 t 给内存做快照，假设内存数据量是 4GB，磁盘的写入带宽是 0.2GB/s，简单来说，至少需要 20s（4/0.2 = 20）才能做完。如果在时刻 t+5s 时，一个还没有被写入磁盘的内存数据 A，被修改成了 A'，那么就会破坏快照的完整性，因为 A' 不是时刻 t 时的状态。因此，和拍照类似，我们在做快照时也不希望数据“动”，也就是不能被修改。&lt;/p>
&lt;p>但是，如果快照执行期间数据不能被修改，是会有潜在问题的。对于刚刚的例子来说，在做快照的 20s 时间里，如果这 4GB 的数据都不能被修改，Redis 就不能处理对这些数据的写操作，那无疑就会给业务服务造成巨大的影响。&lt;/p>
&lt;p>这个时候，你可能会说，不是有 bgsave 避免阻塞的嘛。这就是属于犯了一个经典的错误：&lt;strong>避免阻塞和正常处理写操作并不是一回事&lt;/strong>。此时，主线程的确没有阻塞，可以正常接收请求，但是，为了保证快照完整性，它只能处理读操作，因为不能修改正在执行快照的数据。&lt;/p>
&lt;p>为了快照而暂停写操作，这明显是捡了芝麻丢了西瓜，是不能接受的。所以这个时候，Redis 就会借助操作系统提供的写时复制（Copy-On-Write, COW）技术，在执行快照的同时，正常处理写操作。&lt;/p>
&lt;p>简单来说，&lt;code>bgsave&lt;/code> 子进程是有主线程 fork 生成的，可以共享主线程的所有内存数据。&lt;code>bgsave&lt;/code> 子进程运行后，开始读取主线程的内存数据，并把它们写入 RDB 文件。&lt;/p>
&lt;p>此时，如果主线程对这些数据也都是读操作，比如图中的键值对 A，那么，主线程和 &lt;code>bgsave&lt;/code> 子进程相互不影响。但是，如果主线程要修改一块数据，像图中的键值对 C，那么这块数据就会被复制一份，生成该数据的副本，也就是键值对 C'。然后，主线程在这个数据副本上进行修改。同时，&lt;code>bgsave&lt;/code> 子进程可以继续把原来的数据（键值对 C）写入 RDB 文件。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/5ff29115a4d799cbeda2f5aeed45c1e02d0f8ab4/content/imag/tech/redis/03_RDB_COW.svg" alt="COW">&lt;/p>
&lt;p>这既保证了快照的完整性，也允许主线程同时对数据进行修改，避免了对正常业务的影响。&lt;/p>
&lt;p>看到这里，我们就解决了对“哪些数据做快照”以及“做快照时数据能否修改”这两大问题：Redis 会使用 &lt;code>bgsave&lt;/code> 对当前内存中的所有数据做快照，这操作师子进程在后台完成的，这就允许主线程同时可以修改数据。&lt;/p>
&lt;h2 id="快照频率">快照频率&lt;/h2>
&lt;p>现在，我们再来看另外一个问题：多久做一次快照？我们在拍照的时候，有时候会使用“连拍”，可以记录人或物连续多个瞬间的状态。那么，快照也适合“连拍”吗？&lt;/p>
&lt;p>对于快照来说，所谓“连拍”就是指连续地做快照，这样一来，快照的间隔时间变得很短，即使某一时刻发生宕机，丢失的数据也不会太多。但我们都知道，凡事都有两面性，如果生成快照的频率过快，则会导致性能和存储资源的消耗。所以，快照时间间隔的选取也是很关键的。&lt;/p>
&lt;p>如下图所示，先在 T0 时刻做一次快照，然后又在 T0+t 时刻再做一次快照，在这期间，数据块 2 和 6 被修改了。如果在 t 这段时间内，机器宕机了，那么只能按照 T0 时刻的快照进行恢复。但由于数据快 2 和 6 的修改值没有快照记录，就无法恢复了。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/5ff29115a4d799cbeda2f5aeed45c1e02d0f8ab4/content/imag/tech/redis/03_RDB_Down.svg" alt="Data_Lose">&lt;/p>
&lt;p>所以，要想尽可能避免数据丢失，t 值就要尽可能小。那么，t 值可以小到什么程度呢？是不是可以每秒做一次快照？毕竟，每次快照都是由 &lt;code>bgsave&lt;/code> 子进程在后台执行，也不会阻塞主线程。&lt;/p>
&lt;p>但这种想法是错误的。虽然 &lt;code>bgsave&lt;/code> 执行时不阻塞主线程，但是，&lt;strong>如果频繁地执行全量快照，也会带来两方面的开销&lt;/strong>。&lt;/p>
&lt;p>一方面，频繁将全量数据写入磁盘，会给磁盘带来很大的 IO 压力，多个快照竞争有限的磁盘带宽，前一个快照还没有结束，后面的又来了，容易造成恶性循环。&lt;/p>
&lt;p>另一方面，&lt;code>bgsave&lt;/code> 子进程需要通过 fork 操作从主线程创建出来。虽然，子进程在创建后不会阻塞主线程，但 fork 这个创建过程本身会阻塞主线程，而且主线程的内存越大，阻塞时间越长。如果频繁 fork 出 &lt;code>bgsave&lt;/code> 子进程，就会频繁阻塞主线程了。因为 Redis 中如果有一个 &lt;code>bgsave&lt;/code> 在运行，就不会再启动第二个 &lt;code>bgsave&lt;/code> 子进程。&lt;/p>
&lt;p>那么针对刚才这种情况，有什么比较好的解决办法呢？如果做过数据集成与同步业务的同学，可能已经想到答案了。既然每次全量同步快照的成本开销较大，那么我们可以这样做：做一次全量快照后，之后对于更新修改的数据进行增量快照记录，这样可以避免每次全量快照的开销。&lt;/p>
&lt;p>在第一次做完全量快照后，T1 和 T2 时刻如果再做快照，只需要将被修改的数据写入快照文件就行。但这么做的前提是，我们需要弄清楚哪些数据被修改了，这一步骤会使用额外的元数据信息去记录哪些数据被修改了，这也会带来额外的空间开销问题。如下图所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/5ff29115a4d799cbeda2f5aeed45c1e02d0f8ab4/content/imag/tech/redis/03_Increment_Data.svg" alt="Increment_Data">&lt;/p>
&lt;p>如果每一个键值对的修改动作一个记录，那么，如果有 10w 个被修改的键值对，就需要有 10w 条额外的记录。而且，有的时候，键值对非常小，比如只有 32 字节，而记录它被修改的元数据信息，可能就需要 8 字节，这样一来，为了记录这些修改，引入的额外空间开销就会比较大。这对于非常重视内存资源的 Redis 来说，有些得不偿失。&lt;/p>
&lt;p>到这里，可以发现，虽然与 AOF 相比，快照的恢复速度快，但是，快照的频率不好把握，如果频率太低，两次快照间一旦宕机，就可能有比较多的数据丢失。如果频率太高，又会产生额外开销。那么，还有什么办法既能利用 RDB 的快速恢复，又能以较小的开销做到尽量少丢失数据呢？&lt;/p>
&lt;p>&lt;strong>混合使用 AOF 日志和内存快照&lt;/strong>：简单来说，内存快照以一定的频率执行，在两次快照之间，使用 AOF 日志记录这期间的所有命令操作。这样一来，快照不用很频繁地执行，这就避免了频繁 fork 对主线程的影响。而且，AOF 日志也只用记录两次快照间的操作。也就是说，不需要记录所有操作，也就不会出现文件过大，可以避免重写带来的开销。&lt;/p>
&lt;p>如下图所示，T1 和 T2 时刻的修改，用 AOF 日志记录，等到第二次做全量快照时，就可以清空 AOF 日志，因为此时的修改都已经记录到快照中了，恢复时就不再用日志了。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/5ff29115a4d799cbeda2f5aeed45c1e02d0f8ab4/content/imag/tech/redis/03_AOF_RDB.svg" alt="AOF_RDB">&lt;/p>
&lt;p>这个方法既能利用 RDB 文件快速恢复的好处，又能使用到 AOF 只记录操作命令的简单优势。（小孩子才做选择，成年人肯定是“全都要”。）&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>在本篇博客中，主要讲解了 Redis 使用内存快照避免数据丢失的方法。这个方法的优势在于，可以快速恢复故障数据，也就是只需要把 RDB 文件直接读入内存，这就避免了 AOF 需要顺序、逐一重新执行操作命令带来的低效性能问题。&lt;/p>
&lt;p>不过，内存快照也有其局限性。如果每次都是全量同步快照信息，不可避免地会带来大量的资源消耗。虽然 Redis 设计了 bgsave 和写时复制方式，尽可能减少了内存快照对正常读写的影响，但是，频繁进行快照仍然是不太能接受的。而混合使用 AOF 和 RDB，正好可以取两者之长，以较小的性能开销保证数据可靠性和性能。&lt;/p>
&lt;p>最后，关于 AOF 和 RDB 的选择可以根据下面三条参考建议：&lt;/p>
&lt;ul>
&lt;li>数据不能丢失时，内存快照和 AOF 的混合使用是一个很好的选择；&lt;/li>
&lt;li>如果允许分钟级别的数据丢失，可以只使用 RDB；&lt;/li>
&lt;li>如果只用 AOF，优先使用 everysec 的配置选项，因为它在可靠性和性能之间取了一个平衡。&lt;/li>
&lt;/ul></content></entry><entry><title type="text">Redis02——AOF 日志</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/redis/redis02_aof/"/><id>https://quakewang.github.io/tech/redis/redis02_aof/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2024-04-09T09:58:35+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="redisaof-日志">Redis——AOF 日志&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>聊起 Redis 的应用场景，大部分开发者第一反应可能会是：&lt;strong>缓存&lt;/strong>，因为它可以把后端数据库中的数据存储在内存中，内存的访问效率要比硬盘高的多，所以直接从内存中读取数据，响应速度会非常快。&lt;/p>……</summary><content type="html">&lt;h1 id="redisaof-日志">Redis——AOF 日志&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>聊起 Redis 的应用场景，大部分开发者第一反应可能会是：&lt;strong>缓存&lt;/strong>，因为它可以把后端数据库中的数据存储在内存中，内存的访问效率要比硬盘高的多，所以直接从内存中读取数据，响应速度会非常快。&lt;/p>
&lt;p>这也是 Redis 目前在业务场景中的普遍使用案例，但其中也暴漏出一个不可忽视的问题：&lt;strong>一旦服务器宕机，内存中的数据将会全部丢失&lt;/strong>。那么该怎么解决这个问题呢？&lt;/p>
&lt;p>比较容易想到的解决办法就是，既然 Redis 存储的数据是从后端数据库中来的，那么直接从数据库中恢复不就好了嘛。但这也会引发以下的问题：&lt;/p>
&lt;ul>
&lt;li>在恢复的过程中，可能需要频繁地访问数据库，会给数据库带来较大的 IO 压力；&lt;/li>
&lt;li>这些数据是从数据库中读取出来，性能肯定比不上 Redis 中读取，会影响使用这些数据的应用程序。&lt;/li>
&lt;/ul>
&lt;p>由此可见，对于 Redis 来说，实现数据的持久化，避免从后端数据库进行恢复，是至关重要的。目前，Redis 的持久化主要有两种方式，分别是 AOF（Append Only File）日志和 RDB 快照。这篇博客先来看第一种——&lt;strong>AOF 日志&lt;/strong>。&lt;/p>
&lt;h2 id="aof-日志">AOF 日志&lt;/h2>
&lt;p>在传统的后端关系型数据库中对于日志的处理一般都是写前日志（Writer Ahead Log，WAL），简单来说，就是在实际写数据之前，先把修改的数据记录到日志文件中，如果后期数据库出现故障可以方便进行恢复。与此不同的是，AOF 日志刚好相反，它是写后日志，“写后”的意思是 Redis 先执行命令，把数据写入内存，然后才记录日志，如下图所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/f0f0c645f8be9ced36342a589fcea03cfcdd6f00/content/imag/tech/redis/02_AOF.svg" alt="AOF_Log">&lt;/p>
&lt;p>看到这里，你可能会感到疑惑，为什么 AOF 要先执行命令再记录日志呢？回答这个问题之前，先来一起看看 AOF 里面记录了哪些内容。&lt;/p>
&lt;p>传统数据库的日志，例如 redo log（重做日志），记录的是修改后的数据，而 AOF 里记录的是 Redis 收到的每一条命令，这些命令是以文本形式保存的。&lt;/p>
&lt;p>我们以 Redis 收到“set key1 value1”命令后记录的日志为例，看看 AOF 日志的内容。其中，&lt;code>*3&lt;/code> 表示当前命令有三个部分，每部分都是由 &lt;code>$+ 数字&lt;/code> 开头，后面紧跟着具体的命令、键或值。这里，“数字”表示这部分中的命令、键或值一共有多少字节。例如，&lt;code>$3 set&lt;/code> 表示这部分有 3 个字节，对应的也就是 &lt;code>set&lt;/code> 命令。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/f0f0c645f8be9ced36342a589fcea03cfcdd6f00/content/imag/tech/redis/02_AOF-Info.svg" alt="AOF_info">&lt;/p>
&lt;p>不过，为了避免额外的检查开销，Redis 在向 AOF 里面记录日志的时候，并不会先去对这些命令进行语法检查。所以，如果先记日志再执行命令的话，如果记录了错误的命令，Redis 在使用日志恢复数据时，就可能会出错。&lt;/p>
&lt;p>而写后日志这种方式，就是先让系统执行命令，只有命令被成功执行，才会被记录在日中，否则，系统就会直接向客户端报错。所以，Redis 使用写后日志这一方式的好处就是：可以&lt;strong>避免出现记录错误命令的情况&lt;/strong>。除此之外，AOF 还有一个好处就是：它是在命令执行后才记录日志，所以不会&lt;strong>阻塞当前线程的写操作&lt;/strong>。&lt;/p>
&lt;p>但我们知道凡事都有两面性，AOF 也有如下两个潜在的风险。&lt;/p>
&lt;p>首先，如果刚执行完一条命令，还没来得及记录日志服务器就宕机了，那么这个命令和相应的数据就会有丢失的风险。如果此时 Redis 是用作缓存，还可以从后端数据库重新读入数据进行恢复，但是，如果 Redis 是直接用作数据库的话，此时，因为命令没有记录日志，所有就无法通过日志进行恢复了。&lt;/p>
&lt;p>其次，AOF 虽然避免了对于当前命令的阻塞，但可能会给下一个操作带来阻塞风险。原因也很简单，AOF 日志也是在主线程中执行的，如果在把日志写入磁盘时，IO 压力较大，就会导致写盘很慢，进而导致后续的操作无法正常执行。&lt;/p>
&lt;p>诶，到这里如果细心一点，就会发现，刚才所描述的两个风险都是和 AOF 写回磁盘的时间点有关。这也意味着，如果我们能够控制一个写命令执行完 AOF 日志写回磁盘的时机，这两个问题也就迎刃而解咯。&lt;/p>
&lt;h2 id="写回策略">写回策略&lt;/h2>
&lt;p>其实，对于这个问题，AOF 机制给我们提供了三个选择，也就是 AOF 配置项 &lt;code>appendfsync&lt;/code> 的三个可选值：&lt;/p>
&lt;ul>
&lt;li>&lt;code>Always&lt;/code>，同步写回：每个命令执行完成之后，立刻同步地将日志写回磁盘；&lt;/li>
&lt;li>&lt;code>Everysec&lt;/code>，每秒写回：每个命令执行完，只是先把日志写到 AOF 文件的内存缓冲区，每隔一秒把缓冲区中的内容写入磁盘；&lt;/li>
&lt;li>&lt;code>No&lt;/code>，操作系统控制的写回：每个写命令执行完，只是先把日志写到 AOF 文件的内存缓冲区，由操作系统决定何时将缓冲区内容写回磁盘。&lt;/li>
&lt;/ul>
&lt;p>针对避免主线程阻塞和减少数据丢失问题，这三种写回策略都无法做到两全其美。我们来分析下其中的原因：&lt;/p>
&lt;ul>
&lt;li>“同步写回”可以做到基本不丢数据，但是它在每一个写命令后都有一个慢速的落盘操作，不可避免地会影响主线程性能；&lt;/li>
&lt;li>“每秒写回”采用一秒写回一次的频率，避免了“同步写回”的性能开销，虽然减少了对系统性能的影响，但是如果发生宕机，上一秒内未落盘的命令操作仍然会丢失。所以，这也只能算是一种策略上的取舍；&lt;/li>
&lt;li>虽然“操作系统控制的写回”在写完缓冲区后，就可以继续执行后续的命令，但是落盘的时机已经不在 Redis 把握之中了，只要 AOF 记录没有写回磁盘，一旦宕机对应的数据就丢失了；&lt;/li>
&lt;/ul>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: center">配置项&lt;/th>
&lt;th style="text-align: center">写回策略&lt;/th>
&lt;th style="text-align: center">优点&lt;/th>
&lt;th style="text-align: center">缺点&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: center">Always&lt;/td>
&lt;td style="text-align: center">同步写回&lt;/td>
&lt;td style="text-align: center">可靠性高，数据基本不丢失。&lt;/td>
&lt;td style="text-align: center">每个写命令都要落盘，性能影响较大。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">Everysec&lt;/td>
&lt;td style="text-align: center">每秒写回&lt;/td>
&lt;td style="text-align: center">性能适中。&lt;/td>
&lt;td style="text-align: center">宕机时丢失 1 秒内的数据。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">No&lt;/td>
&lt;td style="text-align: center">操作系统控制的写回&lt;/td>
&lt;td style="text-align: center">性能好。&lt;/td>
&lt;td style="text-align: center">宕机时丢失数据较多。&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>到这里，我们就可以根据系统对高性能和高可靠性的要求，来选择使用哪种写回策略了。总结一下就是：想要获得高性能，就选择 No 策略；如果想要得到高可靠性保证，就选择 Always 策略；如果允许数据有一点丢失，又希望性能别受太大影响的话，那么就选择 Everysec 策略。&lt;/p>
&lt;p>但是，按照系统的性能需求选定了写回策略，并不是“以逸待劳”了。毕竟，AOF 是以文件的形式在记录接收到的所有写命令。随着接收到的写命令越多，AOF 文件也会越来越大。这也就意味着，一定要小心 AOF 文件过大带来的性能问题。&lt;/p>
&lt;p>这里的“性能问题”，主要在于以三个方面：&lt;/p>
&lt;ul>
&lt;li>文件系统本身对文件大小有限制，无法保存过大的文件；&lt;/li>
&lt;li>如果文件太大，之后再往里面追加命令记录的话，效率也会变低；&lt;/li>
&lt;li>如果服务器发生故障，如果日志文件过大，在恢复过程会非常缓慢，影响 Redis 正常使用。&lt;/li>
&lt;/ul>
&lt;p>所以针对以上问题，我们要采取一定的控制手段，那也就是 &lt;strong>AOF 重写机制&lt;/strong>。&lt;/p>
&lt;h2 id="aof-重写机制">AOF 重写机制&lt;/h2>
&lt;p>AOF 重写机制就是在重写时，Redis 根据数据库的现状创建一个新的 AOF 文件，也就是说，读取数据库中的所有键值对，然后对每一个键值对用一条命令记录它的写入。emmm 这么说的话，可能觉得有点抽象，看个栗子：当读取了键值对 &lt;code>&amp;quot;key1&amp;quot;: &amp;quot;value1&amp;quot;&lt;/code> 之后，重写机制会记录 &lt;code>set key1 value1&lt;/code> 这条命令。这样，当需要恢复时，可以重新执行该命令，实现 &lt;code>&amp;quot;key1&amp;quot;: &amp;quot;value1&amp;quot;&lt;/code> 的写入。&lt;/p>
&lt;p>为什么重写机制可以把日志文件变小呢？实际上，重写机制具有“多变一”的功能。简单来说就是，旧日志文件中的多条命令，在重写后的新日志中变成了一条命令。AOF 文件是以追加的方式，逐一记录接收到的写命令的。当一个键值对被多条写命令反复修改时，AOF 文件会记录相应的多条命令。但是，在重写的时候，是根据这个键值对当前最新的状态，为它生成对应的写入命令。这样一来，一个键值对在重写日志中只用一条命令就行了，而且，在日志恢复时，只用执行这条命令就可以直接完成键值对的写入了。&lt;/p>
&lt;p>不过，虽然 AOF 重写后，日志文件会缩小，但是，要把整个数据库的最新数据的操作日志都写会磁盘，也是非常耗时的，这个时候就带来新的问题——“重写会不会阻塞主线程？”&lt;/p>
&lt;p>和 AOF 日志由主线程写回不同，重写过程是由后台子进程 &lt;code>bgrewriteaof&lt;/code> 来完成的，这也是为了避免阻塞主线程，导致数据库性能下降。&lt;/p>
&lt;p>对应过程主要如下：&lt;/p>
&lt;p>每次执行重写时，主线程 fork 出后台的 &lt;code>bgrewriteaof&lt;/code> 子进程。此时，fork 会把主线程的内存拷贝一份给 &lt;code>bgrewriteaof&lt;/code> 子进程，这里面就包含了数据库的最新数据。然后，&lt;code>bgrewriteaof&lt;/code> 子进程就可以在不影响主线程的情况下，逐一把拷贝的数据写成操作，记入重写日志。&lt;/p>
&lt;p>在这个过程中，由于主线程未阻塞，仍然可以处理新来的操作。此时，如果有写操作，第一处日志就是指正在使用的 AOF 日志，Redis 会把这个操作写到它的缓冲区。这样一来，即使宕机了，这个 AOF 日志的操作仍然是齐全的，可以用于恢复。&lt;/p>
&lt;p>还有一点就是新的 AOF 重写日志，这个操作也会被写到日志的缓冲区。这样，重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后，重写日志记录的这些最新操作也会写入新的 AOF 文件，以保证数据库最新状态的记录。这个时候，我们就可以使用新的 AOF 文件替代旧文件了。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/f0f0c645f8be9ced36342a589fcea03cfcdd6f00/content/imag/tech/redis/02_AOF-Bgrewriteaof.svg" alt="AOF_Rewrite">&lt;/p>
&lt;p>总结来说，每次 AOF 重写时，Redis 会先执行一个内存拷贝，用于重写；然后，使用两个日志保证在重写过程中，新写入的数据不会丢失。而且，因为 Redis 采用额外的线程进行数据重写，所以，这个过程并不会阻塞主线程。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>在这篇博客中，简单介绍了 Redis 用于避免数据丢失的 AOF 方法。这个方法通过逐一记录操作命令，在恢复时再逐一执行命令的方式，保证了数据的可靠性。&lt;/p>
&lt;p>这个方法看似“简单”，但也是充分考虑了对 Redis 性能的影响。总结来说，它提供了 AOF 日志的三种写回策略，分别是 Always、Everysec 和 No，这三种策略在可靠性上是从高到低，而在性能上则是从低到高。&lt;/p>
&lt;p>此外，为了避免日志文件过大，Redis 还提供了 AOF 重写机制，直接根据数据库里数据的最新状态，生成这些数据的插入命令，作为新日志。这个过程通过后台线程完成，避免了对主线程的阻塞。&lt;/p>
&lt;p>其中，三种写回策略体现了系统设计中的一个重要原则，即 trade-off，或者称为“取舍”，指的就是在性能和可靠性保证之间做取舍。其实，放在生活中也是同理，我们总是在一边得到，一边失去。现如今这个时代，做好取舍，活得自洽就好~~&lt;/p></content></entry><entry><title type="text">Redis01——数据结构篇</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/redis/redis01_datastructure/"/><id>https://quakewang.github.io/tech/redis/redis01_datastructure/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2024-03-12T23:39:01+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="从-redis-的数据结构开始聊起">从 Redis 的数据结构开始聊起&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>现如今提到 Redis，大众的第一印象应该就是“快”，也正是由于 Redis 的快的特性，使得其可以适用于分布式缓存、键值对数据存储等场景。但反过来想一下，为什么 Redis 能有如此突出的表现呢？它是如何在相关领域独占鳌头的呢？&lt;/p>……</summary><content type="html">&lt;h1 id="从-redis-的数据结构开始聊起">从 Redis 的数据结构开始聊起&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>现如今提到 Redis，大众的第一印象应该就是“快”，也正是由于 Redis 的快的特性，使得其可以适用于分布式缓存、键值对数据存储等场景。但反过来想一下，为什么 Redis 能有如此突出的表现呢？它是如何在相关领域独占鳌头的呢？&lt;/p>
&lt;p>诶，这个时候可能懂点皮毛的程序员就会说，因为它是&lt;strong>内存数据库&lt;/strong>，所有的操作都是在内存上完成的，内存的访问速度本身就比磁盘要快。这么回答当然没错，但是如果再深入一点的话呢？这样就要归功于 Redis 的&lt;strong>数据结构&lt;/strong>。在 Redis 中键值对是按一定的数据结构来组织的，所以说用户对于键值对的操作，最终就是对数据结构进行增删改查的操作。&lt;/p>
&lt;p>如果上手使用过 Redis 的话，我们可以知道常见的有：String、List、Hash、Set 和 Sorted Set。不过，这些都只是 Redis 键值对中值的数据类型，说通俗点就是数据的保存形式。但在这篇博客中，我们来一起探究其底层的实现。&lt;/p>
&lt;p>简单来说，底层数据结构一共有 6 种，分别是简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组。它们和数据类型的对应关系如下图所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/da5705cd5fa4db4c6207e5fabcbc756f98bd2b1c/content/imag/tech/redis/01_DataStructure.svg" alt="Redis_DataSture">&lt;/p>
&lt;p>可以看到，String 类型的底层实现只有一种数据结构，也就是简单动态字符串。而 List、Hash、Set 和 Sorted Set 这四种数据类型，都有两种底层实现结构。通常情况下，我们会把这四种类型称为集合类型，它们的特点是&lt;strong>一个键对应了一个集合的数据&lt;/strong>。&lt;/p>
&lt;p>看到这里，我们可以想一下，这些数据结构都是值的底层实现，键和值本身之间用什么结构组织呢？为什么集合类型有那么多的底层结构，它们之间有何区别呢？下面我们就这两个问题来展开讨论。&lt;/p>
&lt;h2 id="键和值用什么结构">键和值用什么结构？&lt;/h2>
&lt;p>先说答案：&lt;strong>Redis 的键和值之前使用哈希表来组织和存储&lt;/strong>。在 Redis 中，每个键都与一个值相关联，并且这些键值对都存储在哈希表中。一个哈希表，其实就是一个数组，数组的每个元素称为一个哈希桶，通过哈希函数将键映射到哈希桶中，从而快速地访问和检索数据。所以，我们常说，一个哈希表是由多个哈希桶组成，每个哈希桶中保存了键值对数据。&lt;/p>
&lt;p>这个时候可能有的读者就会想到：“如果值是集合类型的话，作为数组元素的哈希桶该怎么保存呢？”其实，这个答案很简单，哈希桶中保存的元素并不是这个值本身，而是指向这个值的指针。也就是说，不管值是 String 还是集合类型，哈希桶中的元素都是指向它们的指针。&lt;/p>
&lt;p>我们可以借助下图来理解：哈希桶的中 entry 元素中保存了 &lt;code>*key&lt;/code> 和 &lt;code>*value&lt;/code> 指针，分别指向了实际的键和值，这样做的好处是，即使值是一个集合，也可以通过 &lt;code>*value&lt;/code> 指针被查找到。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/da5705cd5fa4db4c6207e5fabcbc756f98bd2b1c/content/imag/tech/redis/01_Hash-Bucket.svg" alt="Hash_Bucket">&lt;/p>
&lt;p>因为这个哈希表保存了所有的键值对，所以，可以将之称为&lt;strong>全局哈希表&lt;/strong>。我们知道哈希表最大的优点是可以在 O(1) 的时间复杂度来快速查找对应的键值对。实现过程就是计算键的哈希值，从而得到对应的哈希桶的位置，然后就可以访问相应的 entry 元素。&lt;/p>
&lt;p>在这个查找过程主要依赖于哈希计算，和数据量的多少并没有直接关系。也就是说在理想状况下，不管哈希表里面有 10 万个键还是 100 万个键，我们只需要一次计算就能找到相应的键。&lt;/p>
&lt;p>但事实却并不是这样，当向 Redis 中写入大量数据后，就可能会发现有时候操作会突然变慢了。这是因为忽略了一个潜在的风险：服务器的资源并不是无限的，所以在存储过程中必定会有取舍，对于哈希表也是同理，也就有了&lt;strong>哈希表的冲突问题和 rehash 可能带来的操作阻塞&lt;/strong>。&lt;/p>
&lt;h2 id="哈希冲突">哈希冲突&lt;/h2>
&lt;p>经过上面的分析，我们可以知道在有限的空间内，向哈希表中写入大量的元素时，哈希冲突是不可避免的问题。说简单点就是：当两个或两个以上的 Key 哈希值和哈希桶计算对应关系时，刚好落在了同一个哈希桶中。因为哈希桶的个数通常要少于 Key 的数据，这就导致了有一些 Key 的哈希值同时对应到了同一个哈希桶中。&lt;/p>
&lt;p>在 Redis 中解决哈希冲突的方式时&lt;strong>链式哈希&lt;/strong>，是指&lt;strong>同一个哈希桶中的多个元素用一个链表来保存，它们之间依次使用指针连接&lt;/strong>。&lt;/p>
&lt;p>如下图所示：entry1、entry2 和 entry3 都需要保存在哈希桶 3 中，导致了哈希冲突。此时，entry1 元素会通过一个&lt;em>next 指针指向 entry2，同样，entry2 也会通过&lt;/em>next 指针指向 entry3。这样一来，即使哈希桶 3 中的元素有 100 个，我们也可以通过 entry 元素中的指针，把它们连起来。这就形成了一个链表，也叫作哈希冲突链。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/da5705cd5fa4db4c6207e5fabcbc756f98bd2b1c/content/imag/tech/redis/01_Hash-Conflict.svg" alt="Hash_Conflict">&lt;/p>
&lt;p>但这么做也依然存在一个问题，哈希冲突链上的元素只能通过指针逐一查找再操作。如果哈希表里写入的数据越来越多，哈希冲突可能也会越来越多，这就回导致某些哈希冲突链过长，也就会进一步导致这个链上的元素查找耗时较长，效率也会降低。这也越 Redis 的“快”相违背。&lt;/p>
&lt;p>所以，Redis 会对哈希表做 rehash 操作，也就是通过 rehash 增加现有的哈希桶的数量，让逐渐增多的 entry 元素能在更多的桶之间分散保存，减少单个桶中的元素数量，从而减少单个桶中的冲突。具体做法如下：&lt;/p>
&lt;p>Redis 为了使 rehash 操作更高效，默认使用了两个全局哈希表：哈希表 1 和 哈希表 2.一开始，当插入数据的时候，会使用哈希表 1，此时的哈希表 2 并没有被分配空间。随着数据逐渐增多，Redis 开始执行 rehash，这个过程分为三步：&lt;/p>
&lt;ol>
&lt;li>给哈希表 2 分配更大的空间，例如是当前哈希表 1 的两倍；&lt;/li>
&lt;li>把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中；&lt;/li>
&lt;li>为了节省空间，释放哈希表 1 的空间。&lt;/li>
&lt;/ol>
&lt;p>当完成上述操作之后，就可以从哈希表 1 切换到哈希表 2，用增大的哈希表 2 保存更多的数据，而原来的哈希表 1 释放空间之后，留作下一次 rehash 扩容备用。&lt;/p>
&lt;p>看到这里，有些读者可能会说，原来这么简单的呐。其实并不是，仔细思考一下，在第二步的时候会涉及大量的数据拷贝，如果一次性把哈希表 1 中的数据都迁移到过去，可能会造成 Redis 线程阻塞，无法服务于其他请求。此时，Redis 就无法快速访问数据叻。这就好比，既然一口吃不下，那就多分几口吃呗。是的，Redis 采用的是&lt;strong>渐进式 Hash&lt;/strong>。&lt;/p>
&lt;p>说通俗点就是，在第二步拷贝数据时，Redis 仍然正常处理客户端请求，每处理一个请求时，从哈希表 1 中的第一个索引位置开始，顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中；等处理下一个请求时，再顺带拷贝哈希表 1 中的下一个索引位置的 entries。可以参考下图的内容来理解：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/da5705cd5fa4db4c6207e5fabcbc756f98bd2b1c/content/imag/tech/redis/01_ReHash.svg" alt="ReHash">&lt;/p>
&lt;p>这么做的好处是巧妙地把一次性大量拷贝的开销，分摊到了多次处理请求的过程中，避免了耗时操作，保证了数据的快速访问。&lt;/p>
&lt;p>看到这里想必有可以理解 Redis 的键和值是怎么通过哈希表组织的了吧。对于 String 类型来说，找到哈希桶就能直接进行增删改查了，所以，哈希表的 O(1) 操作复杂度也就是它的复杂度了。但是，对于集合类型来说，即使找到哈希桶了，还要在集合中再进行一步操作。下面就一起来看看，在 Redis 中集合类型是怎么玩的。&lt;/p>
&lt;h2 id="集合数据结构">集合数据结构&lt;/h2>
&lt;p>和 String 类型不同的是，集合类型处理起来要复杂一点。对于集合类型的值来说，第一步是通过全局哈希表找到对应的哈希桶位置，第二步是在集合中再进行增删改查。下来我们就来分析一下集合的操作效率和哪些因素有关。&lt;/p>
&lt;p>首先可以想到的是，于集合的&lt;strong>底层数据结构&lt;/strong>有关。例如，使用哈希表实现的集合，要比使用链表实现的集合访问效率更高。其次，操作效率和这些操作本身的执行特点也有关，比如读写一个元素的效率要比读写所有元素的效率高。&lt;/p>
&lt;p>接下来，我们就分别看一下集合类型的底层数据结构和操作复杂度。&lt;/p>
&lt;h2 id="底层数据结构">底层数据结构&lt;/h2>
&lt;p>在开篇我们提到了，集合类型的底层数据结构主要有 5 种：整数数组、双向链表、哈希表、压缩列表和跳表。&lt;/p>
&lt;p>其中，哈希表的特点刚才已经讲过；至于整数数组和双向链表也是比较常见，它们的操作都是顺序读写。也就是通过数组下标或者链表的指针逐个元素进行访问，时间复杂度基本是 O(N)，操作效率比较低；至于压缩列表和跳表我们平时接触的可能不是很多，但它们是 Redis 重要的数据结构，下面来重点解释一下。&lt;/p>
&lt;p>压缩列表比较类似于一个数组，数组中的每个元素都对应保存一个数据。和数组不同的是，压缩列表在表头有三个字段 zlbytes、zlail 和 zllen，分别表示列表长度、列表尾的偏移量和列表中的 entry 个数，在压缩列表的表尾还有一个 zlend，表示列表结束，如下图所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/da5705cd5fa4db4c6207e5fabcbc756f98bd2b1c/content/imag/tech/redis/01_Compression-List.svg" alt="Compressed-List">&lt;/p>
&lt;p>在压缩列表中，如果我们要查找定位第一个元素和最后一个元素，可以通过表头三个字段的长度直接定位，复杂度是 O(1)。而查找其他元素时，效率就没那么高了，只能逐个查找，此时的复杂度就是 O(N) 了。&lt;/p>
&lt;p>那么跳表呢？效率会不会高一点？&lt;/p>
&lt;p>有序链表只能逐一查找元素，导致操作起来非常缓慢，于是就出现了跳表。具体来说，跳表在链表的基础上，&lt;strong>增加了多级索引，通过索引位置的几个跳转，实现了数据的快速定位&lt;/strong>，如下图所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/da5705cd5fa4db4c6207e5fabcbc756f98bd2b1c/content/imag/tech/redis/01_Skip-List.svg" alt="Skip-List">&lt;/p>
&lt;p>如果我们要在链表中查找 26 这个元素，只能从头开始遍历链表，查找 6 次，直到找到该元素为止。此时，时间复杂度为 O(N)，查找效率很低。&lt;/p>
&lt;p>为了提高查找速度，我们来增加一级索引：从第一个元素开始，每两个元素选一个出来作为索引。这些索引再通过指针指向原始的链表。例如，从前两个元素中抽取元素 1 作为一级索引，从第三、四个元素中抽取 8 作为一级索引，依此类推。此时，我们只需要 4 次查找就能定位到元素 26 了。&lt;/p>
&lt;p>看到这里，聪明的你可能会想到，是不是能够再增加一个二级索引呢？从一级索引中再抽取部分元素作为二级索引。例如，从一级索引中抽取 1、19 和 45 三个元素作为二级索引，二级索引指向一级索引。这样，我们只需要 3 次查找，就能定位到元素 26 了。&lt;/p>
&lt;p>可以看到，这个查找过程就是在多级索引上跳来跳去，最后定位到元素。这也比较符合“跳表”这个名称，哈哈。当数据量很大时，跳表的查找复杂度就是好 O(logN)。&lt;/p>
&lt;p>到此为止，我们讲完了 Redis 中的底层数据结构类型，下面按照查找的时间复杂度总结如下：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: center">名称&lt;/th>
&lt;th style="text-align: center">时间复杂度&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: center">哈希表&lt;/td>
&lt;td style="text-align: center">O(1)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">跳表&lt;/td>
&lt;td style="text-align: center">O(logN)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">双向链表&lt;/td>
&lt;td style="text-align: center">O(N)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">压缩列表&lt;/td>
&lt;td style="text-align: center">O(N)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">整数数组&lt;/td>
&lt;td style="text-align: center">O(N)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="不同操作的复杂度">不同操作的复杂度&lt;/h2>
&lt;p>在 Redis 中集合类型的操作有很多，有读写单个集合元素的，比如 HGET、HSET，也有操作多个元素的，例如 SADD，还有对整个集合进行遍历操作的，例如 SMEMBERS。这么多操作，它们的复杂度也各不相同。而复杂度的高低又是我们选择集合类型的重要依据。主要可以归纳为以下四类：&lt;/p>
&lt;p>第一，&lt;strong>单元素操作&lt;/strong>，是指&lt;strong>每一种集合类型对单个元素实现的增删改查操作&lt;/strong>。例如，Hash 类型的 HGET、HSET 和 HDEL，Set 类型的 SADD、SREM、SRANDMEMBER 等。这些操作的复杂度由集合采用的数据结构决定，例如 HGET、HSET 和 HDEL 是对哈希表做操作，所以他们的复杂度都是 O(1)；Set 类型用哈希表作为底层数据结构时，它的 SADD、SREM 和 SRANDMEMBER 的复杂度也是 O(1)。&lt;/p>
&lt;p>不过，这里有个地方需要注意一下，经过刚才介绍我们知道集合类型支持同时对多个元素进行增删改查，例如 Hash 类型的 HMGET 和 HMSET，Set 类型的 SADD 也支持同时增加多个元素。此时，这些操作的复杂度，就是由单个元素操作复杂度和元素个数决定的。例如，HMSET 增加 M 个元素时，复杂度就从 O(1) 变成 O(M) 了。&lt;/p>
&lt;p>第二，&lt;strong>范围操作&lt;/strong>，是指&lt;strong>集合类型中的遍历操作&lt;/strong>，可以返回集合中的所有数据，比如 Hash 类型的 HGETALL 和 Set 类型的 SMEMBERS，或者返回一个范围内的部分数据，比如 List 类型的 LRANGE 和 ZSet 类型的 ZRANGE。这类操作的复杂度一般是 O(N)，比较耗时，我们应该尽量避免。&lt;/p>
&lt;p>上述问题作为 Redis 的维护者当然也知道这一点，所以 Redis 从 2.8 版本开始提供了 SCAN 系列操作（包括 HSCAN，SSCAN 和 ZSCAN），这类操作实现了渐进式遍历，每次只返回有限数量的数据。这样一来，相比于 HGETALL、SMEMBERS 这类操作来说，就避免了一次性返回所有元素而导致的 Redis 阻塞。&lt;/p>
&lt;p>第三，&lt;strong>统计操作&lt;/strong>，是指&lt;strong>集合类型对集合中所有元素个数的记录&lt;/strong>，例如 LLEN 和 SCARD。这类操作复杂度只有 O(1)，这是因为当集合类型采用压缩列表、双向链表、整数数组这些数据结构时，这些结构中专门记录了元素的个数统计，因此可以高效地完成相关操作。&lt;/p>
&lt;p>第四，&lt;strong>例外情况&lt;/strong>，是指&lt;strong>某些数据结构的特殊记录&lt;/strong>，例如压缩列表和双向链表都会记录表头和表尾的偏移量。这样一来，对于 List 类型的 LPOP、RPOP、LPUSH、RPUSH 这四个操作来说，它们是在列表的头尾增删元素，这就可以通过偏移量直接定位，所以它们的复杂度也只有 O(1)，可以实现快速操作。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>在这篇博客，主要介绍了 Redis 的底层数据结构，这既包括了 Redis 中用来保存每个键和值的全局哈希表结构，也包括了支持集合类型实现的双向链表、压缩列表、整数数组、哈希表和跳表这五大底层结构。&lt;/p>
&lt;p>Redis 之所以能快速操作键值对，一方面是因为 O(1) 复杂度的哈希表被广泛使用，包括 String、Hash 和 Set，它们的操作复杂度基本由哈希表决定，另一方面，Sorted Set 也采用了时间复杂度为 O(logN) 的跳表。不过，集合类型的范围操作，因为要遍历底层数据结构，复杂度通常是 O(N)。所以我们可以使用其他命令来替代，例如可以使用 SCAN 来代替，避免 Redis 内部产生费时的全集合遍历操作。&lt;/p>
&lt;p>当然，我们也不能忘了复杂度较高的 List 类型，它的两种底层实现结构：双向链表和压缩列表的复杂度都是 O(N)。因此，我们可以根据实际情况灵活地使用 List 类型。例如，既然它的 POP/PUSH 效率很高，那么就将它主要用于 FIFO 队列场景，而不是作为一个可以随机读写的集合。&lt;/p>
&lt;p>至于整数数组和压缩列表虽然在查找时间复杂度方面并没有很大的优势，但这二者在底层都是非常紧凑的数据结构，要比链表占用的内存要更少，Redis 本身定位又是内存数据库，大量数据存到内存中，所以需要尽可能地优化，提高内存的利用率。&lt;/p>
&lt;p>Redis 数据类型丰富，每个类型的操作繁多，如果第一次接触是很难记住所有操作的复杂度。单正所谓&lt;strong>万变不离其宗&lt;/strong>，只要我们掌握其原理，就可以做到以不变应万变叻。&lt;/p></content></entry><entry><title type="text">分布式 006——事务</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/distributedsystem/ds006_transaction/"/><id>https://quakewang.github.io/tech/distributedsystem/ds006_transaction/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2024-01-18T10:56:37+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="关于分布式事务你知道哪些">关于分布式事务你知道哪些？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>我们还是从电商系统开始谈起，确实有点俗套，但从业务场景的角度考虑可以更好地解释一些概念。在我们平时网购的过程中，创建的每一笔订单，对于电商平台来说都会涉及到两个核心步骤：一是订单业务采取下订单操作，二是库存业务采取减库存操作。&lt;/p>……</summary><content type="html">&lt;h1 id="关于分布式事务你知道哪些">关于分布式事务你知道哪些？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>我们还是从电商系统开始谈起，确实有点俗套，但从业务场景的角度考虑可以更好地解释一些概念。在我们平时网购的过程中，创建的每一笔订单，对于电商平台来说都会涉及到两个核心步骤：一是订单业务采取下订单操作，二是库存业务采取减库存操作。&lt;/p>
&lt;p>在实际的后台中，这两个业务通常会运行在不同的服务器上，甚至是运行在不同区域的服务器上，在 2024 年听见“异地多活”已经不是什么新鲜的概念了。但是对于同一笔订单，当且仅当订单操作和减库存操作一致时，才能保证交易的正确性。也就是说一笔订单，只有当上述两个操作都完成，才能算处理成功，否则就是处理失败。&lt;/p>
&lt;p>这个问题放在分布式领域，对应的就是&lt;strong>分布式事务&lt;/strong>，下面就一起来详细了解一些关于事务的基础知识。&lt;/p>
&lt;h2 id="从事务开始谈起">从事务开始谈起&lt;/h2>
&lt;p>在深入分布式事务之前，我们先来重新回顾一下什么是事务。&lt;/p>
&lt;p>事务（Transaction）提供一种机制，&lt;strong>将包含一系列操作的工作序列纳入到一个不可分割的执行单元&lt;/strong>。只有所有操作均被正确执行才能提交事务；任意一个操作失败都会导致整个事务回滚（Rollback）到之前状态，即所有操作均被取消。简单来说，事务提供了一种机制，使得工作要么全部都不做，要么完全被执行，简单概括就是 all or nothing。&lt;/p>
&lt;p>通常情况下，我们所指的事务都是本地事务，也就是运行在单机上的事务。这类事务也就具备 ACID 四大特性，其具体含义如下：&lt;/p>
&lt;ul>
&lt;li>A：原子性（Atomicity），事务被视为一个不可分割的原子操作，即事务最终的状态只有两种，全部执行成功和全部不执行，不会停留在中间某个环节。若处理事务的任何一项操作不成功，就会导致整个事务失败。一旦操作失败，所有操作都会被取消（即回滚），使得事务仿佛没有被执行过一样。就好比买一件商品，购买成功时，则给商家付了钱，商品到手；购买失败时，则商品在商家手中，消费者的钱也没花出去。&lt;/li>
&lt;li>C：一致性（Consistency），是指事务操作前和操作后，数据满足完整性约束，数据库保持一致性状态。比如，用户 A 和用户 B 在银行分别有 200 元和 100 元，总共 300 元。用户 A 给用户 B 转账 100 元，分为两个步骤，从 A 的账户扣除 100 元和对 B 的账户增加 100 元。一致性就是要求上述步骤操作后，最后的结果是用户 A 还有 100 元，用户 B 有 200 元，总共 300 元，而不会出现用户 A 扣除了 100 元，但用户 B 未增加的情况（该情况，用户 A 和 B 均为 100 元，总共 200 元）。&lt;/li>
&lt;li>I：隔离性（Isolation），事务之间的操作应该相互隔离，一个事务的执行不应该受到其他事务的干扰。并发执行的多个事务应该产生与串行执行相同的结果，以避免数据不一致性和冲突。比如说，消费者购买商品这个事务，是不影响其他消费者购买的。&lt;/li>
&lt;li>D：持久性（Durability），也被称为永久性，是指一个事务被执行后，那么它对数据库所做的更新就永久地保存下来了。即使发生系统崩溃或宕机等故障，重新启动数据库系统后，只要数据库能够重新被访问，那么一定能够将其恢复到事务完成时的状态。就像消费者在网站上的购买记录，即使换了一台设备，也依然可以查到。&lt;/li>
&lt;/ul>
&lt;p>只有在数据操作请求满足上述四个特性的条件下，存储系统才能保证处于正确的工作状态。因此，无论是在传统的集中式存储系统还是在分布式存储系统中，任何数据操作请求都必须满足 ACID 特性。&lt;/p>
&lt;p>那么分布式事务是什么呢？顾名思义，就是在分布式系统中运行的事务，由多个本地事务所组成。在分布式系统中，对于事务的处理要复杂的多，这些事务可能来自于不同的机器，不同的地区，或者不同的操作系统。开头所提到的电商处理订单问题，就是一个比较典型的分布式事务。&lt;/p>
&lt;p>分布式事务由多个事务组成，因此基本满足 ACID，其中的 C 是&lt;strong>强一致性&lt;/strong>，也就是所有操作均执行成功，才提交最终结果，以保证数据一致性或完整性。但随着分布式系统规模不断扩大，复杂度急剧上升，达成强一致性所需时间周期较长，限定了复杂业务的处理。为了适应复杂业务，出现了 BASE 理论，该理论的一个关键点就是采用&lt;strong>最终一致性代替强一致性&lt;/strong>。&lt;/p>
&lt;p>梳理清楚上述的基本概念之后，接下来就一起看看如何实现一个简单的分布式事务。&lt;/p>
&lt;h2 id="实现分布式事务">实现分布式事务&lt;/h2>
&lt;p>在探讨如何实现分布式事务之前，我们要了解为什么要有分布式事务？实际上，分布式事务主要是解决在分布式环境下，组合事务的一致性问题。实现分布式事务有以下 3 种基本方法：&lt;/p>
&lt;ul>
&lt;li>基于 XA 协议的二阶段提交协议方法；&lt;/li>
&lt;li>三阶段提交协议方法；&lt;/li>
&lt;li>基于消息的最终一致性方法。&lt;/li>
&lt;/ul>
&lt;p>其中，基于 XA 协议的二阶段提交协议方法和三阶段提交协议方法，采用了强一致性，遵从 ACID。基于消息的最终一致性方法，采用了最终一致性，遵从 BASE 理论。&lt;/p>
&lt;h3 id="基于-xa-协议的二阶段提交方法">基于 XA 协议的二阶段提交方法&lt;/h3>
&lt;p>XA 是一个分布式事务协议，规定了事务管理器和资源管理器接口，使得应用程序能够通过事务管理器来管理分布式环境下的多个资源，通常是数据库。由此可以看出，XA 协议主要包括事务管理器和本地资源管理器两个部分。&lt;/p>
&lt;p>XA 实现分布式事务的原理，比较类似于&lt;a href="https://quakewang.github.io/tech/distributedsystem/ds003_mutex/">分布式互斥&lt;/a>博客中所介绍的集中式算法：事务管理器相当于协调者，负责各个本地资源的提交和回滚；而资源管理器就是分布式事务的参与者，通常由数据库实现，比如 Oracle、DB2 等商业数据库都实现了 XA 接口。&lt;/p>
&lt;p>基于 XA 协议的二阶段提交方法中，二阶段提交协议（Two-phase Commit Protocol，2PC），用于保证分布式系统中事务提交时的数据一致性，是 XA 在全局事务中用于协调多个资源的机制。&lt;/p>
&lt;p>那么 2PC 是如何保证分布在不同节点上的分布式事务的一致性呢？为了做到这一点，会需要引入一个协调者来管理所有的节点，并确保这些节点能够正确提交操作结果，若提交失败则放弃事务。接下来，我们看看两阶段提交协议的具体过程。&lt;/p>
&lt;p>两阶段提交协议的执行过程，分为投票（Voting）和提交（Commit）两个阶段。&lt;/p>
&lt;p>首先，我们看一下第一阶段&lt;strong>投票&lt;/strong>：在这一阶段，协调者（Coordinator，即事务管理器）会向事务的参与者，也就是本地资源管理器，发起执行操作的 CanCommit 请求，并等待参与者的响应。参与者接收到请求后，会执行请求中的事务操作，将操作信息记录到事务日志中但不提交，也就是暂时不会修改数据库中的数据，待参与者执行成功，则向协调者发送“Yes”消息，表示同意操作；若不成功，则发送“No”消息，表示终止操作。&lt;/p>
&lt;p>当所有的参与者都返回了操作结果（Yes 或 No 消息）后，系统进入了第二阶段&lt;strong>提交&lt;/strong>阶段（也可以称为，执行阶段）。在提交阶段，协调者会根据所有参与者返回的信息向参与者发送 DoCommit（提交）或 DoAbort（取消）指令。具体规则如下：&lt;/p>
&lt;ul>
&lt;li>若协调者从参与者那里收到的都是“Yes”消息，则向参与者发送“DoCommit”消息。参与者收到“DoCommit”消息后，完成剩余的操作（比如修改数据库中的数据）并释放资源（整个事务过程中占用的资源），然后向协调者返回“HaveCommitted”消息；&lt;/li>
&lt;li>若协调者从参与者收到的消息中包含“No”消息，则向所有参与者发送“DoAbort”消息。此时投票阶段发送“Yes”消息的参与者，则会根据之前执行操作时的事务日志对操作进行回滚，就好像没有执行过请求操作一样，然后所有参与者会向协调者发送“HaveCommitted”消息；&lt;/li>
&lt;li>协调者接收到来自所有参与者的“HaveCommitted”消息后，就意味着整个事务结束了。&lt;/li>
&lt;/ul>
&lt;p>这么说可能比较抽象，接下来我们以一个具体的案例来进行讲解。假设用户 A 想要在某宝上购买 100 件 T 恤，这中间可能会涉及到很多的业务流程，但重点聚焦于&lt;strong>下单&lt;/strong>和&lt;strong>减少库存&lt;/strong>这两个操作。&lt;/p>
&lt;p>第一阶段：订单系统接收到来自于用户 A 的下单操作，会将与用户 A 有关的订单数据库锁住，准备好增加一条关于用户 A 购买 100 件 T 恤的信息，并将同意消息“Yes”回复给协调者。但库存系统由于 T 恤库存不足，出货失败，因此向协调者回复了一个终止消息“No”。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/f2c89d7278812380b1b22488c9ea2d5e927a1da7/content/imag/tech/distributed/06_xa01.svg" alt="2pc-01">&lt;/p>
&lt;p>第二阶段：由于库存系统操作不成功，因此，协调者就会向订单系统和库存系统发送“DoAbort”消息。订单系统接收到“DoAbort”消息后，将系统内的数据退回到没有用户 A 购买 100 件 T 恤的版本，并释放锁住的数据库资源。订单系统和库存系统完成操作后，向协调者发送“HaveCommitted”消息，表示完成了事务的撤销操作。&lt;/p>
&lt;p>至此，用户 A 购买 100 件 T 恤这一事务已经结束，用户 A 购买失败。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/f2c89d7278812380b1b22488c9ea2d5e927a1da7/content/imag/tech/distributed/06_xa02.svg" alt="2pc-02">&lt;/p>
&lt;p>由上述流程可以看出，二阶段提交的&lt;strong>算法思路&lt;/strong>可以概括为：协调者向参与者下发请求事务操作，参与者接收到请求后，进行相关操作并将操作结果通知协调者，协调者根据所有参与者的反馈结果决定各参与者是要提交操作还是撤销操作。&lt;/p>
&lt;p>虽然基于 XA 的二阶段提交算法尽量保证了数据的强一致性，而且实现成本低，但依然有些不足。主要有以下三个问题：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>同步阻塞问题&lt;/strong>：二阶段提交算法在执行过程中，所有参与节点都是事务阻塞型的。也就是说，当本地资源管理器占有临界资源时，其他资源管理器如果要访问同一临界资源，会处于阻塞状态。因此，基于 XA 的二阶段提交协议不支持高并发场景。&lt;/li>
&lt;li>&lt;strong>单点故障问题&lt;/strong>：该算法类似于集中式算法，一旦事务管理器发生故障，整个系统都处于停滞状态。尤其是在提交阶段，一旦事务管理器发生故障，资源管理器会由于等待管理器的消息，而一直锁定事务资源，导致整个系统被阻塞。&lt;/li>
&lt;li>&lt;strong>数据不一致问题&lt;/strong>：在提交阶段，当协调者向所有参与者发送“DoCommit”请求时，如果发生了局部网络异常，或者在发送提交请求的过程中协调者发生了故障，就会导致只有一部分参与者接收到了提交请求并执行提交操作，但其他未接到提交请求的那部分参与者则无法执行事务提交。于是整个分布式系统便出现了数据不一致的问题。&lt;/li>
&lt;/ul>
&lt;h3 id="三阶段提交方法">三阶段提交方法&lt;/h3>
&lt;p>三阶段提交协议（Three-phase Commit Protocol，3PC），是对二阶段提交（2PC）的改进。为了更好地处理两阶段提交的同步阻塞和数据不一致问题，三阶段提交引入了&lt;strong>超时机制&lt;/strong>和&lt;strong>准备阶段&lt;/strong>。&lt;/p>
&lt;ul>
&lt;li>与 2PC 只是在协调者引入超时机制不同，3PC 同时在协调者和参与者中引入了超时机制。如果协调者或参与者在规定的时间内没有接收到来自其他节点的响应，就会根据当前的状态选择提交或者终止整个事务，从而减少了整个集群的阻塞时间，在一定程度上减少或减弱了 2PC 中出现的同步阻塞问题。&lt;/li>
&lt;li>在第一阶段和第二阶段中间引入了一个准备阶段，或者说把 2PC 的投票阶段一分为二，也就是在提交阶段之前，加入了一个预提交阶段。在预提交阶段尽可能排除一些不一致的情况，保证在最后提交之前各参与节点的状态是一致的。&lt;/li>
&lt;/ul>
&lt;p>于是，三阶段提交协议就有 CanCommit、PreCommit、DoCommit 三个阶段，下面我们来看一下这个三个阶段。&lt;/p>
&lt;h4 id="第一cancommit-阶段">第一，CanCommit 阶段&lt;/h4>
&lt;p>协调者向参与者发送请求操作，也就是 CanCommit 请求，询问参与者是否可以执行事务提交操作，然后等待参与者的响应；参与者收到 CanCommit 请求之后，回复 Yes，表示可以顺利执行事务；否则回复 No。&lt;/p>
&lt;p>3PC 的 CanCommit 阶段与 2PC 的 Voting 阶段相比：&lt;/p>
&lt;ul>
&lt;li>比较类似的点在于：协调者均需要向参与者发送请求操作（CanCommit 请求），询问参与者是否可以执行事务提交操作，然后等待参与者的响应。参与者收到 CanCommit 请求之后，回复 Yes，表示可以顺利执行事务；否则回复 No；&lt;/li>
&lt;li>不同之处在于：2PC 中，在投票阶段，若参与者可以执行事务，会将操作信息记录到事务日志中但不提交，并返回结果给协调者。但在 3PC 中，在 CanCommit 阶段，参与者仅会判断是否可以顺利执行事务，并返回结果。而操作信息记录到事务日志，但不提交的操作由第二阶段预提交阶段执行&lt;/li>
&lt;/ul>
&lt;p>CanCommit 阶段不同节点之间的事务请求成功和失败的流程，如下所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/ca132fb13fb354ed04d0bf9af7ea4e5bcef06436/content/imag/tech/distributed/06_3pc01.svg" alt="3pc-01">&lt;/p>
&lt;p>当协调者接收到所有参与者回复的消息后，进入预提交阶段，也就是 PreCommit 阶段。&lt;/p>
&lt;h4 id="第二precommit-阶段">第二，PreCommit 阶段&lt;/h4>
&lt;p>协调者根据参与者的回复情况，来决定是否可以进行 PreCommit 操作。&lt;/p>
&lt;ul>
&lt;li>如果所有参与者回复的都是“Yes”，那么协调者就会执行事务的预执行；&lt;/li>
&lt;li>协调者向参与者发送 PreCommit 请求，进入预提交阶段；&lt;/li>
&lt;li>参与者接收到 PreCommit 请求后执行事务操作，并将 Undo 和 Redo 信息记录到事务日志中；&lt;/li>
&lt;li>如果参与者成功执行了事务操作，则返回 ACK 响应，同时开始等待最终指令。&lt;/li>
&lt;li>假如任何一个参与者向协调者发送了“No”消息，或者等待超时之后，协调者都没有收到参与者的响应，就执行中断事务的操作；&lt;/li>
&lt;li>协调者会向所有参与者发送“Abort”消息；&lt;/li>
&lt;li>参与者收到“Abort”消息之后，或超时后仍未收到协调者的消息，则会执行事务的中断操作。&lt;/li>
&lt;/ul>
&lt;p>预提交阶段，不同节点上事务执行成功和失败的流程，如下所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/ca132fb13fb354ed04d0bf9af7ea4e5bcef06436/content/imag/tech/distributed/06_3pc02.svg" alt="3pc-02">&lt;/p>
&lt;p>预提交阶段保证了在最后提交阶段（DoCommit 阶段）之前所有参与者的状态是一致的。&lt;/p>
&lt;h4 id="第三docommit-阶段">第三，DoCommit 阶段&lt;/h4>
&lt;p>DoCommit 阶段进行真正的事务提交，根据 PreCommit 阶段协调者发送的消息，进入执行提交阶段或事务中断阶段。&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>执行提交阶段&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>若协调者接收到所有参与者发送的 Ack 响应，则向所有参与者发送 DoCommit 消息，开始执行阶段；&lt;/li>
&lt;li>参与者接收到 DoCommit 消息之后，正式提交事务。完成事务提交之后，释放所有锁住的资源，并向协调者发送 Ack 响应；&lt;/li>
&lt;li>协调者接收到所有参与者的 Ack 响应之后，完成事务。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>事务中断阶段&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>协调者向所有参与者发送 Abort 请求；&lt;/li>
&lt;li>参与者接收到 Abort 消息之后，利用其在 PreCommit 阶段记录的 Undo 信息执行事务的回滚操作，释放所有锁住的资源，并向协调者发送 Ack 消息；&lt;/li>
&lt;li>协调者接接收到参与者反馈的 Ack 消息之后，执行事务的中断，并结束事务。&lt;/li>
&lt;/ul>
&lt;p>执行阶段不同节点上事务执行成功和失败、事务中断的流程，如下图所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/ca132fb13fb354ed04d0bf9af7ea4e5bcef06436/content/imag/tech/distributed/06_3pc03.svg" alt="3pc-03">&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>3PC 协议在协调者和参与者均引入了超时机制。即当参与者在预提交阶段向协调者发送 Ack 消息后，如果长时间没有得到协调者的响应，在默认情况下，参与者会自动将超时的事务进行提交，从而减少整个集群的阻塞时间，在一定程度上减少或减弱了 2PC 中出现的同步阻塞问题。&lt;/p>
&lt;p>但三阶段提交仍然存在数据不一致的情况，比如在 PreCommit 阶段，部分参与者已经接受到 ACK 消息进入执行阶段，但部分参与者与协调者网络不通，导致接收不到 ACK 消息，此时接收到 ACK 消息的参与者会执行任务，未接收到 ACK 消息且网络不通的参与者无法执行任务，最终导致数据不一致。&lt;/p>
&lt;h3 id="基于消息的最终一致性方法">基于消息的最终一致性方法&lt;/h3>
&lt;p>2PC 和 3PC 核心思想均是以集中式的方式实现分布式事务，这两种方法都存在两个共同的缺点，一是，同步执行，性能差；二是，数据不一致问题。为了解决这两种问题，通过分布式消息来确保事务最终一致性的方案便出现咯。&lt;/p>
&lt;p>最终一致性的思想就是：将需要分布式处理的事务通过消息或者日志的方式异步执行，消息或日志可以存到本地文件、数据库或消息队列中，再通过业务规则进行失败重试。这个案例，就是使用基于分布式消息的最终一致性方案解决了分布式事务的问题。&lt;/p>
&lt;p>基于分布式消息的最终一致性方案的事务处理，引入了一个消息中间件，用于在多个应用之间进行消息传递。比如可以采用 RocketMQ 机制来支持消息事务。&lt;/p>
&lt;p>基于消息中间件协调多个节点分布式事务执行操作的示意图，如下所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/ca132fb13fb354ed04d0bf9af7ea4e5bcef06436/content/imag/tech/distributed/06_final01.svg" alt="final-01">&lt;/p>
&lt;p>仍然以网上购物为例。假设用户 A 在某电商平台下了一个订单，需要支付 50 元，发现自己的账户余额共 150 元，就使用余额支付，支付成功之后，订单状态修改为支付成功，然后通知仓库发货。&lt;/p>
&lt;p>在该事件中，涉及到了订单系统、支付系统、仓库系统，这三个系统是相互独立的应用，通过远程服务进行调用。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/ca132fb13fb354ed04d0bf9af7ea4e5bcef06436/content/imag/tech/distributed/06_final02.svg" alt="final-02">&lt;/p>
&lt;p>根据基于分布式消息的最终一致性方案，用户 A 通过终端手机首先在订单系统上操作，通过消息队列完成整个购物流程。然后整个购物的流程如下所示。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/ca132fb13fb354ed04d0bf9af7ea4e5bcef06436/content/imag/tech/distributed/06_final03.svg" alt="final-03">&lt;/p>
&lt;ol>
&lt;li>订单系统把订单消息发给消息中间件，消息状态标记为“待确认”；&lt;/li>
&lt;li>消息中间件收到消息后，进行消息持久化操作，即在消息存储系统中新增一条状态为“待发送”的消息；&lt;/li>
&lt;li>消息中间件返回消息持久化结果（成功 / 失败），订单系统根据返回结果判断如何进行业务操作。失败，放弃订单，结束（必要时向上层返回失败结果）；成功，则创建订单；&lt;/li>
&lt;li>订单操作完成后，把操作结果（成功 / 失败）发送给消息中间件；&lt;/li>
&lt;li>消息中间件收到业务操作结果后，根据结果进行处理：失败，删除消息存储中的消息，结束；成功，则更新消息存储中的消息状态为“待发送（可发送）”，并执行消息投递；&lt;/li>
&lt;li>如果消息状态为“可发送”，则 MQ 会将消息发送给支付系统，表示已经创建好订单，需要对订单进行支付。支付系统也按照上述方式进行订单支付操作；&lt;/li>
&lt;li>订单系统支付完成后，会将支付消息返回给消息中间件，中间件将消息传送给订单系统。若支付失败，则订单操作失败，订单系统回滚到上一个状态，MQ 中相关消息将被删除；若支付成功，则订单系统再调用库存系统，进行出货操作，操作流程与支付系统类似；&lt;/li>
&lt;/ol>
&lt;p>在上述过程中，可能会产生如下异常情况，其对应的解决方案为：&lt;/p>
&lt;ol>
&lt;li>订单消息未成功存储到 MQ 中，则订单系统不执行任何操作，数据保持一致；&lt;/li>
&lt;li>MQ 成功将消息发送给支付系统（或仓库系统），但是支付系统（或仓库系统）操作成功的 ACK 消息回传失败（由于通信方面的原因），导致订单系统与支付系统（或仓库系统）数据不一致，此时 MQ 会确认各系统的操作结果，删除相关消息，支付系统（或仓库系统）操作回滚，使得各系统数据保持一致；&lt;/li>
&lt;li>MQ 成功将消息发送给支付系统（或仓库系统），但是支付系统（或仓库系统）操作成功的 ACK 消息回传成功，订单系统操作后的最终结果（成功或失败）未能成功发送给 MQ，此时各系统数据可能不一致，MQ 也需确认各系统的操作结果，若数据一致，则更新消息；若不一致，则回滚操作、删除消息。&lt;/li>
&lt;/ol>
&lt;p>基于分布式消息的最终一致性方案采用消息传递机制，并使用异步通信的方式，避免了通信阻塞，从而增加系统的吞吐量。同时，这种方案还可以屏蔽不同系统的协议规范，使其可以直接交互。&lt;/p>
&lt;p>在不需要请求立即返回结果的场景下，这些特性就带来了明显的通信优势，并且通过引入消息中间件，实现了消息生成方（如上述的订单系统）本地事务和消息发送的原子性，采用最终一致性的方式，只需保证数据最终一致即可，一定程度上解决了二阶段和三阶段方法要保证强一致性而在某些情况导致的数据不一致问题。&lt;/p>
&lt;p>可以看出，分布式事务中，当且仅当所有的事务均成功时整个流程才成功。所以，分布式事务的一致性是实现分布式事务的关键问题，目前来看还没有一种很简单、完美的方案可以应对所有场景。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>本节内容从事务的 ACID 特性出发，介绍了分布式事务的概念、特征，以及如何实现分布式事务。在关于如何实现分布式的部分，以网购为例，介绍了常见的三种实现方式，即基于 XA 协议的二阶段提交方法，三阶段方法以及基于分布式消息的最终一致性方法。&lt;/p>
&lt;p>二阶段和三阶段方法是维护强一致性的算法，它们针对刚性事务，实现的是事务的 ACID 特性。而基于分布式消息的最终一致性方案更适用于大规模分布式系统，它维护的是事务的最终一致性，遵循的是 BASE 理论，因此适用于柔性事务。&lt;/p>
&lt;p>在分布式系统的设计与实现中，分布式事务是不可或缺的一部分。可以说，没有实现分布式事务的分布式系统，不是一个完整的分布式系统。分布式事务的实现过程看似复杂，但将方法分解剖析后，就会发现分布式事务的实现是有章可循的。&lt;/p>
&lt;h2 id="扩展base-理论">扩展：BASE 理论&lt;/h2>
&lt;p>在最后咱扩展一点关于 BASE 理论的基础知识。BASE 理论包括基本可用（Basically Available）、柔性状态（Soft State）和最终一致性（Eventual Consistency）。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>基本可用&lt;/strong>：分布式系统出现故障的时候，允许损失一部分功能的可用性，保证核心功能可用。比如，某些电商 618 大促的时候，会对一些非核心链路的功能进行降级处理。&lt;/li>
&lt;li>&lt;strong>柔性状态&lt;/strong>：在柔性事务中，允许系统存在中间状态，且这个中间状态不会影响系统整体可用性。比如，数据库读写分离，写库同步到读库（主库同步到从库）会有一个延时，其实就是一种柔性状态。&lt;/li>
&lt;li>&lt;strong>最终一致性&lt;/strong>：事务在操作过程中可能会由于同步延迟等问题导致不一致，但最终状态下，所有数据都是一致的。&lt;/li>
&lt;/ul>
&lt;p>BASE 理论为了支持大型分布式系统，通过牺牲强一致性，保证最终一致性，来获得高可用性，是对 ACID 原则的弱化。ACID 与 BASE 是对一致性和可用性的权衡所产生的不同结果，但二者都保证了数据的持久性。ACID 选择了强一致性而放弃了系统的可用性。与 ACID 原则不同的是，BASE 理论保证了系统的可用性，允许数据在一段时间内可以不一致，最终达到一致状态即可，也即牺牲了部分的数据一致性，选择了最终一致性。&lt;/p>
&lt;p>具体到今天的三种分布式事务实现方式，二阶段提交、三阶段提交方法，遵循的是 ACID 原则，而消息最终一致性方案遵循的就是 BASE 理论。&lt;/p></content></entry><entry><title type="text">分布式 005——锁</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/distributedsystem/ds005_lock/"/><id>https://quakewang.github.io/tech/distributedsystem/ds005_lock/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-12-11T15:56:37+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="重新认识分布式锁">重新认识分布式锁&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>在分布式系列的&lt;a href="https://quakewang.github.io/tech/distributedsystem/ds003_mutex/">第三篇&lt;/a>博客中，简单探讨了有关“分布式互斥”的相关知识，也从中领悟了”有你没我，有我没你“的设计精髓。分布式互斥算法主要是为了解决同一临界资源同一时刻只能被一个程序访问的问题。&lt;/p>……</summary><content type="html">&lt;h1 id="重新认识分布式锁">重新认识分布式锁&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>在分布式系列的&lt;a href="https://quakewang.github.io/tech/distributedsystem/ds003_mutex/">第三篇&lt;/a>博客中，简单探讨了有关“分布式互斥”的相关知识，也从中领悟了”有你没我，有我没你“的设计精髓。分布式互斥算法主要是为了解决同一临界资源同一时刻只能被一个程序访问的问题。&lt;/p>
&lt;p>如果进一步思考可以发现，在之前介绍的算法中，主要讲了如何协调多个进程获取权限和根据权限有序访问&lt;strong>共享资源&lt;/strong>，“获得访问权限的进程可以访问共享资源，其他进程必须等待拥有该权限的进程释放权限”。但是，并没有介绍在访问共享资源时，这个权限是如何设置或产生的，以及设置或产生这个权限的工作原理是什么。&lt;/p>
&lt;p>那么，就带着好奇心，一起来看看分布式锁是如何解决这个问题的。&lt;/p>
&lt;h2 id="分布式锁的用途">分布式锁的用途&lt;/h2>
&lt;p>首先，我们需要重新认识一下什么是“锁”。&lt;/p>
&lt;p>在单机系统中，经常会有多个线程访问同一种资源的情况，按照习惯，我们把这样的资源叫作共享资源，或者临界资源。为了维护线程操作的有效性和正确性，自然而就需要某种机制来减少低效率的操作，避免同时对相同的数据进行不一样的操作，要维护数据的一致性，防止数据脏读和数据丢失等。也就是说，我们需要一种互斥机制，按照某种规则对多个线程进行排队，依次、互不干扰地访问共享资源。&lt;/p>
&lt;p>这个机制指的是，为了实现分布式互斥，在某个地方做&lt;strong>标记&lt;/strong>，这个标记每个线程都能看到，到标记不存在时可以设置该标记，当标记被设置后，其他线程只能等待拥有该标记的线程执行完成，并释放该标记后，才能去设置该标记和访问共享资源。这个标记也就是我们常常说的&lt;strong>锁&lt;/strong>。&lt;/p>
&lt;p>简单来说，锁是多线程同时访问同一资源的场景下，为了让线程互不干扰地访问共享资源，从而保证操作的有效性和正确性的一种标记。&lt;/p>
&lt;p>与普通锁不同的是，&lt;strong>分布式锁&lt;/strong>是指分布式环境下，系统部署在多个机器中，实现多进程分布式互斥的一种锁。为了保证多个进程能看到锁，锁被存在公共存储（比如 Redis、Memcached 等三方存储中），以实现多个进程并发访问同一共享资源，同一时刻只有一个进程可以访问共享资源，确保数据的一致性。&lt;/p>
&lt;p>哪些场景下需要使用分布式锁呢？&lt;/p>
&lt;p>比如，现在某电商网站正在售卖 NewBalacne 990V4（以下简称 ”990V4“），库存当前只有 2 双，但有 5 个来自不同地区的用户 {A, B, C, D, E} 几乎同时下单，那么这 2 双鞋子会花落谁家呢？&lt;/p>
&lt;p>这时，如果习惯于 CRUD 的开发者可能下意识会想到，这还不是小菜一碟，谁先提交订单请求，谁就购买成功呗。但实际业务中，为了高并发地接受大量用户订单请求，很少有电商网站真正实施那么简单的措施。&lt;/p>
&lt;p>有时候我们在思考问题的时候，多反问自己几个为什么，出于经验主义，很多时候第一反应都是需要打磨的。&lt;/p>
&lt;p>回到正题，对于订单的优先级，不同电商网站可能会采取不同的策略，比如有些电商根据下单时间判断谁可以购买成功，而有些会更偏向于付款时间来做出判断。但，无论采用什么样的规则去判断哪个用户可以下单成功，都必须要保证 990V4 在售出时，数据库中更新的库存是正确的。为了便于理解，我们以下单时间作为购买成功的判断依据。&lt;/p>
&lt;p>经过上面的提示，我们能想到最简单的方案就是，给 990V4 加一个锁。当有一个用户提交订单之后，后台服务器给库存数量加一个锁，根据该用户的订单修改库存。而其他用户则必须等到锁释放以后，才能重新获取库存数，继续购买。&lt;/p>
&lt;p>所以在当前这个案例中 990V4 的库存就是共享资源，不同的购买者对应着多个进程，后台服务器对共享资源加的锁就是告诉其他进程：“现在是我说了算，你们靠边站！”&lt;/p>
&lt;p>但问题就这么简单的解决了嘛？答案是否定的，继续往下分析。&lt;/p>
&lt;p>想象一下，用户 A 想买 1 双 990V4，用户 B 想买两双 990V4。在理想状态下，用户 A 网络质量稳定，率先下单买走了一双，这个时候库存也就还剩一双。此时应该提示用户 B 库存不足，用户 B 购买失败。但实际情况是，用户 A 和用户 B 同时获取到商品库存还剩 2 双，用户 A 买走 1 双，在用户 A 更新库存之前，用户 B 又买走了 2 双，此时用户 B 更新库存，商品还剩 0 双。这时，电商卖家就头大了，总共 2 双 990V4，却卖出去了 3 双。&lt;/p>
&lt;p>不难看出，如果只使用单机锁将会出现不可预知的后果。因此，在高并发场景下，为了保证临界资源同一时间只能被一个进程使用，从而确保数据的一致性，我们就需要引入分布式锁了。&lt;/p>
&lt;p>此外，在大规模分布式系统中，单个机器的线程锁无法管控多个机器对同一资源的访问，这时使用分布式锁，就可以把整个集群当作一个应用一样去处理，实用性和扩展性也就更好。&lt;/p>
&lt;h2 id="分布式锁的三种实现方法">分布式锁的三种实现方法&lt;/h2>
&lt;p>接下来，我带你看看实现分布式锁的 3 种主流方法，即：&lt;/p>
&lt;ul>
&lt;li>基于数据库实现分布式锁，这里的数据库指的是关系型数据库；&lt;/li>
&lt;li>基于缓存实现分布式锁；&lt;/li>
&lt;li>基于 ZooKeeper 实现分布式锁。&lt;/li>
&lt;/ul>
&lt;h3 id="数据库实现分布式锁">数据库实现分布式锁&lt;/h3>
&lt;p>实现分布式锁最直接的方式可以通过数据库进行实现，首先创建一张表用于&lt;strong>记录共享资源信息&lt;/strong>，然后通过操作该表的数据来实现共享资源信息的修改。&lt;/p>
&lt;p>当我们要锁住某个资源时，就在该表中增加一条记录，想要释放锁的时候就删除这条记录。数据库对共享资源做了&lt;strong>唯一性约束&lt;/strong>，如果有多个请求被同时提交到数据库的话，数据库会保证只有一个操作可以成功，操作成功的那个线程就获得了访问共享资源的锁，可以进行操作。&lt;/p>
&lt;p>基于数据库实现的分布式锁，是最容易理解的。但是，因为数据库需要落到硬盘上，频繁读取数据库会导致 IO 开销大，因此这种分布式锁适用于并发量低，对性能要求低的场景。对于电商系统双 11、双 12 等需求量激增的场景，数据库锁是无法满足其性能要求的。而在平日的购物中，我们可以在局部场景中使用数据库锁实现对资源的互斥访问。&lt;/p>
&lt;p>下面，我们还是以电商售卖 990V4 的场景为例。鞋子库存是 2 双，有 3 个来自不同地区的用户 {A, B, C} 想要购买，其中用户 A 想买 1 双，用户 B 想买 2 双，用户 C 想买 1 双。用户 A 和用户 B 几乎同时下单，但用户 A 的下单请求最先到达服务器。&lt;/p>
&lt;p>因此，该商家的产品数据库中增加了一条关于用户 A 的记录，用户 A 获得了锁，他的订单请求被处理，服务器修改 990V4 库存数，减去 1 后还剩下 1 双。当用户 A 的订单请求处理完成后，有关用户 A 的记录被删除，服务器开始处理用户 B 的订单请求。这时，库存只有 1 双了，无法满足用户 B 的订单需求，因此用户 B 购买失败。从数据库中，删除用户 B 的记录，服务器开始处理用户 C 的订单请求，库存中 1 双鞋子，刚好满足用户 C 的订单需求。所以，数据库中增加了一条关于用户 C 的记录，用户 C 获得了锁，他的订单请求被处理，服务器修改 990V4 数量，减去 1 后还剩下 0 双。&lt;/p>
&lt;p>&lt;img src="" alt="">&lt;/p>
&lt;p>可以看出，基于数据库实现分布式锁比较简单，关键在于创建一张锁表，为申请者在锁表里建立一条记录，记录建立成功则获得锁，消除记录则释放锁。该方法依赖于数据库，主要有两个缺点：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>单点故障问题&lt;/strong>：一旦数据库不可用，会导致整个系统崩溃。&lt;/li>
&lt;li>&lt;strong>死锁问题&lt;/strong>：数据库锁没有失效时间，未获得锁的进程只能一直等待已获得锁的进程主动释放锁。倘若已获得共享资源访问权限的进程突然挂掉、或者解锁操作失败，使得锁记录一直存在数据库中，无法被删除，而其他进程也无法获得锁，从而产生死锁现象。&lt;/li>
&lt;/ul>
&lt;h3 id="缓存实现分布式锁">缓存实现分布式锁&lt;/h3>
&lt;p>刚才说到，数据库的性能限制了业务的并发量，那么对于 6·18、双十一等需求量激增的场景是否有解决办法呢？&lt;/p>
&lt;p>这个时候，基于缓存实现分布式锁的方式，非常适合解决这种场景下的问题。&lt;strong>所谓基于缓存，也就是说把数据存放在计算机的内存中，不需要写入磁盘，从而减少了 IO 带来的资源损耗&lt;/strong>。接下来，就以 Redis 为例展开这部分内容。&lt;/p>
&lt;p>Redis 通常可以使用 setnx(key, value) 函数来实现分布式锁。key 和 value 就是基于缓存的分布式锁的两个属性，其中 key 表示锁 id，value = currentTime + timeOut，表示当前时间 + 超时时间。也就是说，某个进程获得 key 这把锁后，如果在 value 的时间内未释放锁，系统就会主动释放锁。&lt;/p>
&lt;p>setnx 函数的返回值有 0 和 1：&lt;/p>
&lt;ul>
&lt;li>返回 1，说明该服务器获得锁，setnx 将 key 对应的 value 设置为当前时间 + 锁的有效时间；&lt;/li>
&lt;li>返回 0，说明其他服务器已经获得了锁，进程不能进入临界区。该服务器可以不断尝试 setnx 操作，以获得锁。&lt;/li>
&lt;/ul>
&lt;p>我们还是以电商售卖 990V4 的场景为例，来说明基于缓存实现的分布式锁，假设现在库存数量是足够的。&lt;/p>
&lt;p>用户 A 的请求因为网速快，最先到达 Server2，setnx 操作返回 1，并获取到购买鞋子的锁；用户 B 和用户 C 的请求，几乎同时到达了 Server1 和 Server3，但因为这时 Server2 获取到了 990V4 数据的锁，所以只能加入等待队列。&lt;/p>
&lt;p>Server2 获取到锁后，负责管理 990V4 的服务器执行业务逻辑，只用了 1s 就完成了订单。订单请求完成后，删除锁的 key，从而释放锁。此时，排在第一顺位的 Server1 获得了锁，可以访问鞋子的数据资源。但不巧的是，Server1 在完成订单后发生了故障，无法主动释放锁。&lt;/p>
&lt;p>于是，排在第三顺位的 Server3 只能等设定的有效时间（比如 10 分钟）到期，锁自动释放后，才能访问鞋子的数据资源，也就是说用户 C 只能到 00:10:01 以后才能继续抢购。&lt;/p>
&lt;p>&lt;img src="" alt="">&lt;/p>
&lt;p>总结来说，&lt;strong>Redis 通过队列来维持进程访问共享资源的先后顺序&lt;/strong>。&lt;/p>
&lt;p>Redis 锁主要基于 setnx 函数实现分布式锁，当进程通过 setnx 函数返回 1 时，表示已经获得锁。排在后面的进程只能等待前面的进程主动释放锁，或者等到时间超时才能获得锁。&lt;/p>
&lt;p>相对于基于数据库实现分布式锁的方案来说，基于缓存实现的分布式锁的优势表现在以下几个方面：&lt;/p>
&lt;ul>
&lt;li>性能更好。数据被存放在内存，而不是磁盘，避免了频繁的 IO 操作。很多缓存可以跨集群部署，避免了单点故障问题。&lt;/li>
&lt;li>使用方便。很多缓存服务都提供了可以用来实现分布式锁的方法，比如 Redis 的 setnx 和 delete 方法等。&lt;/li>
&lt;li>可以直接设置超时时间（例如 expire key timeout）来控制锁的释放，因为这些缓存服务器一般支持自动删除过期数据。&lt;/li>
&lt;/ul>
&lt;p>这个方案的不足是，通过超时时间来控制锁的失效时间，并不是十分靠谱，因为一个进程执行时间可能比较长，或受系统进程做内存回收等影响，导致时间超时，从而不正确地释放了锁。&lt;/p>
&lt;p>为了解决基于缓存实现的分布式锁的这些问题，我们再来看看基于 ZooKeeper 实现的分布式锁吧。&lt;/p>
&lt;h3 id="zookeeper-实现分布式锁">Zookeeper 实现分布式锁&lt;/h3>
&lt;p>ZooKeeper 是基于树形数据存储结构实现分布式锁，来解决多个进程同事访问同一临界资源时，数据的一致性问题。ZooKeeper 的树形数据存储结构主要由 4 种节点构成：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>持久节点（PERSISTENT），这是默认的节点类型，一直存在于 ZooKeeper 中。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>持久顺序节点（PERSISTENT_SEQUENTIAL），在创建节点时，ZooKeeper 根据节点创建的时间顺序对节点进行编号命名。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>临时节点（EPHEMERAL），当客户端与 ZooKeeper 连接时临时创建的节点，与持久节点不同，当客户端与 ZooKeeper 断开连接后，该进程创建的临时节点会被删除。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>临时顺序节点（EPHEMERAL_SEQUENTIAL）。就是按时间顺序编号的临时节点。&lt;/p>
&lt;p>&lt;strong>根据上述节点的特征，ZooKeeper 基于临时顺序节点实现了分布式锁。&lt;/strong>&lt;/p>
&lt;p>还是以电商售卖 990 V4 为例，假设用户 A、B、C 同时在 11 月 11 日的零点整提交了购买鞋子的请求，ZooKeeper 会采用如下方法来实现分布式锁：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>在与该方法对应的持久节点 shared_lock 的目录下，为每个进程创建一个临时顺序节点。如下图所示，990 V4 就是一个拥有 shared_lock 的目录，当有人买鞋子的时候，会为他创建一个临时顺序节点。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>每个进程获取 shared_lock 目录下的所有临时节点列表，注册 Watcher，用于监听子节点变更的信息。当监听到自己的临时节点是顺序最小的，则可以使用共享资源。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>每个节点确定自己的编号是否是 shared_lock 下所有子节点中最小的，若最小，则获得锁。例如，用户 A 的订单最先到服务器，因此创建了编号为 1 的临时顺序节点 LockNode1。该节点的编号是持久节点目录下最小的，因此获取到分布式锁，可以访问临界资源，从而可以购买鞋子。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>若本进程对应的临时节点编号不是最小的，则分为两种情况：&lt;/p>
&lt;ol>
&lt;li>本进程为读请求，如果比自己序号小的节点中有写请求，则等待；&lt;/li>
&lt;li>本进程为写请求，如果比自己序号小的节点中有请求，则等待。&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ol>
&lt;p>例如，用户 B 也想要购买 990 V4，但在他之前，用户 C 想看看鞋子的库存量。因此，用户 B 只能等用户 A 买完吹风机、用户 C 查询完库存量后，才能下单购买。&lt;/p>
&lt;p>&lt;img src="" alt="">&lt;/p>
&lt;p>根据上面的流程，我们可以看出，使用 ZooKeeper 实现的分布式锁，可以解决前两种方法提到的各种问题，比如单点故障、不可重入、死锁等问题。但该方法实现比较复杂，且需要频繁地添加和删除节点，所以性能可能不如基于缓存实现的分布式锁。&lt;/p>
&lt;h2 id="小结">小结&lt;/h2>
&lt;/li>
&lt;/ul></content></entry><entry><title type="text">分布式 004——选举策略</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/distributedsystem/ds004_election/"/><id>https://quakewang.github.io/tech/distributedsystem/ds004_election/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-11-22T20:37:40+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="分布式选举">分布式选举&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>既然提到了分布式，集群就是绕不开的话题。简单来说，集群一般是由两个或两个以上的服务器组建而成，每个服务器都是一个节点。我们经常会听到数据库集群、管理集群等概念，也知道数据库集群提供了读写功能，管理集群提供了管理、故障恢复等功能。&lt;/p>……</summary><content type="html">&lt;h1 id="分布式选举">分布式选举&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>既然提到了分布式，集群就是绕不开的话题。简单来说，集群一般是由两个或两个以上的服务器组建而成，每个服务器都是一个节点。我们经常会听到数据库集群、管理集群等概念，也知道数据库集群提供了读写功能，管理集群提供了管理、故障恢复等功能。&lt;/p>
&lt;p>那么问题，对于一个集群来说，多个节点到底是怎么协同，怎么管理的呢。比如，数据库集群如何保证写入的数据在每个节点都一致呢？比如上一篇博客中谈到的分布式各个服务之间可能会打架——&lt;a href="https://quakewang.github.io/tech/distributedsystem/ds003_mutex/">互斥&lt;/a>。&lt;/p>
&lt;p>这个时候，你也许会说，这还不简单，选一个”领导“来负责调度和管理其他节点不就可以了嘛。诶，很不错。这就是我们常说的”主从“架构设计。这个”领导“在分布式叫做主节点，而选”领导“的过程在分布式领域中叫作分布式选举。&lt;/p>
&lt;p>关于如何选主这个过程，就是我们本篇的重点咯。&lt;/p>
&lt;h2 id="为什么要有分布式选举">为什么要有分布式选举？&lt;/h2>
&lt;p>主节点，在一个分布式集群中负责对其他节点的协调和管理，也就是说，其他节点都必须听从主节点的安排。主节点的存在，就可以保证其他节点的有序运行，以及需要保证写入每个节点的数据一致性。这里的一致性是指，数据在每个集群节点中都是一样的，不存在不同的情况。&lt;/p>
&lt;p>当然，如果主节点发生故障，集群就会处于一个混乱的状态，比如数据库集群中主节点故障后，可能导致每个节点上的数据一致性得不到保证。就好比，某个公司的 CEO 突然被辞退了，公司短时间可能就会出现人心不稳的情况。&lt;/p>
&lt;p>**这，就应那句话“国不可一日无君”，对应到分布式系统中就是“集群不可一刻无主”。**总结来说，选举的作用就是选出一个主节点，由它来协调和管理其他节点，以保证集群有序运行和节点间数据的一致性。&lt;/p>
&lt;h2 id="分布式选举的算法">分布式选举的算法&lt;/h2>
&lt;p>那么，如何在集群中选出一个合适的“领导者”呢？目前常见的选主方法有基于序号选举的算法，比如 Bully 算法、多数派算法，比如 Raft 算法、ZAB 算法等。下面的内容，我们一个一个来看。&lt;/p>
&lt;h3 id="bully-算法">Bully 算法&lt;/h3>
&lt;p>Bully 算法是一种极为主观的集群选主算法，为什么说是非常主观呢？因为它的选举原则是“长者”为大，简单来说，就是在所有活着的节点中，选取 ID 最大的节点作为主节点。&lt;/p>
&lt;p>在 Bully 算法中，节点的角色有两种，分别是普通节点和主节点。初始化时，所有节点都是平等的，都是普通节点，并且都有资格成为领导者。但是，当选主成功后，有且仅有一个节点成为主节点，其他所有节点都是普通节点。当且仅当主节点故障或与其它节点失去联系后，才会重新选主。&lt;/p>
&lt;p>Bully 算法在选举过程中，需要用到以下 3 种消息：&lt;/p>
&lt;ul>
&lt;li>Election 消息，用于发起选举；&lt;/li>
&lt;li>Alive 消息，对 Election 消息的应答；&lt;/li>
&lt;li>Victory 消息，竞选成功的主节点向其他节点发送的宣誓主权的消息。&lt;/li>
&lt;/ul>
&lt;p>Bully 算法选举的原则是“长者为大”，意味着它的&lt;strong>假设条件是，集群中每个节点均知道其他节点的 ID&lt;/strong>。在此前提下，其具体的选举过程是：&lt;/p>
&lt;ol>
&lt;li>集群中每个节点判断自己的 ID 是否为当前活着的节点中 ID 最大的，如果是，则直接向其他节点发送 Victory 消息，宣誓自己的主权；&lt;/li>
&lt;li>如果自己不是当前活着的节点中 ID 最大的，则向比自己 ID 大的所有节点发送 Election 消息，并等待其他节点的回复；&lt;/li>
&lt;li>若在给定的时间范围内，当前节点没有收到其他节点回复的 Alive 消息，则会判定自己成为主节点，并向其他节点发送 Victory 消息，宣誓自己成为主节点；若接收到来自比自己 ID 大的节点的 Alive 消息，则等待其他节点发送 Victory 消息；&lt;/li>
&lt;li>若该节点收到比自己 ID 小的节点发送的 Election 消息，则回复一个 Alive 消息，告知其他节点，我比你大，重新选举。&lt;/li>
&lt;/ol>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/61a1bd0a1a1b44356b44ddbaf80c57eeed2f8502/content/imag/tech/distributed/04_bully.svg" alt="bully">&lt;/p>
&lt;p>Bully 算法的选择是比较霸道直接的。谁活着且谁的 ID 最大谁就是主节点，其他节点必须无条件服从。这种算法的优点是：选举速度快、算法复杂度低，而且实现起来比较简单。缺点也很明显，主要在于：需要每个节点有全局的节点信息，就造成存储了大量的重复数据；其次，任意一个比当前节点 ID 大的新节点或节点故障后恢复加入集群的时候，都可能会触发重新选举，成为新的主节点，如果该节点频繁退出、加入集群，就会导致集群频繁换主。&lt;/p>
&lt;h3 id="raft-算法">Raft 算法&lt;/h3>
&lt;p>Raft 算是是典型的多数派投票选举算法，其选举机制有点类似于我们日常生活中的民主投票机制，其核心思想是“少数服从多数”。简单来说就是，Raft 算法中，获得投票最多的节点成为主节点。&lt;/p>
&lt;p>采用 Raft 算法选举，集群节点的角色有 3 种：&lt;/p>
&lt;ul>
&lt;li>Leader，即主节点，同一时刻只有一个 Leader，负责协调和管理其他节点；&lt;/li>
&lt;li>Candidate，即候选者，每一个节点都可以成为 Candidate，节点在该角色下才可以被选为新的 Leader；&lt;/li>
&lt;li>Follower，Leader 的跟随者，不可以发起选举。&lt;/li>
&lt;/ul>
&lt;p>Raft 选举的流程，可以分为以下几步：&lt;/p>
&lt;ol>
&lt;li>初始化时，所有节点均为 Follower 状态；&lt;/li>
&lt;li>开始选主时，所有节点的状态由 Follower 转化为 Candidate，并向其他节点发送选举请求；&lt;/li>
&lt;li>其他节点根据接收到的选举请求的先后顺序，回复是否同意成为主。这里需要注意的是，在每一轮选举中，一个节点只能投出一张票；&lt;/li>
&lt;li>若发起选举请求的节点获得超过一半的投票，则成为主节点，其状态转化为 Leader，其他节点的状态则由 Candidate 降为 Follower。Leader 节点与 Follower 节点之间会定期发送心跳包，以检测主节点是否存活；&lt;/li>
&lt;li>当 Leader 节点的任期到了，即发现其他服务器开始下一轮选主周期时，Leader 节点的状态由 Leader 降级为 Follower，进入新一轮选主。&lt;/li>
&lt;/ol>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/61a1bd0a1a1b44356b44ddbaf80c57eeed2f8502/content/imag/tech/distributed/04_raft.svg" alt="raft">&lt;/p>
&lt;p>请注意，&lt;strong>每一轮选举，每个节点只能投一次票&lt;/strong>。这种选举就类似人大代表选举，正常情况下每个人大代表都有一定的任期，任期到后会触发重新选举，且投票者只能将自己手里唯一的票投给其中一个候选者。对应到 Raft 算法中，选主时周期进行的，包括选主和任值两个时间段，选主阶段对应投票阶段，任值阶段对应节点成为主之后的任期。但也有例外的时候，如果主节点故障，会立马发生选举，重新选出一个主节点。&lt;/p>
&lt;p>Google 开源的 Kubernetes，擅长容器管理与调度，为了保证可靠性，通常会部署 3 个节点用于数据备份。这 3 个节点中，有一个会被选为主，其他节点作为备。Kubernetes 的选主采用的是开源的 etcd 组件。而，etcd 的集群管理器 etcds，是一个高可用、强一致性的服务发现存储仓库，就是采用了 Raft 算法来实现选主和一致性的。&lt;/p>
&lt;p>Raft 算法具有选举速度快、算法复杂度低、易于实现的优点；缺点是，它要求系统内每个节点都可以相互通信，且需要获得过半的投票数才能选主成功，因此通信量大。该算法选举稳定性比 Bully 算法好，这是因为当有新节点加入或故障恢复后，会触发选主，但不一定会真正切主，除非新节点或故障后恢复的节点获得投票数过半，才会导致切主。&lt;/p>
&lt;h3 id="zab-算法">ZAB 算法&lt;/h3>
&lt;p>ZAB（Zookeeper Atomic Broadcast）选举算法是 Zookeeper 实现分布式协调功能而设计的。相较于 Raft 算法的投票机制，ZAB 算法增加了通过节点 ID 和数据 ID 作为参考进行选主，节点 ID 和数据 ID 越大，表示数据越新，优先成为主。相比较于 Raft 算法，ZAB 算法尽可能保证数据的最新性。所以，ZAB 算法可以说是对 Raft 算法的改进。&lt;/p>
&lt;p>使用 ZAB 算法选举时，集群中每个节点拥有 3 种角色：&lt;/p>
&lt;ul>
&lt;li>Leader，主节点；&lt;/li>
&lt;li>Follower，跟随者节点；&lt;/li>
&lt;li>Observer，观察者，无投票权。&lt;/li>
&lt;/ul>
&lt;p>选举过程中，集群中的节点拥有 4 个状态：&lt;/p>
&lt;ul>
&lt;li>Looking 状态，即选举状态。当节点处于该节点时，它会认为当前集群中没有 Leader，因此自己进入选举状态；&lt;/li>
&lt;li>Leading 状态，即领导者状态，表示已经选出主，且当前节点为 Leader；&lt;/li>
&lt;li>Following 状态，即跟随者状态，集群中已经选出主后，其他非主节点状态更新为 Following，表示对 Leader 的追随；&lt;/li>
&lt;li>Observeing 状态，即观察者状态，表示当前节点为 Observer，持观望态度，没有投票权和选举权。&lt;/li>
&lt;/ul>
&lt;p>投票过程中，每个节点都有一个唯一的三元组（server_id，server_zxID，epoch），其中 server_id 表示本节点的唯一 ID；server_zxID 表示本节点存放的数据 ID，数据 ID 越大表示数据越新，选举权重越大；epoch 表示当前选取轮数，一般用逻辑时钟表示。&lt;/p>
&lt;p>ZAB 选举算法的核心是“少数服从多数，ID 大的节点优先成为主”，因此选举过程中通过 vote_id，vote_zxID 来表明投票给哪个节点，其中 vote_id 表示投票节点的 ID，vote_zxID 表示被投票节点的服务器 zxID。&lt;strong>ZAB 算法选主的原则是：server_zxID 最大者成为 Leader；若 server_zxID 相同，则 server_id 最大者成为 Leader&lt;/strong>。&lt;/p>
&lt;p>下面就以 3 个 Server 的集群为例，此处每个 Server 代表一个节点，来介绍 ZAB 选主的过程。&lt;/p>
&lt;p>第一步：当系统刚启动时（三个服务器同时启动），3 个服务器当前投票均为第一轮投票，即 epoch = 1，且 zxID 均为 0。此时每个服务器都会优先推选自己，并将投票信息 &amp;lt;epoch, voted_id, vote_zxID&amp;gt; 广播出去。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/61a1bd0a1a1b44356b44ddbaf80c57eeed2f8502/content/imag/tech/distributed/04_zab01.svg" alt="zab01">&lt;/p>
&lt;p>第二步：根据判断规则，由于 3 个 Server 的 epoch、zxID 都相同，因为比较 server_id，较大者即为推选对象，因此 Server1 和 Server2 将 vote_id 改为 3，更新自己的投票信息，并重新广播自己的投票。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/61a1bd0a1a1b44356b44ddbaf80c57eeed2f8502/content/imag/tech/distributed/04_zab02.svg" alt="zab02">&lt;/p>
&lt;p>第三步：此时系统内所有的服务都推选 Server3，超过半数，因此 Server3 当选 Leader，处于 Leading 状态，向其他服务器发送心跳包并维护连接；Server1 和 Server2 处于 Following 状态。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/61a1bd0a1a1b44356b44ddbaf80c57eeed2f8502/content/imag/tech/distributed/04_zab03.svg" alt="zab03">&lt;/p>
&lt;p>简单来说，ZAB 算法性能高，对系统无特殊要求，采用广播方式发送信息，若节点中有 n 个节点，每个节点同时广播，则集群中信息量为 n*(n-1) 个消息，同意出现广播风暴；且除了投票，还增加了对比节点 ID 和数据 ID，这就意味着还需要知道所有节点的 ID 和数据 ID，所以选举时间相对较长。但该算法选举稳定性比较好，当有新节点加入货节点故障后，出触发选主，但不一定真正切主，除非新节点或故障后恢复的节点数据 ID 和节点 ID 最大，且获得投票数过半，才会导致切主。&lt;/p>
&lt;h2 id="小结">小结&lt;/h2>
&lt;p>本篇内容讲解了什么是分布式选举，以及为什么需要分布式选举。然后走马观花地介绍了 Bully 算法、Raft 算法以及 ZooKeeper 中的 ZAB 算法，并通过实例展示了各类方法的选举流程。&lt;/p>
&lt;p>最后聊一聊为什么“多数派”选主算法通常采用奇数节点，而不是偶数节点呢？&lt;/p>
&lt;p>多数派选主算法的核心是少数服从多数，获得投票多的节点胜出。想象一下，如果现在采用偶数节点集群，当两个节点均获得一半投票时，到底应该选谁为主呢？&lt;/p>
&lt;p>答案是，在这种情况下，无法选出主，必须重新投票选举。但即使重新投票选举，两个节点拥有相同投票数的概率也会很大。&lt;/p>
&lt;p>因此，多数派选主算法通常采用奇数节点。这，也是大家通常看到 ZooKeeper、 etcd、Kubernetes 等开源软件选主均采用奇数节点的一个关键原因。&lt;/p></content></entry><entry><title type="text">不想再谈 Web3</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/web3/web3-001_introduce/"/><id>https://quakewang.github.io/tech/web3/web3-001_introduce/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-11-19T13:09:48+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="写在-2023-立冬之后不想再谈-web-30">写在 2023 立冬之后，不想再谈 Web 3.0&lt;/h1>
&lt;p>和我走的比较近的朋友都知道，我最近对于 Rust 比较感兴趣，关于这门系统级编程语言有哪些优点这里不想多说，但其中最关键的一点是安全，这也就造就了她独有的魅力。&lt;/p>……</summary><content type="html">&lt;h1 id="写在-2023-立冬之后不想再谈-web-30">写在 2023 立冬之后，不想再谈 Web 3.0&lt;/h1>
&lt;p>和我走的比较近的朋友都知道，我最近对于 Rust 比较感兴趣，关于这门系统级编程语言有哪些优点这里不想多说，但其中最关键的一点是安全，这也就造就了她独有的魅力。&lt;/p>
&lt;p>八月底参加了 GTR@Hangzhou 也是第一次参与 Rust 技术相关的 meetup，以及九月下旬去上海看了 GOSIM，其中会有不少开发者都是做 Web 3.0 这一块的。我第一次听 Web 3.0 是在 20 年年底的时候，当时大概的印象就是&lt;strong>去中心化&lt;/strong>。只会觉得很有发展前景，但却很难普及。&lt;/p>
&lt;p>最近立冬，天变冷了，闲着无聊，不如开始重新认识一下 Web 3.0。&lt;/p>
&lt;h2 id="互联网的诞生">互联网的诞生&lt;/h2>
&lt;p>如果想要弄清楚 Web 3.0 的诞生，先来纵向梳理一下互联网的发展历程和现状。&lt;/p>
&lt;p>不言而喻的是，互联网的发展是人类发展步入到信息社会的前提，一方面，大量信息可以被低成本的记录和传播，因为随着硬件技术的发展，存储成本是不断下降的；另一方面，当这些信息内容的总量达到一定量级之后，会对社会和经济活动产生巨大的影响。人们常说的大数据，就是在这种情况下发展的产物，数据变成资源，哪家公司掌握的用户数据越多，可操作的空间就越大。&lt;/p>
&lt;p>在上个世纪 90 年代，互联网的诞生可以说是在纸质媒体之外创造了一个全新的传播渠道，诞生了一批专业的内容生产平台 PGC，此时的互联网是一个只读网络，所有网站的内容都由运营者提供，用户只能观看，是静态互联网。其中代表性的企业包括雅虎、新浪、搜索等门户网站，各网媒雇佣大批编辑，将图文并茂的内容发布为网页。对于读者来说，能做的只有访问网站，浏览数字内容，但读者不能写，无法参与内容的操作。这一时期的互联网是单向的，互联网产生的数据和用户的关系不大。这就是 &lt;strong>Web 1.0 时代&lt;/strong>。&lt;/p>
&lt;p>接着，随着时间的推移，用户开始不满足“只有输入没有输出”的状态了，于是在用户渴望表达自己想法的诉求下，催生出了一大批可以为用户提供创作和表达的平台，比如 Twitter、微博 等。除此之外，还有就是需要用户参与的电商平台、视频网站，以及为这些服务提供终端可基础设施的科技公司，比如微软和苹果，也纷纷涌现。这个阶段，我们在这过渡到 &lt;strong>Web 2.0 时代&lt;/strong>。&lt;/p>
&lt;h2 id="数据资源">数据资源&lt;/h2>
&lt;p>在这一时期，用户的各种行为产生了大量数据，但这些数据的特点是&lt;strong>存储即拥有&lt;/strong>，而不是&lt;strong>谁创造谁拥有&lt;/strong>。这样一来，就会导致如下问题：互联网公司凭借收集的数据和掌握数据资源的优势，可以随便操纵分析用户的数据，为企业谋取利益最大化，也就有了所展示出来的私人立场和公共利益的冲突日益变大。&lt;/p>
&lt;p>我们可以看一下 Meta 这家公司（也就是 Facebook），截止 2021 年末最高市值达到 1 万亿美元，如果按照这个数值参与当年各国 GDP 排名的话，大概可以排进全球前十五的位置。就其背后原因，是 19 亿日活用户和 29 亿月活用户，月活用户在全球人口中占比高达 36%。但在 Facebook 主导的互联网里面，你就必须得遵守它所设定的规则，方面的例子就是当年追求连任的川建国同志一样，被“社交性死亡”。&lt;/p>
&lt;p>由此及彼，现如今的互联网服务差不多都是这个样子，其他领域也是同理。比如，在国内我们常用的沟通工具就是微信，随着网络技术的普及，无线网的覆盖面积越来越大，运营商的流量相较以往也在降价，所以作为用户，是以极低的代价使用这些工具。而作为公司来说，肯定是想盈利的。他们为用户提供了沟通的服务，也随之掌握了用户的信息。通过信息变现，会向用户投送广告，作为用户，是不太愿意看见这些广告的。&lt;/p>
&lt;p>这就是事情的关键，你在互联网上获得了很多服务，但你在互联网上的每一步，其实都离不开某一个互联网巨头的“陪伴”。每个互联网巨头又都为他们的产品设计了一整套的制度，在这套制度面前，用户就像一个孩子在面对家长或者老师，几乎没有任何讨价还价的能力和空间。这一时期互联网功能可谓非常强大，但用户离开互联网公司，可谓是“寸步难行”。&lt;/p>
&lt;p>这个时期，我们感受到了互联网服务的强大，但也深陷离开互联网公司就“寸步难行”的困境！互联网公司利用用户对自己产品的依赖，正在像“灰犀牛”一样，把私人机构的价值诉求，变成某种社会规则。&lt;/p>
&lt;h2 id="web-30互联网的革命">Web 3.0：互联网的革命？&lt;/h2>
&lt;p>那么有的人可能会说，如果数据归用户自己所有，不就可以了嘛。这也就是 Web 3.0 要做的事情！&lt;strong>把互联网打开，从技术层面让互联网实现一种开放性，再通过技术逻辑的再造去改变它的“精神气质”&lt;/strong>。&lt;/p>
&lt;p>不过，关于 Web 3.0 还没有形成一个大家公认的定义。比如以太坊的前 CTO，Polkadot 创始人加文·伍德（Gavin Wood）就提出，Web 3.0 是一组包容性协议，可为应用程序构建模块；这些模块取代了传统的 Web 技术，如 HTTP 和 MySQL 等，同时可以提供一种全新的创建应用程序的方式。再比如著名硅谷风投机构 A16Z，将 Web 3.0 定义为“一组包含区块链、加密协议、数字资产、去中心化金融和社交平台的技术”。&lt;/p>
&lt;p>简单来说可以这样理解，Web 3.0 即是互联网功能的协议化，协议仅按照代码体现的有限规则运行，既没有更多的利益诉求，也没有更多地投射到社会层面的价值主张；协议之间可以通过相互组合实现功能的叠加，并体现出一种开放和协作的精神。&lt;/p>
&lt;p>举个例子，支付宝的诞生对于电商的兴起来说，是发挥了基础性的作用的，因为在没有支付宝的情况下，买家和卖家互不相识且互不信任，卖家担心发货之后收不到货款，买家担心付款之后收不到货，交易几乎无法完成。而支付宝就在买家和卖家之间，发挥了一种“担保交易”的功能，来组织资金结算，进而促进了贸易的达成。但支付宝的想法并没有到此为止，而是在支付的基础上继续绑定借贷业务，通过推出交易金额越大，信用额度也就越大的激励政策，推荐自己的借贷业务，这就是 Web 2.0 的典型模式。但我们要注意一点，支付业务和借贷业务的绑定，其实会对借贷市场的其他金融机构形成一种排斥效果，这明显对于金融市场扩大供给、促进市场竞争是不利的。&lt;/p>
&lt;p>那么，在 Web 3.0 网络中，支付宝的作用就不再由某一家机构承担了，而是由一个协议来承担，这样就算没有第三方机构作为中介，不信任的双方仍可以安全地进行交易结算，这种协议就叫 HTLC，全名叫哈希时间锁协议，它还可以为其他金融机构创造一种更加公平的竞争环境。&lt;/p>
&lt;p>功能的协议化仅仅是 Web3.0 建设的第一步，在这些协议的背后，还需要一个作为“硬件系统”的支撑协议运行的分布式计算网络，和一个作为“软件系统”的通用的、围绕用户的身份验证、数据记录以及如何使用授权和激励的经济系统。&lt;/p>
&lt;h2 id="硬件系统">硬件系统&lt;/h2>
&lt;p>“硬件系统”为协议执行提供技术支撑，从底层到应用层分别实现通信、计算、存储和交互等功能，并且在每个环节中都体现出一种“可替代性”，也就是说这些功能并不是依赖于某个特定的机构或者组织才能实现的，但是功能的逻辑是准确而可信的，这种特性又被称为是“去信任的信任”，即 Trustless Trust。&lt;/p>
&lt;p>比如，在目前互联网中，我们所有的程序在执行计算任务的时候，都要自行解决输入的可用性、可靠性以及相互之间的授权问题，还需要因此花费额外的资源，去解决各种不兼容和安全风险等问题。而定位于“互联网计算机”的 IC 协议，就提供了一种 Web 3.0 的解决方案。&lt;/p>
&lt;p>IC 协议是基于区块链共识机制，在 TCP/IP 协议和应用层之间构建了一个包括多个物理节点的虚拟子网；子网内部的节点对输入输出达成共识，可以相互验证计算结果；多个子网之间可以通信，并且可以通过相互组合，实现计算能力的大幅提升。&lt;/p>
&lt;p>其实，类似于 IC 的设计理念，在通信、计算、存储等领域也有类似的探索，这些协议通过相互组合为数据的产生、存储、调用以及隐私保护创造了一个不依赖于任何特定机构的，且功能完整的链条，从而使得互联网具备了一种克服“单点故障”的基本架构。&lt;/p>
&lt;h2 id="经济系统">经济系统&lt;/h2>
&lt;p>而在“硬件系统”之外，围绕用户还需要一套针对身份认证、数据记录以及使用授权和激励的经济系统。这套系统需要内嵌到协议的执行过程，协议的实施无需任何第三方机构的参与和帮助。基于这套经济系统，不同的协议就可以相互叠加和组合，并实现经济利益的协调。&lt;/p>
&lt;p>那么，从“硬件和软件”系统的功能来看，区块链就是 Web 3.0 发展高度依赖的一项技术，区块链不仅可以不依赖任何第三方机构实现去信任的协作，还可以通过代币系统对系统成员进行激励。但是，区块链并不是 Web 3.0 的全部，因为区块链的主要作用是建立一套可信共享账本，而不能为 Web 3.0 需要解决的全部问题提供解决方案；Web 3.0 还将通过区块链、隐私计算，人工智能甚至物联网技术的结合获得更大的发展空间。&lt;/p>
&lt;p>但需要注意的是，虽然前面讲了很多“硬件和软件”的组合，但从用户使用的角度看，Web 3.0 与 Web 2.0 可能没有什么太大的区别。Web 3.0 通过分布式技术架构和经济系统的打造，将创造一个全新的商业模式，一个高度联系、无国界的数字经济体，并产生大量的自下而上的创新机会。&lt;/p>
&lt;p>Web 3.0 的出现反映了一种底层技术的变革在产业链层面带来的新的职能分工，而新的职能分工往往就意味着新的业务模式和新的产业机会。所以，Web 3.0 就是基于技术的方式，对现有互联网进行的一次“破坏性创新”。&lt;/p>
&lt;h2 id="写在之后">写在之后&lt;/h2>
&lt;p>Web 3.0 从技术的角度赋予了互联网开放的精神气质，通过分布式技术架构和经济系统的打造，将创造一个全新的商业模式，一个高度联系、无国界的数字经济体。&lt;/p>
&lt;p>但基于目前的环境，我想说 Web 3.0 只能说是一个美好的愿景，实现起来很难很难，触及到的利益链条太多了。但朝这个方向发展是大势所趋，只不过会比较漫长。&lt;/p>
&lt;p>有的时候很喜欢从技术的角度，去解读现如今的世界，倒不如说技术是为了更好地服务。比如说，在 Web 3.0 的概念中，&lt;strong>计算将会变成一种通用服务&lt;/strong>，这一点我是认可的，毕竟让数据回归到创造数据的人手里才是终点，从而就能摆脱数据垄断。这也就提出“公链”这个概念，大家常说的以太坊就是这样一种提供通用计算服务的基础设施。&lt;/p>
&lt;p>所以如果可以的话，我想回归重新去做 infra。还是拿以太坊来继续往下写一点吧。以太坊是在比特币的基础上，实现了图灵完备的智能合约，它理论上可以支持任何形式的去中心化应用，但毕竟是理论上。事实上，&lt;strong>以太坊只能支持有限数量的去中心化应用&lt;/strong>。就其主要原因就是著名的 CAP 不可能三角定理。也就是说，一个分布式系统中，在一致性、可用性和分区容错性三项特征中，最多只能存在其中两个。&lt;/p>
&lt;p>好好好。打住！，啰里八嗦的写了一堆。整理这些资料的时候，也让自己对于技术的发展有了一个重新的认识，也算是一个科普向的文章，并未涉及到了技术方面的知识，毕竟现如今这个社会上，懂不懂技术都可以好好生活，不是嘛。&lt;/p>
&lt;p>写得比较片面，也比较主观。。&lt;/p></content></entry><entry><title type="text">分布式 003——互斥</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/distributedsystem/ds003_mutex/"/><id>https://quakewang.github.io/tech/distributedsystem/ds003_mutex/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-11-11T11:23:03+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="聊聊分布式互斥">聊聊分布式互斥&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>在之前的两篇博客中，完成了分布式的基础入门，也就是知道了什么是分布式，以及分布式有哪些指标。下面一个篇章，将会聚焦于&lt;strong>分布式协调与同步&lt;/strong>的内容。简单来说就是，如何让程序通过协作共同去达成一个业务目标。&lt;/p>……</summary><content type="html">&lt;h1 id="聊聊分布式互斥">聊聊分布式互斥&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>在之前的两篇博客中，完成了分布式的基础入门，也就是知道了什么是分布式，以及分布式有哪些指标。下面一个篇章，将会聚焦于&lt;strong>分布式协调与同步&lt;/strong>的内容。简单来说就是，如何让程序通过协作共同去达成一个业务目标。&lt;/p>
&lt;h2 id="什么是分布式互斥">什么是分布式互斥？&lt;/h2>
&lt;p>假设如下的场景，你正在吃自选餐，刚刚寻找到你想吃的菜品，突然在这时有一个抠脚大汉过来，把你挤到一边。看着他那体型，你只好耐着性子等他打完菜，再继续打自己的菜。结果你夹菜到一半他又回来中断了你的过程。如果这样反复来几次，你肯定会想，这老几就是来找茬的，马上就会来一场“有你没我，有我没你”的格斗了。&lt;/p>
&lt;p>上述的场景，也同样存在于分布式集群环境。就像我们在选菜过程中不希望被打断一样，对于同一共享资源，一个程序正在使用的时候也不希望被其它程序进程所打扰。这就要求&lt;strong>在同一时刻只能有一个程序能够访问这种资源&lt;/strong>。&lt;/p>
&lt;p>在分布式系统里，这种排他性的资源访问形式，叫作&lt;strong>分布式互斥&lt;/strong>，而这种被互斥访问的共享资源叫作&lt;strong>临界资源&lt;/strong>。&lt;/p>
&lt;p>接下来，一起看看如何才能让分布式系统中的程序互斥地访问临界资源。&lt;/p>
&lt;h2 id="集中式算法">集中式算法&lt;/h2>
&lt;p>对于前文提到的打菜问题，是不是可以增加一个“协调者”来约束大家文明就餐，从而避免强行插入打断别人进程的问题。&lt;/p>
&lt;p>那么，按照这种方式，我们可以在分布式集群中，引入一个协调者程序，得到一个分布式互斥算法。每个程序在需要访问临界资源时，先给协调者发送一个请求。如果当前这个资源处于空闲状态，也就是没有程序正在使用，协调者直接授权请求程序访问；否则，可以按照先来后到的顺序为请求程序“排一个号”。如果有程序使用完资源，则通知协调者，协调者从“排号”的队列里面取出排在最前面的请求，并给它发送授权消息。拿到授权消息的程序，可以直接去访问临界资源。&lt;/p>
&lt;p>这就是 FIFO 的模式，这个互斥算法，就是我们常说的&lt;strong>集中式算法&lt;/strong>。之所以这么称呼，是因为协调者代表着集中程序，如下图所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/417aeb57963aa0bd003b05a377f0fb55ccef6df1/content/imag/tech/distributed/03_centrailzed.svg" alt="central">&lt;/p>
&lt;p>程序 1、2、3、4 为普通运行的程序，另一个程序为协调者。当程序 2 和 程序 4 需要使用临界资源时，会先向协调者发送申请，请求协调者授权。&lt;/p>
&lt;p>不巧的是，程序 3 正在使用临界资源。这时，协调者会根据程序 2 和 4 的申请顺序，依次将它们放入等待队列。比如当前的案例是，程序 4 的申请时间早于程序 2，因此排在程序 2 的前面。&lt;/p>
&lt;p>程序 3 使用完临界资源后，通知协调者释放授权。此时，协调者从等待队列中通知程序 4，并给它发放授权。这时，程序 4 就可以使用临界资源了。依次类推，直到所有申请结束。&lt;/p>
&lt;p>基于上述流程可以看出，&lt;strong>一个程序完成一次临界资源访问，需要如下几个流程和信息交互：&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>向协调者发送请求授权信息，1 次消息交互；&lt;/li>
&lt;li>协调者向程序发放授权信息，1 次消息交互；&lt;/li>
&lt;li>程序使用完临界资源后，向协调者发送释放授权，1 次信息交互。&lt;/li>
&lt;/ol>
&lt;p>因此，每个程序完成一次临界资源访问，至少需要进行 3 次信息交互。&lt;/p>
&lt;p>集中式算法的优点在于直观、简单、信息交互量少、易于实现，并且所有程序只需和协调者通信，程序之间无需通信。但缺点也很明显，就是在协调者自身，原因如下：&lt;/p>
&lt;ul>
&lt;li>一方面，协调者会成为系统的性能瓶颈。假设如果有 200 个程序要访问临界资源，那么协调者要处理 200*3=600 条消息。也就是说，协调者处理的消息数量会随着需要访问临界资源的程序数量线性增加；&lt;/li>
&lt;li>另一方面，容易引发单点故障。当协调者的受到的请求访问量过大，如果某一进程特别重，可能会引发协调者故障，就会导致所有程序都无法访问临界资源，可能会进一步造成系统处于不可用状态。&lt;/li>
&lt;/ul>
&lt;p>因此，在使用集中算法的时候，一定要选择性能好、可靠性高的服务器来运行协调者。&lt;/p>
&lt;h2 id="分布式算法">分布式算法&lt;/h2>
&lt;p>既然引入协调者会带来一些问题，那不用协调者是否可以实现对临界资源的互斥访问呢？比如试试看，能不能协商着来。在访问某块资源之前，先征求其他人的意见，在确认其他人都没有意向使用的时候，就属于来到自己的主场了。&lt;/p>
&lt;p>同理，我们可以把这种方式用于分布式系统。当一个程序要访问临界资源时，先向系统中的其他程序发送一条请求信息，在接受到所有程序返回的同意消息后，才可以访问临界资源。其中请求消息需要包含请求的资源、请求者的 ID，以及发送请求的时间。这就是&lt;strong>民主协商法&lt;/strong>。&lt;/p>
&lt;p>如图所示，程序 1、2、3 需要访问资源 A。在时间戳为 8 的时刻，程序 1 想要使用资源 A，于是向程序 2 和 3 发起使用资源 A 的申请，希望得到它们的同意。在时间戳为 12 的时刻，程序 3 想要使用资源 A，于是向程序 1 和 2 发起访问资源 A 的请求。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/417aeb57963aa0bd003b05a377f0fb55ccef6df1/content/imag/tech/distributed/03_ds01.svg" alt="ds01">&lt;/p>
&lt;p>如果此时程序 2 暂时不访问资源 A，因此同意了程序 1 和 3 的资源访问请求。对于程序 3 来说，由于程序 1 提出请求的实践更早，因此同意程序 1 先使用资源，并等待程序 1 返回用以消息。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/417aeb57963aa0bd003b05a377f0fb55ccef6df1/content/imag/tech/distributed/03_ds02.svg" alt="ds02">&lt;/p>
&lt;p>程序 1接收到其他所有程序的同意消息之后，开始用资源 A。当程序 1 使用完资源 A 后，释放使用权限，向请求队列中需要使用资源 A 的程序 3 发送同意使用资源的消息，并将程序 3 从请求队列中删除。此时，程序 3 收到了其他所有程序的同意消息，获得了使用资源 A 的权限，开始用临界资源 A。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/417aeb57963aa0bd003b05a377f0fb55ccef6df1/content/imag/tech/distributed/03_ds03.svg" alt="ds03">&lt;/p>
&lt;p>从上述流程可以看出，一个程序完成一次临界资源的访问，需要进行如下的信息交互：&lt;/p>
&lt;ol>
&lt;li>向其他 n-1 个程序发送访问临界资源的请求，总共需要 n-1 次消息交互；&lt;/li>
&lt;li>需要接收到其他 n-1 个程序回复的同意消息，方可访问资源，总共需要 n-1 次消息交互。&lt;/li>
&lt;/ol>
&lt;p>可以得出以下结论，一个程序要成功访问临界资源，至少需要 2*(n-1) 次消息交互。假设，现在系统中的 n 个程序都要访问临界资源，则会同时产生 2(n-1) *n 条消息。总结来说，&lt;strong>在大型系统中使用分布式算法，消息数量会随着需要访问临界资源的程序数量呈指数级增加，容易导致高昂的“沟通成本”&lt;/strong>。&lt;/p>
&lt;p>从上述分析不难看出，分布式算法根据“先到先得”以及“投票全票通过”的机制，让每个程序按时间顺序公平地访问资源，简单粗暴、易于实现。但，这种算法可用性很低，主要包括以下两个方面：&lt;/p>
&lt;ul>
&lt;li>当系统内需要访问临界资源的程序增多时，容易产生“信令风暴”，也就是程序收到的请求完全超过了自己的处理能力，从而导致自己正常的业务无法继续开展；&lt;/li>
&lt;li>一旦某一程序发生故障，无法发送同意消息，那么其他程序均除以等待回复的状态中，使得整个系统处于停滞状态，导致整个系统不可用。所以，相对于集中算法的协调者故障，分布式算法的可用性更低。&lt;/li>
&lt;/ul>
&lt;p>针对可用性低的一种改进办法是，如果检测到一个程序故障，则直接忽略这个程序，比如网络阻塞，长时间接收不到返回的消息，既然等不到那么就无需再等咯。&lt;/p>
&lt;p>这就好比在自助餐厅，一个人离开了餐厅，你想选某个菜品也就无需再征求他的建议。但这样的话，每个程序都需要对其他程序进行故障检测，这无疑带来了更大的复杂性。&lt;/p>
&lt;p>因此，&lt;strong>分布式算法适合节点数目少且变动不频繁的系统，且由于每个程序均需通信交互，因此适合 P2P 结构的系统&lt;/strong>。比如，运行在局域网中的分布式文件系统。&lt;/p>
&lt;h2 id="令牌环算法">令牌环算法&lt;/h2>
&lt;p>既然集中式算法、分布式算法都存在一定的缺陷，那么还有什么方法可以实现分布式互斥嘛？答案是肯定的。比较方法总比问题多。轮值 CEO 其实就给了我们一个很好的启示：在轮值 CEO 体系里，CEO 就是临界资源，同时只能有一个人担任，由多名高管轮流出任 CEO。&lt;/p>
&lt;p>类似的，程序访问临界资源问题也可以按照轮值 CEO 的思路实现，如下图所示，所有程序构建一个环结构，令牌按照指定的顺序（顺时针或逆时针）方向在程序之间传递，收到令牌的程序有权访问临界资源，访问完成后将令牌传送给下一个程序；若该程序不需要访问临界资源，则直接把令牌传送给下一个程序。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/8445f327792283513e7cbf3990323575a2074f53/content/imag/tech/distributed/03_poll.svg" alt="poll">&lt;/p>
&lt;p>在分布式领域，通常将这个算法叫作令牌环算法。为了便于理解记忆，可以形象地称之为轮值 CEO 算法。&lt;/p>
&lt;p>因为在使用临界资源前，不需要像分布式算法那样挨个征求其他程序的意见了，所以相对而言，在令牌环算法里单个程序具有更高的通信效率。同时，在一个周期内，每个程序都能访问到临界资源，因此令牌环算法的公平性很好。&lt;/p>
&lt;p>但是，不管环中的程序是否想要访问资源，都需要接收并传递令牌，所以也会带来一些无效通信。假设系统中有 100 个程序，那么程序 1 访问完资源后，即使其它 99 个程序不需要访问，也必须要等令牌在其他 99 个程序传递完后，才能重新访问资源，这就降低了系统的实时性。&lt;/p>
&lt;p>综上，令牌环算法的公平性高，在改进单点故障后，稳定性也很高，适用于系统规模较小，并且系统中每个程序使用临界资源的频率高且使用时间比较短的场景。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>在这篇博客中，我们介绍了分布式互斥的概念和几种实现方法，包括集中式算法，分布式算法和令牌环算法。&lt;/p>
&lt;p>集中式算法引入了一个协调者程序，每个程序在需要访问临界资源时，先给协调者发送一个请求。协调者根据请求的顺序，决定哪个程序可以访问资源。集中式算法的优点是简单和易于实现，但是协调者可能会成为系统的性能瓶颈，或者引发单点故障。&lt;/p>
&lt;p>分布式算法的思路是，一个程序在访问临界资源时，先向其他程序发送请求。只有当所有程序都同意之后，该程序才可以访问资源。这种方式可以避免单点故障的问题，但是可能会产生大量的消息交互，导致“信令风暴”。此外，如果某个程序发生故障，那么整个系统可能会处于停滞状态。&lt;/p>
&lt;p>令牌环算法则是所有程序构成一个环，令牌按照一定的顺序在程序之间传递。只有拿到令牌的程序才能访问资源。这种方法避免了无效的通信，提高了通信效率。同时，由于在一个周期内，每个程序都能访问到资源，因此令牌环算法的公平性很好。但是，不论程序是否需要访问资源，都需要接收并传递令牌，这可能会降低系统的实时性。&lt;/p>
&lt;p>总的来说，每种算法都有其适用的场景和局限性。在实际使用中，需要根据系统的规模，程序的数量，以及临界资源的使用频率和使用时间等因素，选择最合适的算法。同时，也可以考虑使用混合的方式，结合多种算法的优点，以满足不同的需求。&lt;/p></content></entry><entry><title type="text">分布式 002——分布式系统的指标</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/distributedsystem/ds002_norm/"/><id>https://quakewang.github.io/tech/distributedsystem/ds002_norm/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-11-06T10:31:34+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="分布式系统有哪些指标">分布式系统有哪些指标？？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>经过上一篇博客，我们简单了解了分布式的起源，对于分布式技术有了一个整体的印象。这篇内容将会回归理性，一起来看看可以用哪些指标来具体地衡量一个分布式系统。&lt;/p>……</summary><content type="html">&lt;h1 id="分布式系统有哪些指标">分布式系统有哪些指标？？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>经过上一篇博客，我们简单了解了分布式的起源，对于分布式技术有了一个整体的印象。这篇内容将会回归理性，一起来看看可以用哪些指标来具体地衡量一个分布式系统。&lt;/p>
&lt;p>从分布式技术的起源可以看出，分布式系统的出现就是为了用廉价的、普通的机器解决单个服务器处理复杂、大规模数据和任务所存在的性能问题、资源瓶颈问题，以及可用性和可扩展性问题。因为我们知道如果要想使得一台机器能兼顾所有的性能，成本是非常昂贵的。简单来说，分布式的目的就是用尽可能低的成本，处理更多的数据和更复杂的任务。&lt;/p>
&lt;p>由此可以看出，&lt;strong>性能、资源、可用性和可扩展性&lt;/strong>是分布式系统的重要指标。接下来，就详细来逐个来了解一下。&lt;/p>
&lt;h2 id="性能performance">性能（Performance）&lt;/h2>
&lt;p>谈起性能指标，主要是用于衡量一个系统处理各种任务的能力，无论是分布式系统还是单机系统，都会对性能有所要求。&lt;/p>
&lt;p>不同的系统、服务要达成的目的不同，所以各自对于性能的要求也就会有所区别，甚至是相互矛盾的。这里我们来看几个常见的性能指标，分别是吞吐量、响应时间和完成时间。&lt;/p>
&lt;p>&lt;strong>吞吐量&lt;/strong>指的是，单位时间内系统能够处理的请求或事务，能够表示系统的处理能力和效率。&lt;/p>
&lt;p>吞吐量的衡量可以根据具体应用场景而有所不同。例如，在网络通信中，吞吐量可以表示单位时间内传输的数据量；在数据库系统中，吞吐量可以表示每秒钟执行的查询数量；在并发用户访问网站时，吞吐量则可以表示每秒钟处理的请求数量。&lt;/p>
&lt;p>常见的吞吐量指标有 QPS（Queries Per Second）、TPS（Transaction Per Second）和 BPS（Bits Per Second）。&lt;/p>
&lt;ul>
&lt;li>QPS，即查询数每秒，用于衡量一个系统每秒处理的查询数，需要注意的是离开响应时间的要求是无法衡量 QPS 的。这个指标通用用于读操作，越高说明对读操作的支持越好。比如刚才我们所举的数据库的查询操作，就会用这个指标来表示。所以，我们在设计一个分布式系统的时候，如果主要都是偏向于读的操作，那么就需要重点考虑如何提高 QPS，来支持高频的读操作。&lt;/li>
&lt;li>TPS，即事务数每秒，用于衡量一个系统每秒处理的事务数。这个指标通常对应于写操作，越高说明对写操作支持越好。那么如果需要设计一个以写请求的分布式系统，对于 TPS 的支持是需要关心的，从而达到支持高频的写操作；&lt;/li>
&lt;li>BPS，即比特数每秒，用于衡量一个系统每秒处理的数据量。对于一些网络系统、数据管理系统，我们不能简单的按照请求数或事务数来衡量其性能。因为请求与请求、事务与事务之间也存在着很大的差异，比如说，有的事务因为要写入更多的数据，所以比较大。那么在这种情况下，BPS 更能客观地反映系统的吞吐量。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>响应时间&lt;/strong>指的是，系统响应一个请求或输入需要花费的时间。响应时间直接影响到用户体验，对于时延敏感的业务非常重要。比如我们出门时都会用到导航，如果响应时间过长，很容易带错路。&lt;/p>
&lt;p>&lt;strong>完成时间&lt;/strong>指的是，系统真正完成一个请求或处理需要花费的时间。任务并行模式出现的其中一个目的，就是缩短整个任务的完成时间。特别是需要计算海量数据或处理大规模任务时，系统对完成时间的感受非常明显。&lt;/p>
&lt;h2 id="资源占用resource-usage">资源占用（Resource Usage）&lt;/h2>
&lt;p>资源占用指的是，一个系统在正常运行时需要占用的硬件资源，比如 CPU、内存和硬盘等。&lt;/p>
&lt;p>一个系统在没有任何负载时的资源占用，叫作&lt;strong>空载资源占用&lt;/strong>，体现了这个系统自身的资源占用情况。比如，我们手机上在安装一款新的 APP 的时候，在软件的详情页面都会标注出软件的大小，比如多少 KB。这就是该 APP 的空载硬盘资源占用。对于同样的功能，空载资源占用越少，说明系统设计的越整洁，往往也会更容易被用户所接受。&lt;/p>
&lt;p>一个系统满额负载时的资源占用，叫作&lt;strong>满载资源占用&lt;/strong>，体现了这个系统全力运行时占用资源的情况，也体现了系统的处理能力。同样的硬件配置上，运行的业务越多，资源占用越少，说明这个系统设计的越好。&lt;/p>
&lt;h2 id="可用性availability">可用性（Availability）&lt;/h2>
&lt;p>可用性，通常来说是指系统在面对各种异常时可以正确提供服务的能力。可用性是分布式系统的一项重要指标，衡量了系统的 Robustness，是系统容错能力的体现。&lt;/p>
&lt;p>Robustness 也就是中文翻译的“鲁棒性”，我是不太喜欢这个翻译的，让人不知所以然。主要指的是系统在面对异常、错误或不符合预期输入时的稳健性和能力。&lt;/p>
&lt;p>我们经常会在分布式系统中看见&lt;strong>高可用&lt;/strong>这个词，也就是 7*24 不间断连续工作。那么系统的可用性可以用&lt;strong>系统停止服务的时间与总的时间之比衡量&lt;/strong>。假设一个网站总的运行时间是 24 小时，在 24 小时内，如果网站故障导致不可用的时间是 6 个小时，那么系统的可用性就是 6/24=0.25，也就是有四分之一的时间处于不可用阶段。&lt;/p>
&lt;p>除此之外，系统的可用性还可以用&lt;strong>某功能的失败次数与总的请求次数之比来衡量&lt;/strong>，比如给网站发送 1000 次请求，其中有 10 次请求失败，那么可用性就是 99%。&lt;/p>
&lt;p>提到了可用性，有的同学可能会疑惑，诶 这个和可靠性（Reliability）有何区别呢？&lt;/p>
&lt;p>&lt;strong>可靠性&lt;/strong>通常用来表示一个系统完全不出故障的概率，更多地用在硬件领域。而&lt;strong>可用性&lt;/strong>则更多的是指在允许部分组件失效的情况下，一个系统对外仍能正常提供服务的概率。&lt;/p>
&lt;p>Jeff Dean 曾在 Google I/O 大会上透露：谷歌一个基于 1000 台通用计算机的集群，一年之内就有 1000+ 硬盘会出现故障。由于现在比较常见的分布式系统基本上都是基于通用计算机的，这就意味着在这些系统中无法实现真正的可靠，所以我们也会在一些场合见到可靠性和可用性交换使用的情况。&lt;/p>
&lt;h2 id="可扩展性scalability">可扩展性（Scalability）&lt;/h2>
&lt;p>可扩展性，指的是分布式系统通过扩展集群机器规模提供系统性能（吞吐量、响应时间、完成时间）、存储容量、计算能力的特征，是分布式系统的特有性质。&lt;/p>
&lt;p>分布式系统的设计初衷，就是利用集群多机的能力处理单机无法解决的问题。然而，完成某一具体任务所需要的集群规模，取决于单个机器的性能和任务的要求。&lt;/p>
&lt;p>&lt;strong>当任务的需求随着具体业务不断提高时，除了升级系统的性能做垂直 / 纵向扩展外，另一个做法就是通过增加机器的方式去水平 / 横向扩展系统规模。&lt;/strong>&lt;/p>
&lt;p>这里垂直 / 纵向扩展指的是，增加单机的硬件能力，比如 CPU 增强、内存增大等；水平 / 横向扩展指的就是，增加计算机数量。好的分布式系统总是在追求“线性扩展性”，也就是说系统的某一指标可以随着集群中的机器数量呈线性增长。&lt;/p>
&lt;p>衡量系统可扩展性的常见指标是加速比（Speedup），也就是一个系统进行扩展后相对扩展前的性能提升。&lt;/p>
&lt;ul>
&lt;li>如果扩展的目标是为了提高系统吞吐量，则可以用扩展后和扩展前的系统吞吐量之比进行衡量。&lt;/li>
&lt;li>如果目标是为了缩短完成时间，则可以用扩展前和扩展后的完成时间之比进行衡量。&lt;/li>
&lt;/ul>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>这篇内容读起来可能比较无聊，更偏向于科普类的介绍了一些常用的专业术语。需要注意的是，使用这些指标衡量一个分布式系统不能教条化。&lt;/p>
&lt;p>按照不同维度，分布式系统的指标可以分为性能、资源占用、可用性、可扩展性这四大类。我们当然也希望自己开发或者使用的系统，是高性能、高可用、可扩展和低资源同时占用的，但考虑到硬件成本、开发效率等因素，对于不同的系统和业务，必须在设计时做出取舍。&lt;/p></content></entry><entry><title type="text">分布式 001——聊聊分布式的起源</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/distributedsystem/ds001_introduce/"/><id>https://quakewang.github.io/tech/distributedsystem/ds001_introduce/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-11-02T23:35:01+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="分布式该从何谈起">分布式该从何谈起？？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>现如今做开发，写代码出门和人聊天，动不动就会提起分布式，而且很多使用的框架、中间件都会采用分布式的设计。所以开阔自己的眼界，别做个 API Boy(Girl)。想了想开始试水写点相关的文章，一来梳理一下自己的学习心得，再者编写博客的过程中，也是对知识加强的一种途径。BTW 如果能帮助有些人就再好不过啦～～&lt;/p>……</summary><content type="html">&lt;h1 id="分布式该从何谈起">分布式该从何谈起？？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>现如今做开发，写代码出门和人聊天，动不动就会提起分布式，而且很多使用的框架、中间件都会采用分布式的设计。所以开阔自己的眼界，别做个 API Boy(Girl)。想了想开始试水写点相关的文章，一来梳理一下自己的学习心得，再者编写博客的过程中，也是对知识加强的一种途径。BTW 如果能帮助有些人就再好不过啦～～&lt;/p>
&lt;p>作为第一篇文章，先来看看分布式到底是什么。抛去那些抽象的的技术名词，该如何阐述这个概念呢？？&lt;/p>
&lt;p>&lt;em>注：为了更好地理解分布式的发展过程，默认每个计算机或服务器都是单核、单处理器的&lt;/em>。&lt;/p>
&lt;h2 id="分布式起源">分布式起源&lt;/h2>
&lt;h2 id="从第一台计算机开始聊聊">从第一台计算机开始聊聊&lt;/h2>
&lt;p>世界上第一台通用计算机是在 1946 年情人节发布的 ENIAC，由于当时硬件水平的限制，就如大家所说的那样，占地面积大，还显得特别笨重，但在那个时候已经做到了每秒可进行 5000 次加法或者 400 次乘法运算，标志着单机模式的开始。&lt;/p>
&lt;p>从这方面来看，电脑的出现就是为了计算，其实现如今也不例外，如果有时间可以简单来谈谈计算，试着从莱布尼茨开始说起 哈哈。回归正题，所谓&lt;strong>单机模式&lt;/strong>简单来说是指，所有应用程序和数据均部署在一台电脑或服务器上，由一台设备完成所有的处理。&lt;/p>
&lt;p>最近“双十一”，就以电商网站系统为例，删繁就简，只关注以下几个模块，分别是：用户管理、商品管理和订单管理。数据也就包括用户数据、商品数据和订单数据等。如果使用单机模式，那么所有的模块和数据均会部署在同一台计算机上，也就是说数据存储、请求处理均有同一个服务器来完成。这种模式的好处是功能、代码和数据更加集中，便于维护、管理和执行。&lt;/p>
&lt;p>单机模式的示意图，如下所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/9a950dba92cd8c0a964d02dc87d2dfb78b2c50f7/content/imag/tech/distributed/01_single.svg" alt="single">&lt;/p>
&lt;p>Every coin has two sides. 那么单机部署的缺点是什么呢？？单个计算机的处理能力取决于 CPU 和内存等，但硬件的发展速度和性能是有限的（可以看看摩尔定律），而且升级硬件的成本也比较高，可以说硬件的性能是单机模式的瓶颈。还有就是如果当前将所有的任务都交给一台服务器，可能会承受较大的负载压力，或者受到攻击挂了的话，所有的服务也就不能正常运行了，这也就将所有鸡蛋放到一个篮子里的风险，也就是单点失效问题。&lt;/p>
&lt;p>顺着这个思路往下走，既然单机模式存在性能的瓶颈，也就会导致会存在可用性的问题，有没有更好的解决办法呢？&lt;/p>
&lt;h2 id="数据并行">数据并行&lt;/h2>
&lt;p>为解决单机模式的问题，并行计算得到了发展，也就是我们常说的 MapReduce，并进一步出现了数据并行模式，也有的人喜欢称之为数据分布式模式。并行计算采用消息共享模式使用多台计算机并行运行或执行多项任务，核心原理是每台服务器上执行相同的程序，将数据进程拆分到不同的服务器上进行计算。&lt;/p>
&lt;p>需要注意的是，并行计算强调的是对数据进行拆分，任务程序在每台机器上运行。要达到这个目的，首先要做的是把单机模式中的应用和数据分离，才可能实现对数据的拆分。这里的应用就是执行任务的程序，任务就是提交的请求。还是来看上面的这个例子，运行在服务器上的用户管理、商品管理和订单管理等程序都是应用，用户提交的查询浏览商品、购买商品的请求就是任务。&lt;/p>
&lt;p>在单机模式中，应用和数据均在一台计算机或服务器上，要实现数据的并行，必须先将应用和数据分离以便将应用部署到不同的计算机或服务器上；然后，对同类型的数据进行拆分，比如说，不同计算机或服务器上的应用可以到不同的数据库上获取数据执行任务。&lt;/p>
&lt;p>这么讲可能比较抽象，来看看图：&lt;/p>
&lt;p>第一步，将应用与数据分离，分别部署到不同的服务器上。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/9a950dba92cd8c0a964d02dc87d2dfb78b2c50f7/content/imag/tech/distributed/01_app-data.svg" alt="应用与数据分离">&lt;/p>
&lt;p>第二步，对数据进行拆分，比如把同一类型的数据拆分到两个甚至更多的数据库中，这样应用服务器上的任务就可以针对不同数据并行执行了。&lt;/p>
&lt;p>对于电商销售系统，根据商品类型将用户、商品和订单数据拆分到不同的数据库中，部署到不同的服务器上，比如运动鞋服类的数据放在数据库服务器 1 上，电脑数据的数据放在数据库 2。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/9a950dba92cd8c0a964d02dc87d2dfb78b2c50f7/content/imag/tech/distributed/01_seperate-data.svg" alt="数据并行">&lt;/p>
&lt;p>这种模式的好处是，可以利用多台计算机并行处理多个请求，使得我们可以在相同的时间内完成更多的请求处理，解决了单机模式的计算效率瓶颈问题。但这种模式仍然存在如下几个问题，在实际应用中，我们还需要进行相对应的优化：&lt;/p>
&lt;ul>
&lt;li>相同的应用部署到不同的服务器上，当大量用户请求过来时，如何能比较均衡地转发到不同的应用服务器上呢？这也就是我们常说的“负载均衡”的问题，等后期有时间再展开聊聊；&lt;/li>
&lt;li>当请求量较大时，对数据库的频繁读写操作，会导致数据库的 IO 访问成为瓶颈。这种就可以使用“读写分离”的模式，也就是“主从库”的方式来解决，主数据库负责读写操作，然后再同步数据给从库，从而可以做到读数据库只接收读请求，写数据库只接收写请求，需要注意的是保证数据一致性；&lt;/li>
&lt;li>当有些数据成为热点数据时，会导致数据库访问频繁，压力增大。解决这个问题的方法是引入缓存机制，将热点数据加载到缓存中，一方面可以减轻数据库的压力，另一方面也可以提高查询效率。&lt;/li>
&lt;/ul>
&lt;p>由此我们可以看出，数据并行模式实现了多请求并行处理，但如果单个请求特别复杂，比如说需要几个小时甚至几天的时候，这种模式的整体计算效率还是不够高。其主要问题是：&lt;strong>对提升单个任务的执行性能及降低时延无效&lt;/strong>。&lt;/p>
&lt;h2 id="任务并行">任务并行&lt;/h2>
&lt;p>既然数据并行是存在一定缺陷的，我们就可以想是不是可以提高单个任务的执行性能，或者缩短单个任务的执行时间呢？也就出现了&lt;strong>任务并行&lt;/strong>的模式，有的人可能喜欢称之为“任务分布式”，都是一个概念。&lt;/p>
&lt;p>任务并行指的是，&lt;strong>将单个复杂的任务拆分为多个子任务，从而使得子任务可以在不同的计算机并行执行。&lt;/strong>&lt;/p>
&lt;p>我们仍以电商销售系统为例，任务并行首先是对应用进行拆分，比如按照领域模型将用户管理、商品管理、订单管理拆分成多个子系统分别运行在不同的计算机或服务器上。简单来说就是，原本包括用户管理、商品管理和订单管理的一个复杂任务，被拆分成多个子任务在不同的服务器上执行。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/9a950dba92cd8c0a964d02dc87d2dfb78b2c50f7/content/imag/tech/distributed/01_cluster.svg" alt="task- parallel">&lt;/p>
&lt;p>可以看出，任务并行模式完成一项复杂任务主要有两个核心步骤：首先将单任务拆分成多个子任务，然后让多个子任务并行执行。我们可以这样类比，任务拆分对应于部门领导，不同子系统对应不同的开发人员，不同子程序执行不同任务就像不同的程序员使用不同的技术栈一样，并且运行子系统或者子任务的计算机又可以组成一个服务。&lt;/p>
&lt;p>在这种模式中，由于多个子任务可以在多台服务器上运行，因此通过将同一任务待处理的数据分散到多个服务器上，在这些服务器上同事进行处理，就可以加快任务执行的速度。因为，只要一个复杂任务拆分出的任意子任务执行时间变短了，那么这个任务整体的执行时间也就相对而言变短咯。&lt;/p>
&lt;p>当然，任务分布式也存在一定的缺点，它在提供了更好的性能、扩展性、可维护性的同时，也带来了设计上的复杂性问题。毕竟对一个大型的复杂业务进行拆分并不是一件轻松的事情。从长远收益来看，这个短期设计上的阵痛是值得的。我们在平时也要注意多积累，有些技术可能仅仅是自己感兴趣，但不要因为短期内或公司的业务用不上就放弃不去学习，要有长远目标，慢慢多学一点点，回头来看会有不一样的感悟噢～～&lt;/p>
&lt;h2 id="分布式是什么">分布式是什么？？&lt;/h2>
&lt;p>看到这里，有些人就可能会觉得，啰里八嗦讲了一堆，诶 分布式到底是什么呢？&lt;/p>
&lt;p>用大白话说来说分布式就是，&lt;strong>将相同或相关的程序运行在多台服务器上，从而实现特定目标的一种计算方式。&lt;/strong>&lt;/p>
&lt;p>从这个角度来看，数据并行、任务并行其实都可以算作是分布式的一种形态。从这些计算方式的演变中不难看出，产生分布式的最主要原因是，数据量的暴增，如何更有效地追求高性能、可用性以及可扩展性。。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>这篇博客，简单聊了分布式的起源，从最初的单机模式到数据并行，再到任务并行。起初我是想从 GFS 和 MapReduce 的角度来阐述，但脱离业务，从数据出发可能会让不懂大数据的同学来说有点抽象，分布式文件系统和分布式计算引擎等后期有时间再拎出来单独唠唠（开始挖坑叻。。。）&lt;/p>
&lt;p>单机模式指的是，所有业务和数据均部署到同一台机器上。这种模式的好处是功能、代码和数据集中，便于维护、管理和执行，但计算效率是瓶颈。也就是说单机模式性能受限，也存在单点失效的问题。&lt;/p>
&lt;p>数据并行（也叫作数据分布式）模式指的是，对数据进行拆分，利用多台计算机并行执行多个相同任务，通过在相同的时间内完成多个相同任务，从而缩短所有任务的总体执行时间，但对提升单个任务的执行性能及降低时延无效。&lt;/p>
&lt;p>任务并行（也叫作任务分布式）模式指的是，单任务按照执行流程，拆分成多个子任务，多个子任务分别并行执行，只要一个复杂任务中的任意子任务的执行时间变短了，那么这个业务的整体执行时间也就变短了。该模式在提高性能、扩展性、可维护性等的同时，也带来了设计上的复杂性问题，比如复杂任务的拆分。&lt;/p>
&lt;p>在数据并行和任务并行这两个模式的使用上，用户通常会比较疑惑，到底是采用数据并行还是任务并行呢？一个简单的原则就是：任务执行时间短，数据规模大、类型相同且无依赖，则可采用数据并行；如果任务复杂、执行时间长，且任务可拆分为多个子任务，则考虑任务并行。在实际业务中，通常是这两种模式并用。&lt;/p></content></entry><entry><title type="text">MQ010——分布式消息队列（下）</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/bigdata/bigdata_mq010/"/><id>https://quakewang.github.io/tech/bigdata/bigdata_mq010/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-10-22T15:52:48+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="集群如何构建分布式的消息队列集群下">集群：如何构建分布式的消息队列集群（下）&lt;/h1>
&lt;p>我们接着上一讲的内容，继续来看如何构建集群。先来看元数据存储服务的设计选型。在消息队列的集群架构中，元数据存储服务的选型和实现是整个架构设计的核心，其他模块的设计都是围绕着元数据存储服务来展开的。&lt;/p>……</summary><content type="html">&lt;h1 id="集群如何构建分布式的消息队列集群下">集群：如何构建分布式的消息队列集群（下）&lt;/h1>
&lt;p>我们接着上一讲的内容，继续来看如何构建集群。先来看元数据存储服务的设计选型。在消息队列的集群架构中，元数据存储服务的选型和实现是整个架构设计的核心，其他模块的设计都是围绕着元数据存储服务来展开的。&lt;/p>
&lt;h2 id="元数据存储服务设计选型">元数据存储服务设计选型&lt;/h2>
&lt;p>如果博客一路看到这篇的话，想必对于下面的内容并不会感觉到陌生。我们知道业界主要有基于第三方存储引擎和集群内部自实现元数据存储两种方案。先来分析一下这两种方案的具体实现。&lt;/p>
&lt;h3 id="基于第三方存储引擎">基于第三方存储引擎&lt;/h3>
&lt;p>这个方案最重要的一件事就是&lt;strong>组件选型&lt;/strong>。&lt;/p>
&lt;p>从技术上来看，一般只要具备可靠存储能力的组件都可以当作第三方引擎。简单的可以是单机维度的内存、文件，或者单机维度的数据库、KV 存储，进一步可以是分布式的协调服务 Zookeeper、etcd 等等。&lt;/p>
&lt;p>正常来说，在设计的时候，结合自身的业务需求选择一中存储引擎就行。但是也有如 Pulsar 支持插件化的元数据存储服务，用来简化不同场景下的部署成本，比如单机运行、集成测试、线网部署等等。&lt;/p>
&lt;p>从分布式的角度来看，单机维度的存储能满足的场景有限，也会有单机风险。所以处于实际生产需求考虑，一般都会选用分布式的协调服务，比如使用 Zookeeper、etcd 等来当集群的元数据存储服务。所以基于第三方存储引擎的集群架构图一般如下所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/256395b0e23405ae7dc8d1bd70f6712d31341ed1/content/imag/tech/bigdata/mq/10_cluster_third.svg" alt="third-engine">&lt;/p>
&lt;p>在这个架构图中，如果把元数据服务替换成 Zookeeper，也就成了 Kafka 和 Pulsar 的架构了。&lt;/p>
&lt;p>如图所示，这是一个有单独的元数据存储集群和多台 Broker 节点组成的消息队列集群。Broker 连接上 Metadata Service 完成节点发现、探活、主节点选举等功能。&lt;strong>其中 Controller 的角色是由某一台 Broker 兼任的&lt;/strong>。&lt;/p>
&lt;p>细心的小朋友可能已经发现，图中 Controller 和 Metadata Service 是分开的，各自都承担着不同的职责。Controller 是无状态的，因为它不负责保存数据，只负责计算逻辑。所以在这种情况下，一般就会让集群中的某台 Broker 来承担 Controller 的功能。当这台 Broker 挂了后，可以依赖元数据存储服务把 Controller 切换到新的 Broker。因为它是无状态的，所以切换是非常快的。&lt;/p>
&lt;p>但使用这种方案，集群中最少得有 6 个节点，这会导致部署成本、运维复杂度变高。那有没有可能简化架构呢？？我们继续啊来看集群内部实现元数据存储的方案。&lt;/p>
&lt;h3 id="集群内部自实现元数据存储">集群内部自实现元数据存储&lt;/h3>
&lt;p>简单来说，可以通过在多台 Broker 的进程中实现分布式的元数据存储，从而解决依赖第三方组件的一些弊端。整体架构如下图所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/256395b0e23405ae7dc8d1bd70f6712d31341ed1/content/imag/tech/bigdata/mq/10_cluster_self.svg" alt="inner-metadata">&lt;/p>
&lt;p>从技术实现上来看，主要有三种思路：&lt;/p>
&lt;ul>
&lt;li>直接在 Broker 内部构建一个小型的元数据存储集群来提供服务；&lt;/li>
&lt;li>通过某些可以内嵌到进程的小型的分布式存储服务来完成元数据的存储；&lt;/li>
&lt;li>通过某些可以内置的单机持久化的服务，配合节点间的元数据同步机制来完成元数据的存储。&lt;/li>
&lt;/ul>
&lt;p>第一种方案需要在 Broker 中实现一个元数据集群。这个元数据集群和 Broker 集群最大的差别在于它只需要承担单个集群的元数据管理存储，数据量和规模很小，集群一般不需要扩容。所以这个集群适合使用“通过配置发现节点的方案”来构建集群。Kafka 的 KRaft 架构用的就是这种方案。&lt;/p>
&lt;p>第二种方案是利用某种可以内嵌到进程的存储服务来存储元数据，比如 Mnesia 或 RocksDB。如果是单机的存储引擎，比如 RocksDB，那么主要适用于单机部署的场景。单机存储引擎的方案如果要实现元数据的集群化，那么就得在节点之间实现相互同步数据的机制，这个就相对复杂许多。而如果是分布式的存储引擎，如 Mnesia，那么就简单许多，几乎没有工作量，直接调用存储引擎的接口存储元数据即可。&lt;/p>
&lt;p>第三种方案是在节点上部署一个持久化的单机存储引擎，如 RocksDB 等。然后在 Broker 内维护节点间的元数据数据的一致性。这种方式也是一种实现比较简单的方案，开发难度低于第一种方案，高于第二种方案。&lt;/p>
&lt;p>从业界实现来看，目前第一种和第二种方案都有在使用。第三种方案主要用在单机模式下，问题是要维护多个节点的存储服务之间的数据一致性，有一定的开发工作量，并且保持数据强一致比较难。&lt;/p>
&lt;p>总结来看，在集群中实现元数据服务的优点是，后期架构会很简洁，不需要依赖第三方组件。缺点是需要自研实现，投入研发成本比较高。而如果使用独立的元数据服务，产品成型就会很快。这也是当前主流消息队列都是依赖第三方组件来实现元数据存储的原因。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/256395b0e23405ae7dc8d1bd70f6712d31341ed1/content/imag/tech/bigdata/mq/10_cluster_dependency.svg" alt="framework">&lt;/p>
&lt;p>接下来，我们就用实际案例结合前面这些基础知识点，来看一下 Zookeeper、Kafka 是如何构建集群的。&lt;/p>
&lt;h2 id="zookeeper-的集群构建">Zookeeper 的集群构建&lt;/h2>
&lt;p>Zookeeper 是一个分布式的数据协调服务，本质上是一个简单的、分布式、可靠的数据存储服务。核心操作就是数据的写入、分发、读取和 Hook。从客户端看，主要操作就是写入喝读取。从服务端看，主要操作就是集群构建、数据接收、存储、分发和 Hook。&lt;/p>
&lt;p>在集群构建上，它会事先在配置中定义好集群中所有节点的 IP 列表。然后集群启动时，会在这些几点之间进行选举，经过多数投票机制，选举出一个 Leader 节点，从而构建成为集群。在单节点上，集群构建相关的配置一般如下所示，配置中会包含所有节点信息。&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-conf" data-lang="conf">server.0=hadoop102:2888:3888
server.1=hadoop103:2888:3888
server.2=hadoop104:2888:3888
&lt;/code>&lt;/pre>&lt;p>在节点启动的时候，节点之间就会两两进行通信，触发投票。然后根据票数的多少，基于多数原则，选举出一个 Leader 出来。当 Leader 节点发生宕机或者增加节点时，就出重新触发选举。&lt;/p>
&lt;p>多数投票是一个经常用到的投票机制，即某个节点获得票数超过可投票的节点的一半后，就可以当选为 Leader。&lt;strong>从实现角度，一般是通过集群中节点之间的通信和间隔随机投票的机制来完成投票，以保证能够在短时间内完成选举&lt;/strong>。&lt;/p>
&lt;p>当选举完成后，Leader 会主动给各个 Follower 节点发送 ping-pong 请求，以确定节点是否还活着。当 Follower 心跳异常时，就会剔除该节点，当集群中可用的节点数少于总节点数的一半，就会选举不出 Leader，从而导致集群异常。&lt;/p>
&lt;p>因为 ZooKeeper 只是一个数据存储服务，并没有很多管控操作，Leader 节点就负责数据的写入和分发，Follower 不负责写入，只负责数据的读取。当 Leader 收到操作请求时，比如创建节点、删除节点、修改内容、修改权限等等，会保存数据并分发到多个 Follower，当集群中有一半的 Follower 返回成功后，数据就保存成功了。当 Follower 收到写入请求时，就把写入请求转发给 Leader 节点进行处理。&lt;/p>
&lt;p>因为功能和定位上的差异，ZooKeeper 上是没有 Controller 和元数据存储的概念的。它是比较典型的基于固定配置构建集群的方式。&lt;/p>
&lt;h2 id="kafka-的集群构建">Kafka 的集群构建&lt;/h2>
&lt;p>之前我们有说过，目前主流消息队列的集群主要是基于第三方组件来构建的。而 Kafka 正是这种方案的典型实现，接下来我们就看一下 Kafka 基于 Zookeeper 和基于 KRaft 构建集群的两种实现方式。&lt;/p>
&lt;h3 id="基于-zookeeper-的集群">基于 Zookeeper 的集群&lt;/h3>
&lt;p>在这种架构中，Kafka 将 Zookeeper 作为节点发现和元数据存储的组件，通过在 Zookeeper 上创建临时节点来完成节点发现，并在不同的节点上保持各种元数据信息。&lt;/p>
&lt;p>Broker 在启动或重连时，会根据配置中的 Zookeeper 地址找到集群对应的 Zookeeper 集群。然后会在 ZooKeeper 的 /broker/ids 目录中创建名称为自身 BrokerID 的临时节点，同时在节点中保存自身的 Broker IP 和 ID 等信息。当 Broker 宕机或异常时，TCP 连接就会断开或者超时，此时临时节点就会被删除。&lt;/p>
&lt;p>注册完这些信息后，节点发现就算完成了。节点之间的探活依赖 ZooKeeper 内置的探活机制，前面讲过，这里不再赘述。接下来来看一下 Kafka 中的 Controller。&lt;/p>
&lt;p>在 Kafka 中，Controller 是一个虚拟概念，是运行在某台 Broker 上的一段代码逻辑。集群中需要确认一台 Broker 承担 Controller 的角色，那 Controller 是怎么选出来的呢？我们来看一看。&lt;/p>
&lt;p>Kafka 的 Controller 选举机制非常简单，即在 ZooKeeper 上固定有一个节点 /controller。每台 Broker 启动的时候都会去 ZooKeeper 判断一下这个节点是否存在。如果存在就认为已经有 Controller 了，如果没有，就把自己的信息注册上去，自己来当 Controller。集群每个 Broker 都会监听 /Controller 节点，当监听到节点不存在时，都会主动去尝试创建节点，注册自己的信息。&lt;strong>哪台节点注册成功，这个节点就是新的 Controller。&lt;/strong>&lt;/p>
&lt;p>Controller 会监听 ZooKeeper 上多个不同目录，主要监听目录中子节点的增加、删除，节点内容变更等行为。比如会通过监听 /brokers/ids 中子节点的增删，来感知集群中 Broker 的变化。即当 Broker 增加或删除时，ZooKeeper 目录中就会创建或删除对应的节点。此时 Controller 通过 Hook 机制就会监听到节点发生了变化，就可以拿到变化节点的信息，根据这些信息，触发后续的业务逻辑流程。&lt;/p>
&lt;p>Kafka 集群中每台 Broker 中都有集群全量的元数据信息，每台节点的元数据信息大部分是通过 Controller 来维护的，比如 Topic、分区、副本。当这些信息发生变化时，Controller 就会监听到变化。然后根据不同的 Hook（如创建、删除 Topic 等），将这些元数据通过 TCP 调用的形式通知给集群中其他的节点，以保持集群中所有节点元数据信息是最新的。&lt;/p>
&lt;p>可以看下图，我们用 Topic 的创建流程，来串联集群中的 Controller 的集群管理和元数据存储。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/256395b0e23405ae7dc8d1bd70f6712d31341ed1/content/imag/tech/bigdata/mq/10_cluster_topic.svg" alt="kafka-topic">&lt;/p>
&lt;p>如图所示，Kafka 创建 Topic 有两种形式（图中 1 和 2），即通过 Broker 来创建和通过 ZooKeeper 来创建。当调用 Broker 创建 Topic 时，Broker 会根据本地的全量元数据信息，算出新 Topic 的分区、副本分布，然后将这些数据写入到 ZooKeeper。然后 Controller 就会 Hook 到创建 Topic 的行为，更新本地缓存元数据信息，通知对应的 Broker 节点创建分区和副本。 所以，也可以通过直接生成计划然后、写入到 ZooKeeper 的方式来创建 Topic。&lt;/p>
&lt;h3 id="基于-kraft-的集群">基于 KRaft 的集群&lt;/h3>
&lt;p>从架构的角度，基于 KRaft 实现的 Kafka 集群做的事情就是将集群的元数据存储服务从 Zookeeper 替换称为内部实现的 Metadata 模块。这个模块会同时完成 Controller 和元数据存储的工作。&lt;/p>
&lt;p>我们前面讲过，&lt;strong>集群元数据需要分布式存储才能保证数据的高可靠&lt;/strong>。所以 Kafka KRaft 架构的 Metadata 模块是基于 Raft 协议实现的 KRaft，从而实现元数据可靠存储的。&lt;/p>
&lt;p>因为 Kafka 的 Metadata 模块只需要完成元数据存储，所以它的设计思路和 ZooKeeper 是一样的，是主从架构。即通过在配置文件中配置节点列表，然后通过投票来选举出 Leader 节点。这个节点会承担集群管控、元数据存储和分发等功能。&lt;/p>
&lt;p>Metadata 模块的配置如下所示。即通过配置项 controoler.quorum.votes 配置允许成为 Controller 的节点列表，然后这些节点之间会通过投票选举出 Leader 节点，这个 Leader 会完成 Controller 和元数据存储的工作。这个 Leader 相当于基于 ZooKeeper 版本中的 Controller 和 ZooKeeper 的 Leader。&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-conf" data-lang="conf">process.roles=broker,controller
controller.quorum.voters=1@localhost:9093
&lt;/code>&lt;/pre>&lt;p>所以在这个版本架构的实现中，就只有 Controller 了，然后 Controller 自带了元数据存储的功能。Broker 之间通过投票选举出来的 Leader 节点就是 Controller。此时，所有 Broker 都会和 Controller 保持通信，以维护节点的在线状态，从而完成节点发现。当 Controller 发现 Broker 增加或异常时，就会主动执行后续的操作。&lt;/p>
&lt;p>所以，从链路来看，这个架构简化了通过监听 ZooKeeper 来发现节点变更的流程，链路更短，稳定性更高。和基于 ZooKeeper 的架构一样，每台 Broker 依旧有集群全量的元数据信息，这些元数据信息的维护也是通过 Controller 完成的。&lt;/p>
&lt;p>接下来，我们来看一下 KRaft 架构下创建 Topic 的流程，来看下图：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/256395b0e23405ae7dc8d1bd70f6712d31341ed1/content/imag/tech/bigdata/mq/10_cluster_kraft.svg" alt="kraft-topic">&lt;/p>
&lt;p>这里因为没有 ZooKeeper，所以创建 Topic 只有通过 Broker 创建的方式。通过 Admin SDK 调用 Broker 创建 Topic，如果 Broker 不是 Controller，这个请求就会转发到当前的 Controller 上。Controller 会根据本地的元数据信息生成新 Topic 的分区、副本的分布，然后调用对应的 Broker 节点完成分区和副本数据的创建，最后会保存元数据。&lt;/p>
&lt;p>其他的操作，比如删除 Topic、修改配置、创建 ACL 等流程是一样的。更多细节如果感兴趣的话，可以去看一下官方的 &lt;a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-500%3A+Replace+ZooKeeper+with+a+Self-Managed+Metadata+Quorum">KIP&lt;/a>。&lt;/p>
&lt;p>讲到这里，你会发现基于 KRaft 的 Kafka 架构比基于 Zookeeper 架构简单清晰非常多，操作链路也短很多。这样可以解决基于 Zookeeper 架构中一些难以解决的问题，如集群可承载分区数量上限较低，缓存不一致等等。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>目前，消息队列的主流实现方式都是依赖第三方组件来完成数据存储，常见的有 ZooKeeper、etcd 等。为了简化架构，我们还可以通过在集群内自建元数据存储服务来替代第三方组件，虽然需要研发投入，但从架构长期演进的合理性来看，我是推荐这种方式的，毕竟后期架构会很简洁。&lt;/p>
&lt;p>ZooKeeper 集群的组件，是基于配置文件中指定集群中其他节点的 IP 地址和端口来实现节点发现的，属于单播发现机制。这种方式的缺点就是扩容需要修改配置、重启集群。所以，还有一种通过配置多播地址和端口来实现集群发现的方式，其好处是可以动态发现节点，属于单播的一种升级，目前 Elasticsearch 和消息队列 RabbitMQ 都属于多播的实现。从 Kafka 的集群构建来看，基于独立的元数据存储服务，会导致架构复杂和引入缓存不一致等问题。集群内部实现元数据存储，可以简化架构，避免不一致。从技术合理性来看，或许尝试内置元数据存储是个不错的方案。&lt;/p></content></entry><entry><title type="text">MQ009——分布式消息队列（上）</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/bigdata/bigdata_mq009/"/><id>https://quakewang.github.io/tech/bigdata/bigdata_mq009/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-10-12T22:28:03+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="集群如何构建分布式的消息队列集群上">集群：如何构建分布式的消息队列集群？（上）&lt;/h1>
&lt;h2 id="有状态服务和无状态服务">有状态服务和无状态服务&lt;/h2>
&lt;p>在正式讲解如何构建一个分布式的消息队列集群之前，我们可以先来了解一下什么是有状态服务，以及什么是无状态服务。&lt;/p>……</summary><content type="html">&lt;h1 id="集群如何构建分布式的消息队列集群上">集群：如何构建分布式的消息队列集群？（上）&lt;/h1>
&lt;h2 id="有状态服务和无状态服务">有状态服务和无状态服务&lt;/h2>
&lt;p>在正式讲解如何构建一个分布式的消息队列集群之前，我们可以先来了解一下什么是有状态服务，以及什么是无状态服务。&lt;/p>
&lt;p>这两个词在我们日常开发中也是经常遇到的，这二者之间最重要的一个区别在于：&lt;strong>是否需要在本地存储持久化数据&lt;/strong>。简单来说就是，需要在本地存储持久化数据的就是有状态服务，反之就是无状态服务。&lt;/p>
&lt;p>说这两个，主要是因为有状态服务和无状态服务构建集群的思路完全是不一样的。HTTP Web Server 就是典型的无状态服务。在搭建 HTTP Web 集群的时候，我们经常会使用 Nginx 或者其他网关后面挂一批 HTTP 节点，此时后端的这批 HTTP 服务节点就是一套集群。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/d5544b2793d115647763576c36da456165bf41c6/content/imag/tech/bigdata/mq/09_cluster_nginx.svg" alt="nginx">&lt;/p>
&lt;p>如上图所示，因为 HTTP Web 是无状态的服务，不同的节点不需要知道其他节点的存在。Nginx 认为后端所有的节点的功能是一样的，所以请求经过 Nginx 后，只需要根据一定转发策略，如轮询、加权轮询、按 Key Hash 等将请求转发给后端的 Web 服务节点即可。然后在节点增减的时候，Nginx 会感知到节点的增减。执行转发或者不转发就可以咯。&lt;/p>
&lt;p>至于消息队列通常来说都是有状态服务。消息是和分片绑定，分片是和节点绑定。所以，当需要发送一个消息后，就需要发送到固定的节点，如果把消息发送到错误的节点，就会失败。所以，为了将消息发送到对的节点和从对的节点削峰数据，消息队列在消息的收发上，就有服务端转发和客户端寻址两种方案。&lt;/p>
&lt;p>所以，消息队列集群应该是按照有状态来设计的。接下来，我们就看看如何设计出一个集群化的消息队列服务。&lt;/p>
&lt;h2 id="消息队列的集群设计思路">消息队列的集群设计思路&lt;/h2>
&lt;p>当前业界主流的分布式集群，一般都是基于主从（Master/Slave）思想来设计的。即通过一个组件来管理整个集群的相关工作，比如创建和删除 topic、节点上下线等等。这个组件一般叫做 Master 或 Controller。&lt;/p>
&lt;p>然后还需要有一个组件来完成集群元数据（比如节点信息、Topic 信息等等）的存储，这个组件一般叫做元数据服务。当然还有一批数据流节点来完成数据的读写和存储工作，这个组件一般叫做 Broker。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/d5544b2793d115647763576c36da456165bf41c6/content/imag/tech/bigdata/mq/09_cluster_broker.svg" alt="broker">&lt;/p>
&lt;h3 id="元数据存储">元数据存储&lt;/h3>
&lt;p>我们先来看一下集群中的元数据是如何存储的。&lt;/p>
&lt;p>消息队列集群元数据是指集群中 Topic、分区、配置、节点和权限等信息。元数据必须保证可靠、高校的存储，不允许丢失。因为一旦元数据丢失，其实际的消息数据也会变得没有意义。&lt;/p>
&lt;p>从技术上看，业界主要有第三方存储引擎和集群内部自实现存储两种方案。&lt;/p>
&lt;p>&lt;strong>依赖第三方存储引擎&lt;/strong>是指直接使用第三方组件来完成元数据信息的存储，比如 Zookeeper、etcd、单机或分布式数据库等等。这种方案的优点是拿来即用，无需额外的开发成本，产品成型快，稳定性较高。缺点是需要依赖第三方组件，会增加额外的部署维护成本，并且受限于第三方组件的瓶颈和稳定性，也可能会有数据一致性问题。像 Kafka、Pulsar 基于 Zookeeper 都是用的这种方式。&lt;/p>
&lt;p>&lt;strong>集群内部自实现存储&lt;/strong>是指在消息队列应用内部自定义实现元数据存储服务，相当于在消息队列集群中实现一个小型的 Zookeeper。这种方案的优点是集群内部集成了这部分能力，部署架构就很简单轻量，应用自我把控性高，不会有第三方以来问题。缺点是开发成本较高，从头开始自研，相对于成熟组件而言，稳定性上短期内会比较弱，需要投入时间打磨。Kafka 去 Zookeeper 后的 KRaft 架构中的元数据存储，就是基于这个思路实现的。&lt;/p>
&lt;h3 id="节点发现">节点发现&lt;/h3>
&lt;p>接下来，我们一起看看看如何完成节点发现。我们知道集群是由多个节点组成的，此时组成集群的最基本要求就是：所有节点知道对方的存在或者有一个组件知道所有节点的存在，这样才能完成后续的集群管理和调度。这个过程就是节点发现的过程。&lt;/p>
&lt;p>从技术上看，当前业界主要有配置文件、类广播机制、集中式组件三种手段来完成节点发现。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>配置文件&lt;/strong>：通过指定文件配置所有节点 IP，然后节点启动后根据配置文件去找到所有的节点，从而完成节点发现。&lt;/li>
&lt;li>&lt;strong>类广播机制&lt;/strong>：通过广播、DNS 解析等机制，自动去发现集群中所有节点。比如通过解析 DNS 域名，得到域名绑定的所有 IP，从而发现集群中所有节点。&lt;/li>
&lt;li>&lt;strong>集中式组件&lt;/strong>：所有节点都向集中式组件去注册和删除自身的节点信息，此时这个组件就会包含所有节点的信息，从而完成节点发现。&lt;/li>
&lt;/ul>
&lt;p>第一种方案的好处是实现简单，在节点发现这块几乎不需要额外的开发成本，缺点就是集群扩容需要修改配置文件，水平扩容不方便，需要重启。比如 Zookeeper 和 KRaft 就是用的这种方案。&lt;/p>
&lt;p>第二种方案好处是可以自动发现新节点，自动扩容集群。缺点是开发成本很高，需要通过广播或者类似的机制发现集群中的其他节点。&lt;/p>
&lt;p>第三种的好处是可以动态地感知节点的变更，水平扩容非常方便，实现也简单。所以当前主流消息队列都是用的这种方案。Kafka 基于 Zookeeper 的版本，RocketMQ 和 Pulsar 都是用的这种方案。&lt;/p>
&lt;p>完成节点后，接下来就需要能够感知节点的变更，以便在节点故障时及时将其踢出集群。而这种动作就得依靠节点探活来实现。&lt;/p>
&lt;h3 id="节点探活">节点探活&lt;/h3>
&lt;p>从实现角度来看，一般需要有一个角色来对集群内所有节点进行探活或者保活，这个角色一般是主节点或第三方组件。&lt;/p>
&lt;p>如下图所示，技术上一般分为主动和定时上探测两种，这两种方式的主要区别在于心跳探活发起方的不同。从技术和实现上看，差别都不大；从稳定性来看，一般推荐主动上报。因为由中心组件主动发起探测，当节点较多时，中心组件可能会有性能瓶颈，所以目前业界主要的探活实现方式也是主动上报。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/d5544b2793d115647763576c36da456165bf41c6/content/imag/tech/bigdata/mq/09_cluster_ping.svg" alt="ping-pong">&lt;/p>
&lt;p>从探测策略上看，基础都是基于 ping-pong 的方式来完成探活。心跳发起一般会根据一定的时间间隔发起心跳探测。如果保活组件一段时间没有接收到心跳或者主动心跳探测失败，就会提出这个节点。比如每 3 秒探测一次，连续 3 次探测失败就剔除节点。探测行为一般会设置较短的超时时间，以便尽快完成探测。&lt;/p>
&lt;p>以 Kafka 为例，它是基于 Zookeeper 提供的临时节点和 Hook 机制来实现节点保活的。即节点加入集群时会创建 TCP 长连接并创建临时节点，当 TCP 连接断开时就会删除临时节点。临时节点的变更会触发后续的相关操作，比如将节点加入集群、将节点剔除集群等等。&lt;/p>
&lt;p>所以基于 Zookeeper 实现节点发现和保活就很简单，只要通过 SDK 创建临时节点即可，只要 TCP 连接存活，临时节点就会存在。那么怎样确认连接存活呢？底层还是通过 ping-pong 机制、客户端主动上报心跳的形式实现的。&lt;/p>
&lt;p>因为 Zookeeper 具备这两个机制且组件相对成熟、稳定性较高，所以很多消息列队都会用 Zookeeper 来实现节点发现和探活。完成节点探活后，接下来我们看看集群的主节点是怎么选举出来的。&lt;/p>
&lt;h3 id="主节点选举">主节点选举&lt;/h3>
&lt;p>从技术上看，理论上只要完成了节点探活，即节点健康的情况下，这批节点就都是能被选为主节点的。当然，有的集群可以配置哪些节点可以被选举为主节点，哪些节点不能被选举主节点，但是这点不影响后续的选举流程。&lt;/p>
&lt;p>主节点的选择一般有相互选举和依赖第三组件争抢注册两种方式。&lt;/p>
&lt;p>&lt;strong>相互选举&lt;/strong>是指所有节点之间相互投票，选出一个得票最多的节点成为 Leader。投票的具体实现可以参考 Raft 算法，这里就不展开。目前业界 Zookeeper、ElasticSearch、Kafka KRaft 版本等都是用的这种方案。&lt;/p>
&lt;p>&lt;strong>依赖第三方组件争抢注册&lt;/strong>是通过引入一个集中式组件来辅助完成节点选举。比如可以在 Zookeeper、etcd 上的某个位置写入数据，哪个节点先写入成功它就是 Leader 节点。当节点异常时，会触发其他节点争抢写入数据。依此类推，从而完成主节点的选举。&lt;/p>
&lt;p>在消息队列中，这个主节点一般称为 Controller，Controller 主要是用来完成集群管理相关的工作，集群的管理操作一般指创建和删除 Topic、配置变更等等行为。&lt;/p>
&lt;p>所以抽象来看，一般情况下消息队列的集群结构如下所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/d5544b2793d115647763576c36da456165bf41c6/content/imag/tech/bigdata/mq/09_cluster_vote.svg" alt="vote">&lt;/p>
&lt;p>其中，Metadata Service 负责元数据的存储，Controller 负责读取、管理元数据信息，并通过集群中的 Broker 执行各种操作。此时从实际架构实现的角度来看，Broker 的元数据上报可以走路径 1，通过 Controller 上报元数据到 Metadata Service，也可以直连 Metadata Service 走路径 2 上报元数据。两条路径没有明显的优劣，一般根据实际的架构实现时的选型做考虑。&lt;/p>
&lt;p>当完成元数据存储、节点发现、节点探活、主节点选举后，消息队列的集群就创建完成了。接下来我们通过集群启动、创建 Topic、Leader 切换三个动作来分析一下集群的运行机制。先来看一下集群启动的流程。&lt;/p>
&lt;h2 id="消息队列的集群构建流程">消息队列的集群构建流程&lt;/h2>
&lt;h3 id="集群启动">集群启动&lt;/h3>
&lt;p>集群启动其实就是节点启动的过程，可以看下图：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/d5544b2793d115647763576c36da456165bf41c6/content/imag/tech/bigdata/mq/09_cluster_start.svg" alt="start">&lt;/p>
&lt;p>节点启动大致分为以下四步：&lt;/p>
&lt;ol>
&lt;li>节点启动时在某个组件（如图中的 Controller 或 Metadata Service）上注册节点数据，该组件会保存该节点的元数据信息；&lt;/li>
&lt;li>节点注册完成后，会触发选举流程选举出一个主节点（Controller）；&lt;/li>
&lt;li>节点会定期向主节点（或 Metadata Service）上报心跳用来确保异常节点能快速被剔除；&lt;/li>
&lt;li>当节点异常下线或有新节点上线时，同步更新集群中的元数据信息。&lt;/li>
&lt;/ol>
&lt;p>从运行的角度看，完成这一步，集群就算已经构建完成了。接下来我们看看如何创建 Topic。&lt;/p>
&lt;h3 id="创建-topic">创建 Topic&lt;/h3>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/d5544b2793d115647763576c36da456165bf41c6/content/imag/tech/bigdata/mq/09_cluster_topic.svg" alt="topic">&lt;/p>
&lt;p>创建 Topic 大致分为以下四步：&lt;/p>
&lt;ol>
&lt;li>客户端指定分区和副本数量，调用 Controller 创建 Topic；&lt;/li>
&lt;li>Controller 根据当前集群中的节点、节点上的 Topic 和分区等元数据信息，再根据一定的规则，计算出新的 Topic 的分区、副本的分区，同事选出分区的 Leader（主分片）；&lt;/li>
&lt;li>Controller 调用 Metadata Service 保存元数据信息；&lt;/li>
&lt;li>Controller 调用各个 Broker 节点创建 Topic、分区、副本。&lt;/li>
&lt;/ol>
&lt;p>再来看看删除 Topic 和扩容分区是如何执行的。&lt;/p>
&lt;p>如果要删除 Topic，首先依旧要先往 Controller 发送一个删除 Topic 的指令；然后 Controller 会通知 Topic 分区所在的节点，删除分区和副本数据，删除 Topic；最后再删除 Metadata Service 中的 Topic 元数据/扩容分区的操作也是类似的，Controller 接收到扩容分区的指令，根据逻辑计算出新分区所在的节点，然后通知对应的节点创建分区，同时保存相关元数据。&lt;/p>
&lt;h3 id="leader-切换">Leader 切换&lt;/h3>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/d5544b2793d115647763576c36da456165bf41c6/content/imag/tech/bigdata/mq/09_cluster_leader.svg" alt="leader">&lt;/p>
&lt;p>Leader 切换的流程可以分为以下四步：&lt;/p>
&lt;ol>
&lt;li>Controller 会持续监听节点的存活状态，持续监控 Broker 节点是否可用；&lt;/li>
&lt;li>根据一定的机制，判断节点挂掉后，开始触发执行 Leader 切换操作；&lt;/li>
&lt;li>Controller 通过 RPC 调用通知存活的 Broker2 和 Broker3，将对应分区的 Follower 提升为 Leader；&lt;/li>
&lt;li>变更保存所有元数据。&lt;/li>
&lt;/ol>
&lt;p>从客户端的视角来看，服务端是没有机制通知客户端 Leader 发生切换的。此时需要依靠客户端主动更新元数据来感知已经发生 Leader 切换。客户端一般会在接收到某些错误或者定期更新元数据来感知到 Leader 的切换。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>集群构建的思路分为有状态服务和无状态服务，两种类型服务的构建思路是不一样的。有状态服务需要解决元数据存储、节点发现、节点探活、主节点选举等四部分。&lt;/p>
&lt;p>元数据存储主要有依赖第三方组件实现和集群内自定义实现元数据存储两个思路。第三方组件主要有 ZooKeeper、etcd 等，依赖第三方组件是当前主流的选择，因为其实现较为简单，前期稳定性较高。自定义实现元数据存储是指在消息队列 Broker 集群内实现元数据存储服务，从而简化架构，实现虽较为复杂，但长期来看相对更合理。&lt;/p>
&lt;p>节点发现主要有静态发现和动态发现两个思路。静态发现是指通过配置文件配置好集群的所有节点，各个节点之间通过配置内容来发现对方，从而组建成一个集群。动态发现是指依赖一个中心组件或者类广播机制来动态完成节点之间的相互发现，即当节点上线或下线的时候，及时感知到变化，从而将节点加入到集群或者从集群中剔除。&lt;/p>
&lt;p>节点探活主要分为主动上报和定时探测两种，业界主要使用主动上报的实现形式。&lt;/p>
&lt;p>主节点在消息队列中一般叫做 Controller，一般通过节点间选举或者依赖第三方组件争抢注册来完成选举。Controller 主要用来完成集群内的管理类操作，如节点上下线、Topic 创建 / 删除 / 修改、Leader 切换等等。Controller 由集群中的某个 Broker 担任。&lt;/p></content></entry><entry><title type="text">MQ008——剖析 Kafka 架构设计</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/bigdata/bigdata_mq008/"/><id>https://quakewang.github.io/tech/bigdata/bigdata_mq008/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-09-27T20:06:23+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="kafka-系统架构">Kafka 系统架构&lt;/h1>
&lt;p>首先来看一下 Kafka 的架构图：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/47efebf445373794f1e81afb87ad1af3a96a03bd/content/imag/tech/bigdata/mq/08_kafka_framework.svg" alt="kafka-framework">&lt;/p>……</summary><content type="html">&lt;h1 id="kafka-系统架构">Kafka 系统架构&lt;/h1>
&lt;p>首先来看一下 Kafka 的架构图：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/47efebf445373794f1e81afb87ad1af3a96a03bd/content/imag/tech/bigdata/mq/08_kafka_framework.svg" alt="kafka-framework">&lt;/p>
&lt;p>如上图所示，Kafka 由 Producer、Broker、Zookeeper 和 Consumer 四个模块组成。其中，Zookeeper 用来存储元数据信息，集群中所有元数据都持久化存储在 Zookeeper 中。之前的内容，我们有讲过，使用 Zookeeper 作为元数据存储服务会带来额外的维护成本、数据一致性和集群规模限制（主要是分区数）等问题。所以 Kafka3.0 使用内置的 Raft 机制替代 Zookeeper。&lt;/p>
&lt;p>Kafka 有 Topic 和分区的概念，一个 Topic 可以包含一个或多个分区。消费方面，通过 Group 来组织消费者和分区的关系。&lt;/p>
&lt;p>从消息的生命周期来看，生产者也需要通过客户端寻址拿到元数据信息。客户端通过生产分区分配机制，选择消息发送到哪个分区，然后根据元数据信息拿到分区 Leader 所在的节点，最后将数据发送到 Broker。Broker 收到消息并持久化存储。消费端使用消费分组或直连分区的机制去消费数据。如果使用消费分组，就会经过消费者和分区的分配流程，消费到消息后，最后项服务端提交 Offset 记录消费进度，用来避免重复消费。&lt;/p>
&lt;p>讲完基础概念和架构，我们继续围绕着前面所提到的五个模块来分析一下 Kafka，先来看一下协议和网络模块。&lt;/p>
&lt;h2 id="协议和网络模块">协议和网络模块&lt;/h2>
&lt;p>Kafka 是自定义的私有协议，经过多年发展目前有 V0、V1 和 V2 三个版本，稳定在 V2 版本。官方目前没有支持其他协议，比如 HTTP，但是商业版的 Kafka 都会支持 HTTP 协议，主要原因还是 HTTP 协议使用的便携性。&lt;/p>
&lt;p>Kafka 协议从结构上看包含协议头和协议体两部分，&lt;strong>协议头包含基础通用的信息，协议体由于每个接口的功能参数不一样，内容结构上差异很大&lt;/strong>。&lt;/p>
&lt;p>Kafka 协议的细节在&lt;a href="https://quakewang.github.io/tech/bigdata/bigdata_mq003/">通信协议&lt;/a>中已经讲过，这里就不做过多的赘述。关于协议的更多详细信息还可以参考 &lt;a href="https://kafka.apache.org/protocol.html#:~:text=Kafka%20uses%20a%20binary%20protocol,of%20the%20following%20primitive%20types.">官方文档&lt;/a>。&lt;/p>
&lt;p>Kafka 服务端的网络层是基于 Java NIO 和 Reactor 来开发的，通过多级的线程调度来提供性能。Kafka 网络层细节在&lt;a href="%5Bhttps://quakewang.github.io/tech/bigdata/bigdata_mq004/%5D(https://quakewang.github.io/tech/bigdata/bigdata_mq004/)">网络模块&lt;/a> 也有讲过，可以自己翻回去看。。&lt;/p>
&lt;h2 id="数据存储">数据存储&lt;/h2>
&lt;p>下面，我们继续来看 Kafka 1的存储层，Kafka 同样分为元数据存储和消息存储两部分。&lt;/p>
&lt;h3 id="元数据存储">元数据存储&lt;/h3>
&lt;p>上面我们说过，Kafka 的元数据是存储在 Zookeeper 里面的。元数据信息包括 Topic、分区、Broker 节点和配置信息等。Zookeeper 会持久化存储全量元数据信息，Broker 本身不存储任何集群相关的元数据信息。在 Broker 启动的时候，需要连接 Zookeeper 读取全量元数据信息。&lt;/p>
&lt;p>Zookeeper 是一个单独的开源项目，它自带了集群组网、数据一致性、持久化存储和监听机制等完整的能力。它的底层是基于 Zab 协议组件集群，有 Leader 节点和 Slave 节点的概念，数据写入全部在 Leader 节点完成，Slave 负责数据的读取工作。&lt;/p>
&lt;p>从 Zookeeper 的角度来看，&lt;strong>Kafka 只是它的一个使用者&lt;/strong>。Kafka 用 Zookeeper 的标准使用方式向 Zookeeper 集群上写入、删除和更新数据，以完成 Kafka 的元数据管理、集群构建等工作。所以每台 Broker 启动时，都会在 Zookeeper 注册、监听一些节点信息，从而感知集群的变化。&lt;/p>
&lt;p>另外，Kafka 集群的一些如消费进度信息、事务信息，分层存储元数据，以及 3.0 后的 Raft 架构相关的元数据信息，都是基于内置 Topic 来完成存储的。把数据存储在内置 Topic 中，算是一个比较巧妙的思路了，也是一个值的借鉴的技巧。Kafka 中存储不同功能的元数据信息的 Topic 列表如下所示：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">数据类型&lt;/th>
&lt;th style="text-align: left">Topic 名称&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">消费进度&lt;/td>
&lt;td style="text-align: left">_consumer_offsets&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">事务消息&lt;/td>
&lt;td style="text-align: left">_transaction_state&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">Kafka Raft 版本的元数据&lt;/td>
&lt;td style="text-align: left">_cluster_metadata&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">分层存储元数据&lt;/td>
&lt;td style="text-align: left">_remote_log_metadata&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="消息数据">消息数据&lt;/h3>
&lt;p>在消息数据存储方面，Kafka 的数据是以分区为维度单独存储的。即写入数据到 Topic 后，根据生产分区分配关系，会将数据分发到 Topic 中不同的分区。此时底层不同分区的数据是存储在不同的“文件“中的，即一个分区一个数据存储“文件“。这里提到的“文件“也是一个虚指，在系统底层的表现是一个目录，里面的文件会分段存储。&lt;/p>
&lt;p>如下图所示，当 Broker 收到数据后，是直接将数据写入到不同的分区文件中的。所以在消费的时候，消费者也是直接从每个分区读取数据。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/47efebf445373794f1e81afb87ad1af3a96a03bd/content/imag/tech/bigdata/mq/08_kafka_messagedata.svg" alt="message-data">&lt;/p>
&lt;p>在底层数据存储中，Kafka 的存储结构是以 Topic 和分区维度来组织的。一个分区一个目录，目录名称是 TopicName + 分区号。每个分区的目录下，都会有 .index、.log、.timeindex 三类文件。其中，.index 是偏移量（offset）索引文件，.log 是消息数据的存储文件，.timeindex 是时间戳索引文件。两个索引文件分别根据 Offset 和时间检索数据。&lt;/p>
&lt;p>在节点维度，也会持久存储当前节点的数据信息（如 BrokerID）和一些异常恢复用的 Checkpoint 等数据。由于每个分区存储的数据量会很大，分区数据也会进行分段存储。分段是在 .log 进行的，文件分段的默认数据大小也是 1G，可以通过配置项来修改。&lt;/p>
&lt;p>Kafka 提供了根据过期时间和数据大小清理的机制，清理机制是在 Topic 维度生效的。当数据超过配置的过期时间或者超过大小的限制之后，就会进行清理。清理的机制也是延时清理的机制，它是根据每个段文件进行清理的，即整个文件的数据都过期后，才会清理数据。需要注意的是，根据大小清理的机制是在分区维度生效的，不是 Topic。即当分区的数据大小超过设置大小，就会触发清理逻辑。&lt;/p>
&lt;p>在存储性能上，Kafka 的写入大量依赖顺序写、写缓存、批量写来提高性能。消费方面依赖批量读、顺序读、读缓存的热数据、零拷贝来提高性能。在这些技巧中，每个分区的顺序读写诗高性能的核心。&lt;/p>
&lt;p>接下来，我们看一下 Kafka 的客户端关于生产者和消费者的实现。&lt;/p>
&lt;h2 id="生产者和消费者">生产者和消费者&lt;/h2>
&lt;p>Kafka 客户端在连接 Broker 之前需要经过客户端寻址，找到目标 Broker 的信息。在早期，Kafka 客户端是通过连接 Zookeeper 完成寻址操作的，但是因为 Zookeeper 性能不够，如果大量的客户端都访问 Zookeeper，那么就会导致 Zookeeper 超载，从而导致集群异常。&lt;/p>
&lt;p>所以在新版的 Kafka 中，客户端是通过直连 Broker 完成寻址操作的，不会直接和 Zookeeper 进行交互。即 Broker 与 Zookeeper 进行交互，在本地缓存全量的元数据信息，然后客户端通过连接 Broker 拿到元数据信息，从而避免对 Zookeeper 造成太大负载。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/47efebf445373794f1e81afb87ad1af3a96a03bd/content/imag/tech/bigdata/mq/08_kafka_seek.svg" alt="seek">&lt;/p>
&lt;h3 id="生产者">生产者&lt;/h3>
&lt;p>生产者完成寻址后，在发送数据的时候可以将数据发送到 Topic 或直接发送到分区。发送到 Topic 时会经过生产分区分配的流程，即根据一定的策略将数据发送到不同的分区。&lt;/p>
&lt;h4 id="kafka-提供了轮询和-keyhash-两种策略">Kafka 提供了轮询和 KeyHash 两种策略&lt;/h4>
&lt;p>轮询策略是指按消息维度轮询，将数据平均分配到多个分区。Key Hash 是指根据消息的 Key 生成一个 Hash 值，然后和分区数量进行取余操作，得到的结果可以确定要将数据发送到哪个分区。生产消息分配的过程是在客户端完成的。&lt;/p>
&lt;p>Kafka 协议提高了批量（Batch）发送的语义，所以生产端会在本地先缓存数据，根据不同的分区聚合数据之后，在根据一定的策略批量将数据写入到 Broker。因为这个 Batch 机制的存在，客户端和服务端的吞吐性能会提高很多。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/47efebf445373794f1e81afb87ad1af3a96a03bd/content/imag/tech/bigdata/mq/08_kafka_producer.svg" alt="producer">&lt;/p>
&lt;p>客户端批量往服务端写有两种方式：一种是协议和内核就提供了 Batch 语义，一种是在业务层将一批数据聚合成一次数据发送。这两种虽然都是批量发送，但是它们的区别在于：&lt;/p>
&lt;ul>
&lt;li>第一种批量消息中的每条数据都会有一个 Offset，每条消息在 Broker 看来就是一条消息。第二种批量消息是在这批量消息就是一条消息，只有一个 Offset。&lt;/li>
&lt;li>在消费端看来，第一种对客户端是无感的，一条消息就是一条消息。第二种需要消费者感知生产的批量消息，然后解析批量，逐条处理。&lt;/li>
&lt;/ul>
&lt;h4 id="消费者">消费者&lt;/h4>
&lt;p>Kafka 的消费端只提供了 Pull 模式的消费。即客户端是主动不断地去服务端轮询数据、获取数据，消费则是直接从分区拉取数据的。Kafka 提供了消费分组消费和直连分区消费两种模式，这两者的区别在于，是否需要进行消费者和分区的分配，以及消费进度谁来保存。&lt;/p>
&lt;p>大部分情况下，都是基于消费分组消费。消费分组创建、消费者或分区变动的时候会进行重平衡，重新分配消费关系。Kafka 默认提供了 RangeAssignor（范围）、RoundRobinAssignor（轮询）、 StickyAssignor（粘性）三种策略，也可以自定义策略。消费分组模式下，一个分区只能给一个消费者消费，消费是顺序的。&lt;/p>
&lt;p>当客户端成功消费数据后，会往服务端提交消费进度信息，此时服务端也不会删除具体的消息数据，只会保存消费位点信息。位点数据保存在内部的一个 Topic（__consumer_offset）中。消费端同样提供了自动提交和手动提交两种模式。当消费者重新启动时，会根据上一次保存的位点去消费数据，用来避免重复消费。&lt;/p>
&lt;p>最后我们来看一下 Kafka 对 HTTP 协议和管控操作的支持。&lt;/p>
&lt;h2 id="http-协议支持和管控操作">HTTP 协议支持和管控操作&lt;/h2>
&lt;p>Kafka 内核是不支持 HTTP 协议的，如果需要支持，则需要在 Broker 前面挂一层代理。&lt;/p>
&lt;p>管控的大部分操作是通过 Kafka Protocol 暴露的，基于四层的 TCP 进行通信。还有部分可以通过直连 Zookeeper 完成管控操作。&lt;/p>
&lt;p>在早期很多管控操作都是通过操作 Zookeeper 完成的。后来为了避免对 Zookeeper 造成压力，所有的管控操作都会通过 Broker 再封装一次，即客户端 SDK 通过 Kafka Protocol 调用 Broker，Broker 再去和 Zookeeper 交互。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/47efebf445373794f1e81afb87ad1af3a96a03bd/content/imag/tech/bigdata/mq/08_kafka_http.svg" alt="http">&lt;/p>
&lt;p>Kafka 命令行提供了管控、生产、消费、压测等功能，其底层就是通过客户端 SDK 和 Broker 进行交互的。我们在代码里面也可以通过客户端 SDK 完成相应的操作，不用必须通过命令行。&lt;/p>
&lt;p>因为历史的演进，在一些命令行里面，还残留着直连 Zookeeper 的操作。而我们也可以通过直接操作 Zookeeper 中的数据完成一些操作，比如更改配置、创建 Topic 等等。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>最后，我们再来总结一下 Kafka。&lt;/p>
&lt;ul>
&lt;li>协议层只支持私有的 Kafka Protocol 协议；&lt;/li>
&lt;li>网络层是基于原生的 Java NIO 开发，底层也是通过多路复用、异步 IO、Reactor 模型等技术来提高网络模块的性能；&lt;/li>
&lt;li>存储层是每个分区对应一份具体的存储文件，分区文件在底层会分段存储，同时支持基于时间和大小的数据过期机制；&lt;/li>
&lt;li>元数据存储是通过 Zookeeper 来实现的，所有的元数据都存储在 Zookeeper 中；&lt;/li>
&lt;li>客户端的访问同样也需要经过客户端寻址机制。老版本可以通过 Zookeeper 获取元数据信息，新版本只能通过 Broker 拿到元数据信息。拿到所有元数据信息后，才会直连 Broker；&lt;/li>
&lt;li>生产端支持将数据写入到 Topic 或指定写入某个分区，写入 Topic 时需要经过生产分区分配操作，选择出最终需要写入的分区，同时支持批量写入的语义；&lt;/li>
&lt;li>消费端也有消费分组的概念，消费时需要在多个消费者和消费分组之间进行消费的负载均衡，同时也支持指定分区消费的模式。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Kafka 从生产到消费的全过程：&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>在生产端，客户端会先和 Broker 建立 TCP 连接，然后通过 Kafka 协议访问 Broker 的 MetaData 接口或渠道集群的元数据信息。接着生产者会向 Topic 或分区发送数据，如果是发送到 Topic，那么客户端会有消息分区分配的过程。因为 Kafka 协议具有批量发送语义，所以客户端会先在客户端缓存数据。然后根据一定的策略，通过异步线程将数据发送到 Broker；&lt;/li>
&lt;li>Broker 收到数据之后，会根据 Kafka 协议解析出请求内容，做好数据校验，然后重整数据结构，将数据按照分区的维度写入到底层不同的文件中。如果分区配置了副本，则消息数据会被同步到不同的 Broker 中进行保存；&lt;/li>
&lt;li>在消费端，Kafka 提供了消费分组和指定分区消费两种模式。消费端也会先经过寻址拿到完整的元数据信息，然后连接上不同的 Broker。如果是消费分组模式消费，则需要经过重平衡、消费分区分配流程，然后连接上对应分区的 Leader，接着调用 Broker 的 Fetch 接口进行消费。最后一步则是需要提交消费进度来保存消费信息。&lt;/li>
&lt;/ol></content></entry><entry><title type="text">MQ007——剖析消费者 SDK</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/bigdata/bigdata_mq007/"/><id>https://quakewang.github.io/tech/bigdata/bigdata_mq007/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-09-20T15:29:02+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="消费端消费者客户端-sdk-有哪些设计">消费端：消费者客户端 SDK 有哪些设计？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>上一篇内容讲了生产端，这次继续来聊聊有关消费端的内容。从技术上看，消费端 SDK 和生产端 SDK 一样，主要包括客户端基础功能和消费相关功能两部分。客户端基础功能之前已经讲过，这里也就不做过多的赘述。&lt;/p>……</summary><content type="html">&lt;h1 id="消费端消费者客户端-sdk-有哪些设计">消费端：消费者客户端 SDK 有哪些设计？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>上一篇内容讲了生产端，这次继续来聊聊有关消费端的内容。从技术上看，消费端 SDK 和生产端 SDK 一样，主要包括客户端基础功能和消费相关功能两部分。客户端基础功能之前已经讲过，这里也就不做过多的赘述。&lt;/p>
&lt;p>从实现上看，消费相关功能包括&lt;strong>消费模型、分区消费模型、消费分组（订阅）、消费确认和消费失败处理&lt;/strong>五个部分。我们一个一个来看。&lt;/p>
&lt;h2 id="消费模型的选择">消费模型的选择&lt;/h2>
&lt;p>为了满足不同场景的业务需求，从实现机制上来看，主流消息队列一般支持 Pull、Push 和 Pop 三种消费模型。&lt;/p>
&lt;h3 id="pull-模型">Pull 模型&lt;/h3>
&lt;p>Pull 模型是指客户端通过不断轮询的方式想服务端拉取数据。它是消息队列中使用最广泛和最基本的模型，主流的消息队列一般也都支持这个模型。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/81508b8799de9c4aa4951e78ff3fafa553672d79/content/imag/tech/bigdata/mq/07_consumer_pull01.svg" alt="pull01">&lt;/p>
&lt;p>它的好处是客户端根据自身的处理速度去拉取数据，不会对客户端和服务端造成额外的风险和负载压力。缺点是可能会出现大量无效返回的 Pull 调用，另外消费及时性不够，无法满足一些需要全链路低耗时的场景。&lt;/p>
&lt;p>为了提供消费性能，Pull 模型都会支持批量读，即&lt;strong>在客户端指定需要拉取多少条数据或者拉取多大的数据&lt;/strong>，然后传递给服务端。客户端拉取到数据并处理完成后，再重复拉取数据处理。如前面讲的，这种拉取模式的缺点是可能会出现长时间轮询到空数据的情况，从而浪费通信资源，提高服务端的负载。&lt;/p>
&lt;p>比如下面这个场景，当 Topic1 数据已经被消费完，此时如果消费者频繁来拉取数据并立即返回结果，客户端就会不停地重复请求服务端。当空数据请求特别多的时候，就会造成资源损耗，不利于提高吞吐，也有可能导致负载问题。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/81508b8799de9c4aa4951e78ff3fafa553672d79/content/imag/tech/bigdata/mq/07_consumer_pull02.svg" alt="pull02">&lt;/p>
&lt;p>为了解决这个问题，正常的思路是在客户端根据一定策略进行等待和回避。这样做的话，就会出现如何设置等待时间的问题，客户端等待时间设置不合理就会出现消费不及时的情况。&lt;/p>
&lt;p>为了解决空请求带来的问题，一般服务端会协助处理，有如下两种思路：&lt;/p>
&lt;h4 id="1-服务端-hold-住请求">1. 服务端 hold 住请求&lt;/h4>
&lt;p>当客户端根据策略拉取数据时，如果没有足够的数据，就先在服务端等一段时间，等有数据后一起返回给客户端。这种方案的好处是，可以尽量提高吞吐能力，不会有太多的空交互请求。缺点则是如果长时间不给客户端回包，会导致客户端请求超时，另外当数据不够时，hold 住请求的时间太长就会提高消费延时。&lt;/p>
&lt;h4 id="2-服务端有数据的时候通知客户端">2. 服务端有数据的时候通知客户端&lt;/h4>
&lt;p>当服务端不 hold 住请求，立刻返回空数据，客户端收到空数据时则不再发起请求，会等待服务端的通知。当服务端有数据的时候，再主动通知客户端来拉取。这种方案的好处是可以及时通知客户端来拉取数据，从而降低消费延时。缺点是因为客户端和服务端一般是半双工的通信，此时服务端是不能主动向客户端发送消息的。&lt;/p>
&lt;p>所以在 Pull 模型中，比较合适的方案是客户端告诉服务端：&lt;strong>最多需要多少数据、最少需要多少数据、未达到最小数据时可以等多久&lt;/strong>三个信息。然后服务端首先判断是否有足够的数据，有的话就立即返回，否则就根据客户端设置的等待时长 hold 住请求，如果超时，无论是否有数据，都会直接给客户端返回当前的结果。&lt;/p>
&lt;p>这种策略可以解决频繁不可控的空轮询请求。即使全是空轮询，对单个消费者来说，其 TPS 也是可以预估的，即总时间 / 等待时长 = 总轮询次数。而如果需要降低消费延时，可以通过降低最小获取的数据大小和最大等待时长来提高获取的频率，从而尽量降低延时。通过这种方案，我们可以把理想的消费延迟时间降低到两次 Pull 请求之间的时间间隔。&lt;/p>
&lt;p>在一些业务消息的场景中，因为应对的场景规模有限，可以将最大等待时长设置为 0，此时消费模型就变成了“请求—返回”的模式，当没有数据的时候就会立即返回数据，其余逻辑交给客户端自己处理。&lt;/p>
&lt;h3 id="push-模型">Push 模型&lt;/h3>
&lt;p>Push 模型是为了解决消费及时性而提出来的。这个模型的本意是指当服务端有数据时会主动推给客户端，让数据的消费更加及时。理想中的思路如下图所示，即当服务端由数据以后，会主动推给各个消费者。在实际的 Push 模型的实现上，一般有 Broker 内置 Push 功能、Broker 外独立实现 Push 功能的组件、在客户端实现伪 Push 功能三种思路。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/81508b8799de9c4aa4951e78ff3fafa553672d79/content/imag/tech/bigdata/mq/07_consumer_push01.svg" alt="push01">&lt;/p>
&lt;h4 id="1-broker-内置-push-功能">1. Broker 内置 Push 功能&lt;/h4>
&lt;p>&lt;strong>第一种，Broker 内置 Push 功能是指在 Broker 中内置标准的 Push 的能力，由服务端向客户端主动推送数据。&lt;/strong>&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/81508b8799de9c4aa4951e78ff3fafa553672d79/content/imag/tech/bigdata/mq/07_consumer_push02.svg" alt="push02">&lt;/p>
&lt;p>这种方案的好处是 Broker 自带 Push 能力，无需重复开发和部署。Broker 内部可以感知到数据堆积情况，可以保证消息被及时消费。缺点是当消费者很多时，内核需要主动维护很多与第三方的长连接，并且需要处理各种客户端异常，比如客户端卡住、接收慢、处理慢等情况。这些推送数据、异常处理、连接维护等工作需要消耗很多的系统资源，在性能上容易对 Broker 形成反压，导致 Broker 本身的性能和稳定性出现问题。所以这种方案在主流消息队列中用得较少，为了保证消息投递的高效及时（比如全链路的毫秒级耗时），才会采用这种方案。&lt;/p>
&lt;h4 id="2-broker-外独立实现-push-功能的组件">2. Broker 外独立实现 Push 功能的组件&lt;/h4>
&lt;p>&lt;strong>第二种，Broker 外独立实现 Push 功能的组件是指独立于 Broker 提供一个专门实现 Push 模型的组件。通过先 Pull 数据，再将数据 Push 给客户端，从而简化客户端的使用，提高消费数据的及时性。&lt;/strong>&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/81508b8799de9c4aa4951e78ff3fafa553672d79/content/imag/tech/bigdata/mq/07_consumer_push03.svg" alt="push03">&lt;/p>
&lt;p>这种方案的好处是将 Push 组件单独部署，解决了 Broker 的性能和稳定性问题，也能实现 Push 的效果。缺点是虽然实现了 Push 的模型，但其本质还是先 Pull 再 Push，从全链路来看，还是会存在延时较高的问题，并且需要单独开发独立的 Push 组件，开发和运维成本比较高。&lt;/p>
&lt;p>从实际业务上来讲，这种模型的使用场景较为有限，主要用在回调、事件触发的场景，在实际的流消费场景用的不是很多。主要是因为通过第三方组件的 Push 灵活性不够，性能会比 Pull 第。&lt;/p>
&lt;h4 id="3-客户端实现伪-push-功能">3. 客户端实现伪 Push 功能&lt;/h4>
&lt;p>**第三种，在客户端实现伪 Push 功能是指在客户端内部维护内部队列，SDK 底层通过 Pull 模型从服务端拉取数据存储到客户端的内存队列中。**然后通过回调的方式，触发用户设置的回调函数，将数据推送给应用程序，在使用体验上看就是 Push 的效果。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/81508b8799de9c4aa4951e78ff3fafa553672d79/content/imag/tech/bigdata/mq/07_consumer_push04.svg" alt="push04">&lt;/p>
&lt;p>这种方案的好处在于通过客户端底层的封装，从用户体验看是 Push 模型的效果，解决用户代码层面的不断轮询问题，降低了用户的使用复杂度。缺点是底层依旧是 Pull 模型，还是得通过不断轮询的方式去服务端拉取数据，就会遇到 Pull 模型遇到的问题。&lt;/p>
&lt;p>在客户端实现伪 Push，是目前消息队列在实现 Push 模型常用的实现方案，因为它解决了客户体验上的主动回调触发消费问题。虽然底层会有不断轮询和消费延时的缺点，但是可以通过合理的编码设计来降低这两个问题的影响。&lt;/p>
&lt;p>因为 Push 模型需要先分配区和消费者的关系，客户端就需要感知分区分配、分区均衡等操作，从而在客户端就需要实现比较重的逻辑。并且当客户端和订阅的分区数较多时，容易出现需要长时间的重平衡时间的情况。此时为了解决这个问题，于是就有了 Pop 模型。&lt;/p>
&lt;h3 id="pop-模型">Pop 模型&lt;/h3>
&lt;p>Pop 模型想解决的是客户端实现较重，重平衡会暂停消费并且可能时间较长，从而出现消费倾斜的问题。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/8f6d00d95b0b5561943be6232e5d31ff9246688f/content/imag/tech/bigdata/mq/07_consumer_pop.svg" alt="pop">&lt;/p>
&lt;p>它的思路是客户端不需要感知到分区，直接通过 Pop 模型提供的 get 接口去获取到数据，消费成功后 ACK 数据。这就像发起 HTTP 请求去服务端拉取数据一样，不用感知服务端的数据分布情况，只需要拉到数据。这种方案的好处是简化了消费模型，同时服务端可以感知到消费的堆积情况，可以根据堆积情况返回哪些分区的数据给客户端，这样也就简化了消息数据的分配策略。&lt;/p>
&lt;p>从实现上来看，它将分区分配的工作移到了服务端，在服务端完成了消费者的分区分配、进度管理，然后暴露出新的 Pop 和 ACK 接口。客户端调用 Pop 接口去拿去数据，消费成功后调用 ACK 去确认数据。可以类比 HTTP 中的 Request 和 Response 使用模型。&lt;/p>
&lt;h2 id="分区消费模式">分区消费模式&lt;/h2>
&lt;p>我们知道，消息队列的数据是在 Partition/Queue 维度承载的，所以消费过程中一个重要的工作就是消费者和分区的消费模式问题，即分区的数据能不能被多个消费者并发消费，一条数据能不能被所有消费者消费到，分区的数据能不能被顺序消费等等。&lt;/p>
&lt;p>从技术上看，在数据的消费模式上主要有独占消费、共享消费、广播消费和灾备消费四种思路&lt;/p>
&lt;h3 id="独占消费">独占消费&lt;/h3>
&lt;p>**独占消费是指一个分区同一个时间只能被一个消费者消费。**在消费者启动时，会分配消费者和分区之间的消费关系。当消费者数据和分区数量都没有变化的情况下，两者之间的分配关系不会变动。当分配关系变动时，一个分组也只能被一个消费者消费，这个消费者可能是当前的，也可能是新的。如果消费者数量大于分区数量，则会有消费者被空置；反之，如果分区数量大于消费者数量，一个消费者则可以同时消费多个分区。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/81508b8799de9c4aa4951e78ff3fafa553672d79/content/imag/tech/bigdata/mq/07_consumer_partition01.svg" alt="partition01">&lt;/p>
&lt;p>独占消费的好处是可以保证分区维度的消费是有序的。缺点是当数据出现倾斜、单个消费者出现性能问题或 hang 住时，会导致有些分区堆积严重。Kafka 默认支持的就是独占消费的类型。&lt;/p>
&lt;h3 id="共享消费">共享消费&lt;/h3>
&lt;p>**共享消费是指单个分区的数据可以同时被多个消费者消费。**即分区的数据会依次投递给不同的消费者，一条数据只会投递给一个消费者。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/81508b8799de9c4aa4951e78ff3fafa553672d79/content/imag/tech/bigdata/mq/07_consumer_partition02.svg" alt="partition02">&lt;/p>
&lt;p>这种方式的好处是，可以避免单个消费者的性能和稳定性问题导致分区的数据堆积。缺点是无法保证数据的顺序消费。这种模式一般用在对数据的有序性无要求的场景，比如日志。&lt;/p>
&lt;h3 id="广播消费">广播消费&lt;/h3>
&lt;p>**广播消费是指一条数据要能够被多个消费者消费到。**即分区中的一条数据可以投递给所有的消费者，这种方式是需要广播消费的场景。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/81508b8799de9c4aa4951e78ff3fafa553672d79/content/imag/tech/bigdata/mq/07_consumer_partition02.svg" alt="partition03">&lt;/p>
&lt;p>实现广播消费一般有内核实现广播消费的模型、使用不同的消费分组消费和指定分区消费三种技术思路。&lt;/p>
&lt;ul>
&lt;li>内核实现广播消费的模型，指在 Broker 内核中的消息投递流程实现广播消费模式，即 Broker 投递消息时，可以将一条消息吐给不同的消费者，从而实现广播消费。&lt;/li>
&lt;li>使用不同的消费分组对数据进行消费，指通过创建不同的消费者组消费同一个 Topic 或分区，不同的消费分组管理自己的消费进度，消费到同一条消息，从而实现广播消费的效果。&lt;/li>
&lt;li>指定分区消费，是指每个消费者指定分区进行消费，在本地记录消费位点，从而实现不同消费者消费同一条数据，达到广播消费的效果。&lt;/li>
&lt;/ul>
&lt;p>三种方案的优劣对比：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">广播消费类型&lt;/th>
&lt;th style="text-align: left">优点&lt;/th>
&lt;th style="text-align: left">缺点&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">内核实现&lt;/td>
&lt;td style="text-align: left">客户端成本低，无多余工作&lt;/td>
&lt;td style="text-align: left">服务端开发工作量大&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">消费分组实现&lt;/td>
&lt;td style="text-align: left">统一消费模型，无需服务端开发&lt;/td>
&lt;td style="text-align: left">需要创建很多消费分组&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">指定分区消费&lt;/td>
&lt;td style="text-align: left">统一消费模型，避免创建很多消费分组&lt;/td>
&lt;td style="text-align: left">客户端编码工作较重，使用相对复杂&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="灾备消费">灾备消费&lt;/h3>
&lt;p>&lt;strong>灾备消费是独占消费的升级版，在保持独占消费可以支持顺序消费的基础上，同时加入灾备的消费者。&lt;/strong> 当消费者出现问题的时候，灾备消费者加入工作，继续保持独占顺序消费。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/81508b8799de9c4aa4951e78ff3fafa553672d79/content/imag/tech/bigdata/mq/07_consumer_partition04.svg" alt="partition04">&lt;/p>
&lt;p>好处是既能保持独占顺序消费，又能保证容灾能力。缺点是无法解决消费倾斜的性能问题，而且还需要准备一个消费者来做灾备，使用成本比较高。&lt;/p>
&lt;h2 id="消费分组">消费分组&lt;/h2>
&lt;p>消费分组是用来组织消费者、分区、消费进度关系的逻辑概念。为什么需要消费分组呢？&lt;/p>
&lt;p>在没有消费分组直接消费 Topic 的场景下，如果希望不重复消费 Topic 中的数据，那么就&lt;strong>需要有一个标识来标识当前的消费情况，比如记录进度&lt;/strong>。这个唯一标识就是消费分组。&lt;/p>
&lt;p>在一个集群中可以有很多消费分组，消费分组间通过名称来区分。消费分组自身的数据是集群元数据的一部分，会存储在 Broker 的元数据存储服务中。消费分组主要有管理消费者和分区的对应关系、保存消费者的消费进度、实现消息可重复被消费三类功能。&lt;/p>
&lt;p>消费分组和 Topic 是强相关的，它需要包含 Topic 才有意义，一个空的消费分组是没有意义的。消费分组内有很多个消费者，一个消费分组也可以订阅和消费多个 Topic，一个 Topic 也可以被多个消费分组订阅和消费。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/81508b8799de9c4aa4951e78ff3fafa553672d79/content/imag/tech/bigdata/mq/07_consumer_group01.svg" alt="group01">&lt;/p>
&lt;p>因为 Topic 不存储真实数据，分区才存储消息数据，所以就需要解决消费者和分区的分配关系，即&lt;strong>哪个分区被哪个消费者消费，这个分配的过程就是消费重平衡&lt;/strong>。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/81508b8799de9c4aa4951e78ff3fafa553672d79/content/imag/tech/bigdata/mq/07_consumer_group02.svg" alt="group02">&lt;/p>
&lt;p>从流程上来看，当新建一个消费分组的时候，就需要开始分配消费者和分区的消费关系了。分配完成后，就可以正常消费。如果消费者和分区出现变动，比如消费者挂掉、新增消费者、订阅的 Topic 的分区数发生变化等等，就会重新开始分配消费关系，否则就会存在某些分区不能被订阅和消费的情况。&lt;/p>
&lt;h3 id="协调者">协调者&lt;/h3>
&lt;p>从实现上来看，如果要对消费者和分区进行分配，肯定需要有一个模块拥有消费分组、所有的消费者、分区信息三部分信息，这个模块我们一般命名为&lt;strong>协调者&lt;/strong>。协调者主要的工作就是执行消费重平衡，并记录消费分组的消费进度。&lt;/p>
&lt;p>在消费分组创建、消费者变化、分区变化的时候就会触发重新分配。分区分配的操作可以在协调者内部或消费者上完成。&lt;/p>
&lt;ul>
&lt;li>在协调者完成，即协调者首先获取消费者和分区的信息，然后再协调者内部完成分区分配，最后再把分配关系同步给所有消费者。&lt;/li>
&lt;li>在消费者完成，即负责分配的消费者获取所有消费者和分区的信息，然后该消费者完成分区分配操作，最后再把分配关系同步给其他消费者。&lt;/li>
&lt;/ul>
&lt;p>从技术上来看，这两种形式的优劣区别并不大，取决于代码的实现。&lt;strong>一般在创建消费分组和消费者 / Topic 分区变化的时候，会触发协调者执行消费重平衡&lt;/strong>。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/81508b8799de9c4aa4951e78ff3fafa553672d79/content/imag/tech/bigdata/mq/07_consumer_group03.svg" alt="group03">&lt;/p>
&lt;p>从实现的角度来看，协调者一般是 Broker 内核的一个模块，就是一段代码或者一个类，专门用来完成上述的工作。当有多台 Broker 时，协调者的实现有多种方式，比如 Kafka 集群每台 Broker 都有协调者存在。通过消费分组的名称计算出来一个 hash 值和 _consumer_offset 的分区数，取余计算得出一个分区号。最后这个分区号对应的 Leader 所在的 Broker 节点就是协调者所在的节点。客户端就和计算出来的这台 Broker 节点进行交互，来执行消费重平衡的相关操作。&lt;/p>
&lt;p>当有了协调者后，就需要确认哪个分区给哪个消费者了，此时就需要一个分配策略来执行，这就是消费分区分配策略。&lt;/p>
&lt;h3 id="消费分区分配策略">消费分区分配策略&lt;/h3>
&lt;p>再具体实现上，一般内核会默认提供几种分配策略，也可以通过定义接口来支持用户自定义实现分区分配策略。分区分配策略的制定一般遵循以下三个原则：&lt;/p>
&lt;ul>
&lt;li>各分区的数据能均匀地分配给每个消费者，保证所有消费者的负载最大概率是均衡的，该原则最为常用；&lt;/li>
&lt;li>在每次重新分配的时候，尽量减少陪去和消费者之间的关系变动，这样有助于加快重新分配的速度，并且保持数据处理的连续性，降低处理切换成本；&lt;/li>
&lt;li>可以运行灵活地根据业务特性指定分配关系，比如根据机房就近访问最近的分区、某个 Topic 的奇数分区分配给第一个消费者等等。&lt;/li>
&lt;/ul>
&lt;p>所有消息队列的默认策略都是相对通用的，一般都会包含有轮询、粘性、自定义三种策略。&lt;/p>
&lt;h4 id="轮询">轮询&lt;/h4>
&lt;p>&lt;strong>轮询&lt;/strong>就是指用轮询的方式将分区分配给各个消费者，保证每个消费者的分区数量是尽量相同的，从而保证消费者的负载最大概率上是均衡的。思路是拿到所有主题的所有分区和所有消费者，根据拿到的顺序（实际实现中可能会先全部打乱，以确保随机性）将分区逐个分配给消费者。分配到最后的效果是，每个消费者所分到的分区数是一样的，最多相差 1 个分区。比如 tp0 有 3 分区，tp1 有 2 分区，tp2 有 3 分区，分配后效果如下。&lt;/p>
&lt;blockquote>
&lt;p>消费者 1：tp0-0、tp2-1、tp1-1&lt;/p>
&lt;p>消费者 2：tp2-2、tp0-1、tp2-0&lt;/p>
&lt;p>消费者 3：tp1-0、tp0-2&lt;/p>
&lt;/blockquote>
&lt;p>因为 Topic 一般会有多个分区，默认情况下写入大部分是均匀的。这个方案的优点是，从随机性的原理来看，打乱分区后再分配给每个消费者，消费者的负载大概率是均匀的。但是也有可能出现不均衡，比如当消费组同时订阅多个分区时，有可能会将同一个 Topic 的多个分区都分配给一个消费者，从而出现消费者的负载倾斜。&lt;/p>
&lt;p>在轮询的基础上，为了解决随机轮询的情况，某些流量搞的 Topic 可能会分配给同一个消费者。为了解决这种情况，就可以调整一下轮询的策略，比如在随机的基础上，将 Topic 的不同分区尽量打散到不同的消费者，从而保证整体消费者之间的分区是均衡的，如下所示：&lt;/p>
&lt;blockquote>
&lt;p>消费者1：tp0-0、tp2-1、tp1-1&lt;/p>
&lt;p>消费者 2：tp0-1、tp2-0、tp1-0&lt;/p>
&lt;p>消费者 3：tp0-2、tp2-2&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>主要的核心思路都是为了消费者更加均衡，避免消费倾斜。&lt;/strong>&lt;/p>
&lt;h4 id="粘性">粘性&lt;/h4>
&lt;p>&lt;strong>粘性&lt;/strong>是指尽量减少分区分配关系的变动，进而减少重平衡所耗费的时间和资源损耗。即当已经分配好消费者和分区的消费关系后，当消费者或者分区出现变动，就会触发重平衡。从底层来看，可能就是一个消费者掉了或者新增分区。此时需要重新进行分配的消费者和分区其实是有限的，大部分的分配关系可以不动。而此时，如果使用轮询算法，则要全部打算重来，耗时就会非常长，并且浪费资源，即把原先不需要重新分配的关系都重新分配一遍。&lt;/p>
&lt;p>粘性的效果如下，比如当上面的消费者 3 挂了后，只需要将 tp1-0、tp0-2 平均分给消费者 1 和 2 即可，消费者 1 和 2 原先分配的分区不用动。&lt;/p>
&lt;blockquote>
&lt;p>消费者1：tp0-0、tp2-1、tp1-1、tp1-0&lt;/p>
&lt;p>消费者 2：tp0-1、tp2-0、tp1-0、tp0-2&lt;/p>
&lt;/blockquote>
&lt;p>在实际的实现中，为了减少重新分配关系，有一个非常常用的算法是&lt;strong>一致性哈希&lt;/strong>。一致性哈希的算法经常用在负载均衡中。用一致性哈希实现粘性分配策略的优点是，当节点或者分区变动时，只需要执行少量的分区再分配即可。&lt;/p>
&lt;h4 id="自定义策略">自定义策略&lt;/h4>
&lt;p>在一些消息队列中，也会提供一些与自己相关的特色的分区分配策略。比如 Kafka 就提供了轮询策略改进版的 &lt;code>RoundRobinAssignor &lt;/code>分配策略。这些策略的核心出发点，都是为了解决消费者和分区之间的分配均衡、重平衡耗时、业务场景需要等诉求。&lt;/p>
&lt;p>自定义分区分配算法，和生产端数据的分区分配策略是一样的，内核会提供接口，用户可以根据自身需求自定义算法，然后指定配置生效即可。比如 Kafka 提供了 &lt;code>org.apache.kafka.clients.consumer.internals.PartitionAssignor&lt;/code> 接口来提供自定义分区分配策略。&lt;/p>
&lt;h2 id="消费确认">消费确认&lt;/h2>
&lt;p>那么当数据被消费成功后，就必须进行消费确认操作了，告诉服务端已经成功消费了这个数据。消费确认就是我们在消息队列中常说的 ACK。一般情况下，消费确认分为确认后删除数据和确认后保存数据两种形式。&lt;/p>
&lt;p>&lt;strong>确认后删除数据&lt;/strong>是指集群的每条消息只能被消费一次，只要数据被消费成功，就会回调服务端的 ACK 接口，服务端就会执行数据删除操作。在实际开发过程中，一般都会支持单条 ACK 和 批量ACK 两种操作。这种方式不利于回溯消费，所以用得比较少。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/81508b8799de9c4aa4951e78ff3fafa553672d79/content/imag/tech/bigdata/mq/07_consumer_ack01.svg" alt="ack01">&lt;/p>
&lt;p>&lt;strong>消费成功保存消费进度&lt;/strong>是指当消费数据成功后，调用服务端的消费进度接口来保持消费进度。这种方式一般都是基于配合消费分组一起用的，服务端从消费分组维度来保存进度数据。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/81508b8799de9c4aa4951e78ff3fafa553672d79/content/imag/tech/bigdata/mq/07_consumer_ack02.svg" alt="ack02">&lt;/p>
&lt;p>为了保证消息的回溯消费和多次消费，消息队列大多数用的是第二种方案。&lt;strong>数据的删除交由数据过期策略去执行&lt;/strong>。&lt;/p>
&lt;p>保存消费进度一般为服务端保存和客户端自定义保存两种机制实现。&lt;/p>
&lt;p>&lt;strong>服务端保存&lt;/strong>是指当消费端消费完成后，客户端需要调用一个接口提交信息，这个接口是由服务端提供的”提交消费进度“接口，然后服务端会持久保存进度。当客户端断开重新消费时，可以从服务端读取这个进度进行消费。服务端一般会通过内置的 Topic 或者文件来持久保存该数据。这种方式的优点是客户端会封装好这些逻辑，使用简单，无序管理进度相关的信息，缺点就是不够灵活。服务端保存一般是默认的方案。&lt;/p>
&lt;p>在提交位点信息的时候，底层一般支持自动提交和手动提交两种实现。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>自动提交&lt;/strong>一般是根据时间批次或数据消费到客户端后就自动提交，提交过程客户端无感知；&lt;/li>
&lt;li>&lt;strong>手动提交&lt;/strong>：是指业务根据自己的处理情况，手动提交进度信息，以避免业务处理异常导致的数据丢失。&lt;/li>
&lt;/ul>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">提交方式&lt;/th>
&lt;th style="text-align: left">优点&lt;/th>
&lt;th style="text-align: left">缺点&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">自动提交&lt;/td>
&lt;td style="text-align: left">实现简单，业务无感知，使用成本低&lt;/td>
&lt;td style="text-align: left">可能会漏消费数据，导致数据丢失&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">手动提交&lt;/td>
&lt;td style="text-align: left">安全可控&lt;/td>
&lt;td style="text-align: left">需要代码提交，使用成本较高&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>如果想避免数据丢失的情况下，优先考虑手动提交的方式。&lt;/p>
&lt;p>&lt;strong>客户端自定义保存&lt;/strong>是指当消费完成后，客户端自己管理保存消费进度。此时就不需要向服务端接口提交进度信息了，自定义保存进度信息即可，比如保存在客户端的缓存、文件、自定义的服务中，当需要修改和回滚的时候就比较方便。这种方案的的优点是灵活，缺点是会带来额外的工作量。&lt;/p>
&lt;h2 id="消费失败处理">消费失败处理&lt;/h2>
&lt;p>我们知道，一个完整的消费流程包括消费数据、本地业务处理、消费进度提交三部分。那么从消费失败的角度来看，就应该分为从服务端拉取数据失败、本地业务数据处理失败、提交位点信息失败三种情况。下面我们逐一来看。&lt;/p>
&lt;p>&lt;strong>从服务端拉取数据失败&lt;/strong>，和客户端的错误逻辑处理是一致的，根据可重试错误和不可重试错误的分类，进行重复消费或者向上抛错。&lt;/p>
&lt;p>&lt;strong>本地业务数据处理失败&lt;/strong>，处理起来就变囧复杂了。如果是偶尔失败，那么在业务层做好重试处理逻辑，配合手动提交消费进度的操作即可解决。如果是一直失败，即使重试多次也无法被解决，比如这条数据内容有异常，导致无法被处理。此时如果一直重试，就会出现消费卡住的情况，这就需要配合死信队列等功能，将无法被处理的数据投递到死信队列中，从而保存异常数据，并保证消费进度不阻塞。&lt;/p>
&lt;p>&lt;strong>提交位点信息失败&lt;/strong>，其处理方法通常是一直重试，重复提交，如果持续失败就向上抛错。因为如果提交进度失败，即使再从服务端拉取数据，还是会拉到同一批数据，出现重复消费的问题。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>在消费端，为了提高消费速度和消息投递的及时性，需要选择合适的消费模型，目前主流有 Pull、Push 和 Pop 三种模型。&lt;/p>
&lt;p>这三种模型的应用场景都不一样。目前业界主流消息队列使用的都是 Pull 模型。但为了满足业务需求，很多消息队列也会支持 Push 模型和 Pop 模型。其中，Push 模型的及时性更高，实现较为复杂，限制也比较多。Pop 模型本质上是 Pull 模型的一种，只是在实现和功能层面上，与 Push 的实现思路和使用场景不一样。所以在模型的选择上来看，因为场景复杂，三种模型都是需要的。&lt;/p>
&lt;p>常用的消费模式一般有独占消费、共享消费、广播消费和灾备消费四种。为了避免堆积，保证消息消费顺序，一般需要选择分区独占的消费模式。从单分区的维度，共享消费的性能是最高的。广播消费主要是通过创建多个消费分组、指定分区消费来实现的。灾备消费的场景用的则相对较少。&lt;/p>
&lt;p>从设计上看，消费端要解决的问题依次分为三步：&lt;/p>
&lt;ol>
&lt;li>满足基本的消费需求，能消费到数据，确认数据；&lt;/li>
&lt;li>满足稳定性和性能的需求，能快速稳定地消费到数据；&lt;/li>
&lt;li>支持功能方面的需求，比如回溯消费、消费删除、广播消费等等。&lt;/li>
&lt;/ol>
&lt;p>为了能满足基本的消费需求，服务端会提供消费和确认接口，同时在客户端封装消费和确认操作中，底层通过网络层和服务端建立、维护 TCP 连接，然后通过协议完成基本的消费操作。&lt;/p>
&lt;p>如果要回溯消费，则需要单独记录消费进度。这样就能抽象出消费分组的概念，用来管理消费者、分区和消费进度的关系。通过消费分组来记录消费进度，从而实现数据的多次分发。另外，消费分组机制也可以用在广播消费的场景。&lt;/p>
&lt;p>在消费确认的过程中，一般需要客户端回调服务端提供的确认接口。确认接口分为确认删除和确认记录消费进度两种模式。主流方式是在确认的时候记录消费进度。&lt;/p>
&lt;p>异常处理主要是为了保证数据能被正常消费，重点关注不丢数据、不重复消费、不阻塞住消费三个问题，我们需要针对不同的问题做不一样的处理。&lt;/p></content></entry><entry><title type="text">MQ006——剖析生产者 SDK</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/bigdata/bigdata_mq006/"/><id>https://quakewang.github.io/tech/bigdata/bigdata_mq006/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-09-06T23:00:49+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="生产端生产者客户端-sdk-有哪些设计要点">生产端：生产者客户端 SDK 有哪些设计要点？？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>大部分开发者在使用某个组件或框架的时候，都希望能够做到开箱即用，作为一款成熟的产品来说，也确实应该做到。那么，在使用的过程中是否会有疑问，这些框架的 SDK 底层是如何工作的呢，由哪些功能模块所组成的呢？消息队列的客户端主要包含生产、消费、集群管控三类功能。我们先用 MQ 中的生产者为例，来进行一个浅层次的设计分析。。从客户端 SDK 实现的角度来看，生产模块包含&lt;strong>客户端基础功能和生产相关功能&lt;/strong>两部分，其中基础功能是客户端所有功能共有的。如下图所示：&lt;/p>……</summary><content type="html">&lt;h1 id="生产端生产者客户端-sdk-有哪些设计要点">生产端：生产者客户端 SDK 有哪些设计要点？？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>大部分开发者在使用某个组件或框架的时候，都希望能够做到开箱即用，作为一款成熟的产品来说，也确实应该做到。那么，在使用的过程中是否会有疑问，这些框架的 SDK 底层是如何工作的呢，由哪些功能模块所组成的呢？消息队列的客户端主要包含生产、消费、集群管控三类功能。我们先用 MQ 中的生产者为例，来进行一个浅层次的设计分析。。从客户端 SDK 实现的角度来看，生产模块包含&lt;strong>客户端基础功能和生产相关功能&lt;/strong>两部分，其中基础功能是客户端所有功能共有的。如下图所示：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/da51f810a77f65197bb33a1fe7e3914c764d7f47/content/imag/tech/bigdata/mq/06_producer_framework.svg" alt="producer-framework">&lt;/p>
&lt;p>基础功能是蓝色部分，包括请求连接管理、心跳检测、内容构建、序列化、重试、容错处理等等。生产功能是黄色部分，包括客户端寻址、分区选择、批量发送、生产错误处理、SSL、幂等和压缩等等。&lt;/p>
&lt;h2 id="客户端基础功能">客户端基础功能&lt;/h2>
&lt;h3 id="连接管理">连接管理&lt;/h3>
&lt;p>在&lt;a href="https://quakewang.github.io/tech/bigdata/bigdata_mq004/">网络模块&lt;/a>，讲过客户端和服务端之间基本都是通过各自语言的网络库，创建 TCP 长连接进行通信的。在大部分实现中，为了避免连接数膨胀，每个客户端实例和每台 Broker 只会维护一条 TCP 连接。&lt;/p>
&lt;p>建立一条 TCP 连接是简单的，关键的是，什么情况下建立连接呢？？一般来说有初始化创建连接和使用时创建连接两种方式。&lt;/p>
&lt;ul>
&lt;li>初始化创建连接：指在实例初始化时就创建到各个 Broker 的 TCP 连接，等待数据发送。好处是提前创建好可以避免发送的时候冷启动；缺点是需要提前创建好所有的连接，可能导致连接空跑，会消耗一定的资源。&lt;/li>
&lt;li>使用时创建连接：指在实例初始化时不建立连接，当需要发送数据时再建立。好处是发送时再连接，连接的使用率会较高；缺点是可能出现连接冷启动，会增加一点本次请求的耗时。&lt;/li>
&lt;/ul>
&lt;p>因为客户端会有空闲连接回收机制，创建连接的耗时一般较短，所以在实际的架构实现中，两种方式都会有用到，优劣区别并不是很明显。不过，从资源利用率的角度考虑，&lt;strong>建议使用晚建立连接的方式&lt;/strong>。&lt;/p>
&lt;p>因为连接并不是任何时候都有数据，可能出现长时间连接空闲。所以连接都会搭配连接回收机制，连接建立后如果连接长时间空闲，就会被回收。连接回收的策略一般是判断这段时间内是否有发送数据的行为，如果没有就判断是空闲，然后执行回收。&lt;/p>
&lt;p>因为单个 TCP 连接发送性能存在上限，就需要在客户端启动多个生产者，提高并发读写的能力。一般情况下，每个生产者会有一个唯一的 ID 或唯一标识来标识客户端，比如 ProduceID 或客户端的 IP+Port。&lt;/p>
&lt;blockquote>
&lt;p>单个 TCP 的瓶颈和很多因素有关，比如网路带宽、网络延迟、客户端请求端的 socketbuff 的配置、TCP 窗口大小、发送速率导致本地数据反压堆积、服务端请求队列的堆积情况、收包和回包的速度等等。&lt;/p>
&lt;/blockquote>
&lt;p>接下来继续看看客户端和服务端之间的心跳检测。&lt;/p>
&lt;h3 id="心跳检测">心跳检测&lt;/h3>
&lt;p>心跳检测是客户端和服务端之间保活的一种机制，检测服务端或者客户端的一方不可用时，另一方可以及时回收资源，避免资源浪费。一般都是通过 ping-pong 的方式来发起探测。之前的内容有提到过，消息队列一般都是基于 TCP 协议通信的。所以客户端和服务端之间的心跳机制的实现，一般有基于 TCP 的 KeepAlive 保活机制和应用层主动探测两种形式。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/da51f810a77f65197bb33a1fe7e3914c764d7f47/content/imag/tech/bigdata/mq/06_producer_heart.svg" alt="producer-heart">&lt;/p>
&lt;p>&lt;strong>基于 TCP 的 KeepAlive 保活机制&lt;/strong>：是 TCP/IP协议层内置的功能，需要手动打开 TCP 的 KeepAlive 功能。通过这种方案实现心跳检测，优点是简单，缺点是 KeepAlive 实现是在服务器侧，需要 Server 主动发送检测包，此时如果客户端异常，可能出现很多不可用的 TCP 连接。这种连接会占用服务器内存资源，导致服务器端的性能下降。&lt;/p>
&lt;p>&lt;strong>应用层主动探测&lt;/strong>：一般是 Client 向 Server 发起的，主要解决灵活性和 TCP KeepAlive 的缺陷。探测流程一般是客户端定时发送保活心跳，当服务端连续几次没收到请求，就断开连接。这样做的好处是，可以将压力分担到各个客户端，避免服务端的过载。&lt;/p>
&lt;h3 id="错误处理">错误处理&lt;/h3>
&lt;p>从请求的角度，有些错误是重试可以恢复的，比如连接断开、Leader 切换、发送偶尔超时和服务端某些异常等；有些错误是不可恢复的，比如 Topic / 分区不存在、服务端 Broker 不存在、集群和 Broker 长时间无响应等。所以，在客户端的处理中，也会将错误分为可重试错误和不可重试错误两类。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/da51f810a77f65197bb33a1fe7e3914c764d7f47/content/imag/tech/bigdata/mq/06_producer_exception.svg" alt="producer-exception">&lt;/p>
&lt;p>因为网络环境、架构部署的复杂性，集群可能出现短暂网络抖动、Leader 切换等异常，可重试错误就是这类通过一次或多次重试可能恢复的异常；不可重试的错误，就是不管如何重试都无法恢复的异常。&lt;/p>
&lt;p>虽然实现思路很直接、很简单，但在客服端 SDK 的实现过程中，错误处理是一个包含很多细节的工作，一般需要考虑下面几个常见的点：&lt;/p>
&lt;ul>
&lt;li>如何定义可恢复错误和不可恢复错误；&lt;/li>
&lt;li>完整的错误码的定义和枚举，如何定义一个好的错误码从而提高排查问题的效率；&lt;/li>
&lt;li>错误后重试的代码实现方式是否合理高效；&lt;/li>
&lt;li>判断哪些情况需要停止客户端，向上抛出异常，以免一些错误信息一直在 SDK 内空转，提高上层感知异常和排查异常的难度；&lt;/li>
&lt;li>日志信息打印 debug、info 以及 error 日志时，是否包含了完整的内容。&lt;/li>
&lt;/ul>
&lt;p>发生错误后，客户端一般会提供重试策略，接下来一起看看重试机制的实现，&lt;/p>
&lt;h3 id="重试机制">重试机制&lt;/h3>
&lt;p>重试策略一般会支持重试次数和退避时间的概念。当消息失败，超过设置的退避时间后，会继续重试，当超过重试次数后，就会抛出消息或者将消息投递到配置好的重试队列中。&lt;/p>
&lt;p>退避时间是可以配置的，比如 1s、10s 或者 60s 等。当出现错误时，就会按照退避策略进行退避，再尝试写入。一般情况下，重试是有次数上限的，当然如果想的话也也可以配置无限重试。&lt;/p>
&lt;p>退避策略影响的是重试的成功率，因为网络抖动一般来说是 ms 级，某些严重的情况下可能会抖动十几秒。此时，如果退避策略设置的太短，在退避策略和重试次数用完后，可能消息还没生产成功；反过来，如果退避时间设置太长，可能导致客户端发送堵塞消息堆积。所以消息队列生产者的重试次数和退避策略的设置都是比较讲究的，需要结合业务的场景仔细设计。&lt;/p>
&lt;p>另外，客户端为了满足安全传输、性能和功能方面的需求，客户端都会支持传输加密、压缩、事务、幂等等功能。&lt;/p>
&lt;h2 id="生产相关基础功能">生产相关基础功能&lt;/h2>
&lt;h3 id="客户端寻址机制">客户端寻址机制&lt;/h3>
&lt;p>MQ 作为一个分布式系统，分区会分布在集群的不同节点上。所以从客户端的角度看，往服务端写入数据的时候，服务端有那么多台节点，请求该发送給台节点呢？？&lt;/p>
&lt;p>看见这个问题，可能大部分开发者都会觉得这并不是什么难题，类似我们发送 HTTP 请求，手动指定目标 Broker 的 IP 就行了。就是说在生产者写数据到 Broker 的时候，在代码里面手动指定分区对应的对端的 Broker 地址，然后将数据写到目标 Broker。&lt;/p>
&lt;p>这个思路没问题，但是我们手动指定对端 Broker 地址的时候，怎么知道这个分区在这台 Broker 上的对应关系存在哪里呢？？为了解决这个问题，就从而提出了 Metadata 寻址机制和服务端内部转发两个思路。&lt;/p>
&lt;h4 id="1metadata-寻址机制">1.Metadata 寻址机制&lt;/h4>
&lt;p>服务端会提供一个获取全量的 Metadata 的接口，客户端在启动时，首先通过接口拿到集群所有的元数据信息，本地缓存这部分数据信息。然后，客户端发送数据的时候，会根据元数据的内容，得知服务端的地址是什么，要发送的分区在哪台节点上。最后根据这两部分信息，将数据发送到服务端。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/da51f810a77f65197bb33a1fe7e3914c764d7f47/content/imag/tech/bigdata/mq/06_producer_seek01.svg" alt="producer-seek-metastore">&lt;/p>
&lt;p>消息队列的元数据是指 Topic、分区、Group、节点、配置等集群维度的信息。比如 Topic 有几个分区，分区的 Leader 和 Follwer 在哪些节点上，节点的 IP 和端口是什么，有哪些 Group 等等。&lt;/p>
&lt;p>在 Metadata 寻址机制中，元数据信息主要包括 Topic 及其对应的分区信息和 Node 信息两部分。可以看一下 Kafka 的元数据信息结构；&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-txt" data-lang="txt">&lt;span class="line">&lt;span class="cl">主题分区元数据：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;test1&amp;#34;: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Topic&amp;#34;: &amp;#34;test1&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Partitions&amp;#34;: [
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;ID&amp;#34;: 0,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Error&amp;#34;: {},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Leader&amp;#34;: 101194,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Replicas&amp;#34;: [
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 101194,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 101193
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Isrs&amp;#34;: [
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 101194,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 101193
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Error&amp;#34;: {}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">节点元数据：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;ID&amp;#34;: 101195,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Host&amp;#34;: &amp;#34;9.130.62.0&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Port&amp;#34;: 6097
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;ID&amp;#34;: 101194,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Host&amp;#34;: &amp;#34;9.130.62.1&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Port&amp;#34;: 6096
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;ID&amp;#34;: 101193,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Host&amp;#34;: &amp;#34;9.130.62.2&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;Port&amp;#34;: 6095
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>客户端一般通过&lt;strong>定期全量更新 Metadata 信息和请求报错时更新元数据信息&lt;/strong>两种方式，来保证客户端的元数据信息是最新的。目前 Kafka 也是用的这个方案。&lt;/p>
&lt;h4 id="2-服务端内部转发机制">2. 服务端内部转发机制&lt;/h4>
&lt;p>另外一种服务端内部转发机制，客户端不需要经过寻址的过程，写入的时候是随机把数据写入到服务端任意一台 Broker。具体思路是服务端的每一台 Broker 会缓存所有节点的元数据信息，生产者将数据发送给 Broker 后，Broker 如果判断分区不在当前节点上，会先找到这个分区在哪个节点上，然后把数据转发到目标节点。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/da51f810a77f65197bb33a1fe7e3914c764d7f47/content/imag/tech/bigdata/mq/06_producer_seek02.svg" alt="producer-seek-resend">&lt;/p>
&lt;p>这么做的好处是，分区寻址在服务端完成，客户端的实现成本比较低。但是生产流程多了一跳，耗时增加了。另外服务端因为转发多了一跳，会导致服务端的资源损耗多一倍，比如 CPU、内存、网卡，在大流量的场景下，这种损耗会导致集群负载变高，从而导致集群整体性能降低。所以这种方案不适合大流量、高吞吐的消息队列。&lt;/p>
&lt;p>解决了请求要发送給哪个节点，下面就要思考消息数据要写入到哪个分区呢。&lt;/p>
&lt;h3 id="生产分区分配策略">生产分区分配策略&lt;/h3>
&lt;p>我们知道，数据可以直接写入分区或者写入对应的 Topic。写入 Topic 时，最终数据还是要写入到某个分区。这个数据选择写入到哪个分区的过程，就是生产数据的分区分配过程。过程中的分配策略就是生产分区分配策略。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/da51f810a77f65197bb33a1fe7e3914c764d7f47/content/imag/tech/bigdata/mq/06_producer_partition.svg" alt="producer-partition">&lt;/p>
&lt;p>一般情况下，消息队列默认支持轮询、按 Key Hash、手动指定和自定义分区分配这四种分区分配策略。&lt;/p>
&lt;p>&lt;strong>轮询&lt;/strong>是所有消息队列的默认选项。消息通过轮询的方式依次写到各个分区中，这样可以保证每个分区的数据量是一样的，不会出现分区数据倾斜。&lt;/p>
&lt;blockquote>
&lt;p>分区数据倾斜是指一个 Topic 的每个分区的存储的数据量不一样，有的分区数据量大，有的小，从而导致硬件的负载不均，集群性能出现问题。&lt;/p>
&lt;/blockquote>
&lt;p>既然能解决数据倾斜，那是不是使用轮询就是最优解了呢？？答案是否定的，因为如果我们需要保证数据的写入是有序的，轮询就满足不了。因为在消费模型中，每个分区的消费是相互独立的，如果数据依次写入多个分区，在消费的时候就无法保持顺序。所以若要想数据有序，就需要保证 Topic 只有一个分区。这也是另外两种分配策略的思路。&lt;/p>
&lt;p>&lt;strong>按 Key Hash&lt;/strong> 是指根据消息的 Key 算出一个 Hash 值，然后与 Topic 分区数取余数，算出一个分区号，将数据写入到这个分区中。公式参考：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="n">partitionSeq&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">hash&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">partitionNum&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这种方案的好处是可以根据 Key 来保证数据的分区有序。比如某个用户的访问轨迹，以客户的 AppID 为 Key，按 Key Hash 存储，就可以确保客户维度的数据分区有序。缺点是分区数量不能变化，因为变化后 Hash 值就会变，导致消息乱序。并且因为每个 Key 的数据量不一样，容易导致数据倾斜。&lt;/p>
&lt;p>&lt;strong>手动指定&lt;/strong>很好理解，就是在生产数据的时候，手动指定数据写入哪个分区。这种方案的好处就是灵活，用户可以在代码逻辑中根据自己的需要，选择合适的分区，缺点是业务需要感知分区的数量和变化，代码实现相对复杂。&lt;/p>
&lt;p>除了这三种默认策略，消息队列也支持&lt;strong>自定义分区分配策略&lt;/strong>，让用户灵活使用。通常会在内核提供 interface 机制，用户如果需要指定自定义分区的分区分配策略，可以实现对应的接口，然后配置分区分配策略。比如 Kafka 可以通过实现 &lt;code>org.apache.kafka.clients.producer.Partitioner&lt;/code> 接口实现自定义分区策略。&lt;/p>
&lt;h3 id="批量语义">批量语义&lt;/h3>
&lt;p>为了提高写入性能，有的生产者客户端会提供批量（Batch）写入的语义。客户端支持批量写入数据的前提是，需要在协议层支持批量的语义。否则就只能在业务中自定义将多条消息组成一条消息。&lt;/p>
&lt;p>批量发送的实现思路一般是在客户端内存中维护一个队列，数据写入的时候，先将其写入到这个内存队列，然后通过某个策略从内存队列读取数据，发送到服务端。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/da51f810a77f65197bb33a1fe7e3914c764d7f47/content/imag/tech/bigdata/mq/06_producer_batch.svg" alt="producer-batch">&lt;/p>
&lt;p>批量发送数据的策略和存储模块的刷盘策略很像，都是根据数据条数或时间聚合后，汇总发送到服务端，一般是满足时间或者条数的条件后触发发送操作，也会有立即发送的配置项。&lt;/p>
&lt;p>Kafka 是按照时间的策略批量发送的，提供了 linger.ms、max.request.size、batch.size 三个参数，来控制数据批量发送。&lt;/p>
&lt;blockquote>
&lt;p>linger.ms：设置消息延迟发送的时间，这样可以等待更多的消息组成 Batch 发送。默认为 0 表示立即发送。&lt;/p>
&lt;p>max.request.size：生产者能够发送的请求包大小上限，默认为 1MB。&lt;/p>
&lt;p>batch.size：生产者会尝试将业务发送到相同的 Partition 的消息合包后再进行发送，它设置了合包的大小上限。&lt;/p>
&lt;/blockquote>
&lt;p>为了支持对于性能和可靠性有不同需求的业务场景，客户端一般会支持多种数据发送方式。&lt;/p>
&lt;h3 id="数据发送方式">数据发送方式&lt;/h3>
&lt;p>消息队列一般也会提供同步发送、异步发送和发送即忘三种形式。&lt;/p>
&lt;p>同步和异步更多是语言语法的实现，同步发送主要解决数据发送的即时性和顺序性，一步发送主要考虑性能。下面，我们来重点看一下发送即忘（这个不太好理解）。&lt;/p>
&lt;p>发送即忘指消息发送后不关心请求返回的结果，立即发送下一跳。这种方式因为不用关心发送结果，发送性能会提升很多。缺点是当数据发送失败时无法感知，可能会有数据丢失的情况，所以通常适用在发送不重要的日志等场景。Kafka 提供了 ack = 0 来支持这种模式。&lt;/p>
&lt;p>讲完了发送相关的功能设计，接下来我们看一下管控操作在客户端中的实现方式。&lt;/p>
&lt;h3 id="集群管控操作">集群管控操作&lt;/h3>
&lt;p>集群管控操作一般是用来完成资源的创建、查询、修改和删除等集群管理动作。资源主要包括主题、分区、配置以及消费分组等等。&lt;/p>
&lt;p>命令行工具是最基础的支持方式。如下图所示，它的底层主要通过包装客户端 SDK 和服务端的相关功能接口进行交互。程序编码上一般由&lt;strong>命令行、参数包装和底层 SDK 调用&lt;/strong>三部分组成。主要流程是接收参数、处理参数和调用 SDK 等相关操作。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/da51f810a77f65197bb33a1fe7e3914c764d7f47/content/imag/tech/bigdata/mq/06_producer_cluster.svg" alt="producer-cluster">&lt;/p>
&lt;p>有的消息队列也会支持 HTTP 接口形式的管控操作。好处是因为 HTTP 协议的通用性，业务可以从各个环节发起管控的调用，不是强制使用 admin SDK。另外客户端封装 HTTP 接口实现命令行工具的成本也比较低。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>消息队列生产者客户端的设计，主要关注下面三个部分：&lt;/p>
&lt;ul>
&lt;li>网络模块的开发和管理。这部分是为了完成和服务端的通信，比如请求和返回的构建、心跳检测、错误处理和重试机制等；&lt;/li>
&lt;li>根据服务端提供的各个接口的协议结构，构建请求，完成序列化和反序列化后，通过网络模块发起请求并获得返回；&lt;/li>
&lt;li>在前面两步的基础上，添加各个业务层面的功能，比如生产、消费、事务、幂等和 SSL 这类。&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/da51f810a77f65197bb33a1fe7e3914c764d7f47/content/imag/tech/bigdata/mq/06_producer_summary.svg" alt="producer-summary">&lt;/p>
&lt;p>客户端和服务端交互的过程中，一般要经过元数据寻址，以正确找到分区所在的 Broker。如果我们想避免客户端寻址，只能在服务端内进行转发，但有性能和资源的损耗。所以在主打吞吐的消息队列组件中，转发的方案用的很少。&lt;/p>
&lt;p>从生产者的的角度来看，需要重点关注分区分配策略、批量语义和发送方式三个方面。请求内容构建和序列化属于协议设计的内容，主要取决于协议的具体设计和序列化 / 反序列化框架的选择。&lt;/p></content></entry><entry><title type="text">MQ005——元数据和消息数据的存储设计</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/bigdata/bigdata_mq005/"/><id>https://quakewang.github.io/tech/bigdata/bigdata_mq005/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-08-14T09:19:18+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="如何设计元数据和消息数据的存储模块">如何设计元数据和消息数据的存储模块？？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>存储模块作为 MQ 高吞吐、低延时、高可靠性的基础保证，可以说是最核心的模块。从技术架构的来看，存储模块主要包含&lt;strong>功能实现&lt;/strong>和&lt;strong>性能优化&lt;/strong>两个方面，这篇 blog 就重点来看一下其是如何实现的。&lt;/p>……</summary><content type="html">&lt;h1 id="如何设计元数据和消息数据的存储模块">如何设计元数据和消息数据的存储模块？？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>存储模块作为 MQ 高吞吐、低延时、高可靠性的基础保证，可以说是最核心的模块。从技术架构的来看，存储模块主要包含&lt;strong>功能实现&lt;/strong>和&lt;strong>性能优化&lt;/strong>两个方面，这篇 blog 就重点来看一下其是如何实现的。&lt;/p>
&lt;p>MQ 的存储模块的主流程是数据的写入、存储、读取、过期。写入和持久化存储是基本功能，但因为消息队列独有的产品特性，主要被用来当缓冲分发，它的数据存储是临时的，数据持久化存储后，在一定的时间或操作后，需要能自动过期删除。&lt;/p>
&lt;p>而且 MQ 中的数据一般分为&lt;strong>元数据&lt;/strong>和&lt;strong>消息数据&lt;/strong>。元数据指的是 Topic、Group、User、ACL 和 Config 等集群维度的资源数据信息，消息数据是指客户端写入的用户的业务数据。&lt;/p>
&lt;h2 id="元数据信息的存储">元数据信息的存储&lt;/h2>
&lt;p>元数据信息的特点是数据量比较小，不会经常读写，但是需要保证数据的强一致和高可靠，不允许出现数据的丢失。同时，元数据信息一般需要通知到所有的 Broker 节点，Broker 会根据元数据信息执行具体的逻辑。比如创建 Topic 并生成元数据后，就需要通知对应的 Broker 执行创建分区、创建目录等操作。&lt;/p>
&lt;p>所以元数据信息的存储，一般有两个思路：&lt;/p>
&lt;ul>
&lt;li>基于第三方组件来实现元数据的存储；&lt;/li>
&lt;li>在集群内部实现元数据的存储。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>基于第三方组件来实现元数据的存储是目前业界的主流选择&lt;/strong>。比如 Kafka Zookeeper 版本、RocketMQ 和 Pulsar 用的都是这个思路。其中 Kafka 和 Pulsar 的元数据存储在 Zookeeper 中。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/a602648653a511924c6608e2529299122d829d0a/content/imag/tech/bigdata/mq/05_storage-metadata01.svg" alt="metadata01">&lt;/p>
&lt;p>这个方案最大的优点是集成方便，开发成本低，能满足消息队列功能层面的基本要求，因为可以直接复用第三方组件已经实现的一致性存储、高性能的读写和存储、Hook 机制等能力，而且在后续集群构建规划的过程中也可以继续复用这个组件，能极大程度降低开发难度和工作成本。&lt;/p>
&lt;p>但凡事都有利弊。其缺点也很明显，那就是引入第三方组件会增加集群系统部署和运维的成本，而且第三方组件自身的稳定性问题也会增加系统风险，第三方组件和多台 Broker 之间可能会出现数据信息不一致的情况，导致读写异常。&lt;/p>
&lt;p>&lt;strong>另外一种思路，集群内部实现元数据的存储是指在集群内部完成元数据的存储和分发&lt;/strong>。也就是在集群内部实现类似第三方组件一样的元数据服务，比如 Raft 协议实现内部的元数据存储模块或依赖一些内置的数据库。目前 Kafka 去 Zookeeper 版本用的就是这个思路。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/a602648653a511924c6608e2529299122d829d0a/content/imag/tech/bigdata/mq/05_storage-metadata02.svg" alt="metadata02">&lt;/p>
&lt;p>这个方案的优缺点刚好与第一个相反。优点是部署和运维成本低，不会因为依赖第三方服务导致稳定性问题，也不会有数据不一致的问题。但是缺点是开发成本高，前期要投入大量的开发成本。&lt;/p>
&lt;h2 id="消息数据的存储">消息数据的存储&lt;/h2>
&lt;p>与元数据的存储相比，消息数据的存储要复杂一点。一般情况下，MQ 的存储主要是指消息数据的存储，分为存储结构、数据分段、数据存储格式和数据清理四个部分。&lt;/p>
&lt;h3 id="数据存储结构设计">数据存储结构设计&lt;/h3>
&lt;p>我们先看数据存储目录结构设计。在消息队列中，与存储有关的主要是 Topic 和分区两个维度。用户可以将数据写入 Topic 或直接写入到分区。&lt;/p>
&lt;p>不过如果写入 Topic，数据也是分发到多个分区去存储的。所以从实际数据存储的角度来看，&lt;strong>Topic 和 Group 不承担数据存储功能，承担的是逻辑组织的功能，实际的数据存储是在分区维度完成的&lt;/strong>。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/a602648653a511924c6608e2529299122d829d0a/content/imag/tech/bigdata/mq/05_storage-messagedata01.svg" alt="messagedata01">&lt;/p>
&lt;p>从架构角度上看，数据的落盘也有两种思路：&lt;/p>
&lt;ul>
&lt;li>每个分区单独一个存储“文件”&lt;/li>
&lt;li>每个节点上所有分区的数据都存储在同一个“文件”&lt;/li>
&lt;/ul>
&lt;p>需要注意的是，这里的“文件”是一个虚指，即表示所有分区的数据是存储在一起，还是每个分区的数据分开存储的意思。在实际的存储中，这个“文件”通常以目录的形式存在，目录中会有多个分段文件。&lt;/p>
&lt;p>先来看第一个思路，每个分区对应一个文件的形式去存储数据。具体实现时，每个分区上的数据顺序写到同一个磁盘文件，数据的存储是连续的。因为消息队列在大部分情况下的读写是有序的，所以这种机制&lt;strong>在读写性能上的表现是最高的&lt;/strong>。&lt;/p>
&lt;p>但如果分区太多，会占用太多的系统 FD 资源，极端情况下有可能把节点的 FD 资源耗完，并且硬盘层面会出现大量的随机读写情况，导致写入的性能下降很多，另外管理起来也相对复杂。目前 Kafka 在存储数据的组织上用的就是这种思路。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/a602648653a511924c6608e2529299122d829d0a/content/imag/tech/bigdata/mq/05_storage-messagedata02.svg" alt="messagedata02">&lt;/p>
&lt;p>具体的磁盘的组织结构一般有“目录+分区二级结构”和“目录+分区一级结构”地两种形式。不过从技术上来看，二者并没有太大的优劣区别。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">目录+分区二级结构：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── topic1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── partrt0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── &lt;span class="m">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── topic2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">目录+分区一级结构：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── topic1-0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── topic1-1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── topic1-2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── topic2-0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── topic2-1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── topic2-2
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>再来看第二种思路，每个节点上所有分区的数据都存储在同一文件中，需要为每个分区维一个对应的索引文件，索引文件里会记录每条消息在 File 里面的位置信息，以便快速定位到具体的消息内容。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/a602648653a511924c6608e2529299122d829d0a/content/imag/tech/bigdata/mq/05_storage-messagedata03.svg" alt="messagedata03">&lt;/p>
&lt;p>因为所有文件都在一份文件上，管理简单，也不会占用过多的系统 FD 资源，单机上的数据写入都是顺序的，写入的性能会很高。缺点是同一个分区的数据一般会在文件中的不同位置，或者不同的文件段中，无法利用到顺序读的优势，读取的性能会收到影响，但是随着 SSD 技术的发展，随机读写的性能也越来越高。最简单的体现就是固态越来越便宜咯。如果使用 SSD 或高性能 SSD，一定程度上可以缓解随机读写的性能损耗。&lt;/p>
&lt;p>那么该如何选择呢？&lt;strong>核心考虑是对读写的性能要求&lt;/strong>。&lt;/p>
&lt;ul>
&lt;li>第一种思路，单个文件读和写都是顺序的，性能最高。但是当文件很多且都有读写的场景下，硬盘层面就会退化为随机读写，性能会严重下降；&lt;/li>
&lt;li>第二种思路，因为只有一个文件，不存在文件过多的情况，写入层面一直都会是顺序的，性能一直很高。但是在消费数据的时候，因为多个分区数据存储在同一个文件中，同一个分区的数据在底层存储上是不连续的，硬盘层面会出现随机读的情况，导致读取的性能降低。&lt;/li>
&lt;/ul>
&lt;p>不过随机读写带来的性能问题，可以通过给底层配备高性能的硬件来缓解。所以当前比较多的 MQ 选用的是第二种方案，但是 Kafka 为了保证更高的吞吐性能，选用的是第一种方案。&lt;/p>
&lt;p>但是不管是方案一还是方案二，在数据存储过程中，如果单个文件过大，在文件加载、写入和检索的时候，性能就会有问题，并且 MQ 有自动过期机制，如果单个文件过大，数据清理时会很麻烦，效率很低。所以，我们的消息数据都会分段存储。&lt;/p>
&lt;h3 id="消息数据的分段实现">消息数据的分段实现&lt;/h3>
&lt;p>数据分段的规则一般是根据大小来进行的，比如默认 1G 一个文件，同时会支持配置项调整分段数据的大小。当数据段达到了规定的大小后，就会创建一个新的文件来保存数据。如果进行了分段，消息数据可能分布在不同的文件中。所以我们在读取数据的时候，就需要先定义消息数据在哪个文件中。为了满足这个需求，技术上一般有&lt;strong>根据偏移量定位或根据索引定位&lt;/strong>两种思路。&lt;/p>
&lt;p>根据偏移量（Offset）来定位消息在哪个分段文件中，是指通过记录每个数据段文件的起始偏移量、中止偏移量、消息的偏移量信息，来快速定位消息在哪个文件。&lt;/p>
&lt;p>当消息数据存储时，通常会用一个自增的数值型数据（比如 Log）来表示这条数据在分区或 commitlog 中的位置，这个值就是消息的偏移量。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/a602648653a511924c6608e2529299122d829d0a/content/imag/tech/bigdata/mq/05_storage-messagedata04.svg" alt="messagedata04">&lt;/p>
&lt;p>在实际的编码过程中，记录文件的起始偏移量一般有两种思路：单独记录每个数据段的起始和结束偏移量，在文件名称中携带起始偏移量的信息。因为数据是顺序存储的，每个文件都记录了其对应的起始偏移量，那么下一个文件的起始偏移量就是上一个文件的结束偏移量。&lt;/p>
&lt;p>如果用索引定位，会直接存储消息对应的文件信息，而不是通过偏移量来定位到具体的文件。具体是通过维护一个单独的索引文件，记录消息在哪个文件和文件的哪个位置。读取消息的时候，先根据消息 ID 找到存储的信息，然后找到对应的文件和位置，读取数据。RocketMQ 用的就是这个思路。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/a602648653a511924c6608e2529299122d829d0a/content/imag/tech/bigdata/mq/05_storage-messagedata05.svg" alt="messagedata05">&lt;/p>
&lt;p>这两种方案所面临的的场景不一样。根据偏移量定位数据，通常用在每个分区各自存储一份文件的场景；根据索引定位数据，通常用在所有分区的数据存储在同一份文件的场景。因为前一种场景，每一分数据都属于同一个分区，那么通过位点来二分查找数据的效率是最高的。第二种场景，这一份数据属于多个不同分区，再使用二分查找就不是那么明智咯，可以选择使用哈希查找。&lt;/p>
&lt;h3 id="数据消息存储格式">数据消息存储格式&lt;/h3>
&lt;p>消息数据存储格式一般包含消息写入文件的格式和消息内容的格式两个方面。&lt;/p>
&lt;p>消息写入文件的格式指消息是以什么格式写入到文件中的，比如 JSON 字符串或二进制。从性能和空间冗余的角度来看，消息队列中的数据基本都是以二进制的格式写入到文件的。这部分二进制数据，我们不能直接用 vim/cat 等命令查看，需要用专门的工具读取，并解析对应的格式。&lt;/p>
&lt;p>消息内容的格式是指写入到文件中的数据都包含哪些信息。对于一个成熟的消息队列来说，消息内容格式不仅关系功能维度的扩展，还牵涉性能维度的优化。&lt;/p>
&lt;p>如果消息格式设计的不够精简，功能和性能都会大打折扣。比如冗余字段会增加分区的磁盘占用空间，使存储和网络开销变大，性能也会下降。如果缺少字段，则可能无法满足一些功能上的需求，导致无法实现某些功能，又或者是实现某些功能的成本较高。所以在数据的存储格式设计方面，内容的格式需要尽量完整且不要有太多冗余。&lt;/p>
&lt;p>这么说可能会感觉比较抽象，我们分析一下 Kafka 的消息内容格式设计来直观的感受一下。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">字段名&lt;/th>
&lt;th style="text-align: left">含义&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">baseOffset&lt;/td>
&lt;td style="text-align: left">用在 Batch 中，该批次消息的起始 offset&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">lastOffset&lt;/td>
&lt;td style="text-align: left">用在 Batch 中，该批次消息的结束 offset&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">count&lt;/td>
&lt;td style="text-align: left">表示该行记录包含了多少条信息&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">baseSequence&lt;/td>
&lt;td style="text-align: left">用在 Batch 中，起始序号，用来支持幂等和事务&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">lastSequence&lt;/td>
&lt;td style="text-align: left">用在 Batch 中，结束序号，用来支持幂等和事务&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">producerId&lt;/td>
&lt;td style="text-align: left">PID，用来支持幂等和事务&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">producerEpoch&lt;/td>
&lt;td style="text-align: left">分区 leader 纪元，可以看作分区 leader 的版本号或更新次数&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">isTransactional&lt;/td>
&lt;td style="text-align: left">是否事务信息&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">isControl&lt;/td>
&lt;td style="text-align: left">-&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">position&lt;/td>
&lt;td style="text-align: left">该行记录在本文件的偏移量&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">size&lt;/td>
&lt;td style="text-align: left">该行记录的总大小&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">magic&lt;/td>
&lt;td style="text-align: left">当前数据存储格式的版本，kafka 有 v0、v1 和 v2 三种格式&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">compresscodec&lt;/td>
&lt;td style="text-align: left">压缩格式，NONE 表示不压缩&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">crc&lt;/td>
&lt;td style="text-align: left">CRC 校验码，用来校验数据在传输过程中的准确性&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">isvalid&lt;/td>
&lt;td style="text-align: left">表示数据是否可用，比如是否被删除&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">offset&lt;/td>
&lt;td style="text-align: left">该行记录的在这个分区的偏移量&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">LogAppendTime&lt;/td>
&lt;td style="text-align: left">数据写入到文件的时间&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">keysize&lt;/td>
&lt;td style="text-align: left">key 长度&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">valuesize&lt;/td>
&lt;td style="text-align: left">payload 长度&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">sequence&lt;/td>
&lt;td style="text-align: left">序号，用来支持幂等和事务&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">headerKeys&lt;/td>
&lt;td style="text-align: left">消息 header 的内容&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">key&lt;/td>
&lt;td style="text-align: left">消息的 key&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">payload&lt;/td>
&lt;td style="text-align: left">消息的内容&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>可以看到，Kafka 的消息内容包含了业务会感知到的消息的 Header、Key 和 Value，还包含了时间戳、偏移量、协议版本、数据长度和大小、校验码等基础信息，最后还包含了压缩、事务、幂等 Kafka 业务相关的信息。&lt;/p>
&lt;p>需要注意的是，因为 kafka 支持 Batch 特性，所以消息格式中还包含 base 和 last 等 Batch 相关信息。&lt;/p>
&lt;h3 id="消息数据清理机制">消息数据清理机制&lt;/h3>
&lt;p>前文提到过，消息队列的数据在持久化存储后，需要在一定策略后自动过期删除。那么数据过期机制是如何实现的呢？&lt;/p>
&lt;p>消息队列中的数据最终都会删除，时间周期短的话几个小时、甚至几分钟，正常情况一天、三天、七天，长的话可能数个月，基本很少有场景需要再消息队列中存储一年的数据。&lt;/p>
&lt;p>消息队列的数据过期机制一般有手动删除和自动删除两种形式，从实现上看主要有三种思路：&lt;/p>
&lt;ul>
&lt;li>消费完成执行 ACK 删除数据&lt;/li>
&lt;li>根据时间和保留大小删除&lt;/li>
&lt;li>ACK 机制和过期机制相结合&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>消费完成执行 ACK 删除数据&lt;/strong>，技术上的实现思路一般是：当客户端完成消费数据后，回调客户端的 ACK 接口，告诉服务端数据已经消费成功，服务端就会标记删除该行数据，以确保消息不会被重复消费。ACK 的请求一般会有单条消息 ACK 和批量消息 ACK 两种形式。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/a602648653a511924c6608e2529299122d829d0a/content/imag/tech/bigdata/mq/05_storage-messagedata06.svg" alt="messagedata06">&lt;/p>
&lt;p>因为消息队列的 ACK 一般是顺序的，如果前一条消息无法被正确处理并 ACK，就无法消费下一条数据，导致消费卡住。此时就需要死信队列的功能，把这条数据先写入到死信队列，等待后续的结果。然后 ACK 这条消息，确保消费正确进行。&lt;/p>
&lt;p>这种方案，优点是不会出现重复消费，一条消息只会被消费一次。缺点是 ACK 成功后消息被删除，无法满足需要消息重放的场景。&lt;/p>
&lt;p>&lt;strong>根据时间和保留大小删除指消息在被消费后不会被删除&lt;/strong>，只会通过提交消费位点的形式标记消费进度。&lt;/p>
&lt;p>实现思路一般是服务端提供偏移量提交的接口，当客户端消费成功数据后，客户端会回调偏移量接口，告诉服务端这个偏移量的数据已经消费成功了，让服务端吧偏移量记录起来。然后服务端会根据消息保留的策略，比如保留时间或保留大小来清理数据。一般通过一个常驻的异步线程来清理数据。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/a602648653a511924c6608e2529299122d829d0a/content/imag/tech/bigdata/mq/05_storage-messagedata07.svg" alt="messagedata07">&lt;/p>
&lt;p>这个方案，一条消息可以重复消费多次。不管有没有被成功消费，消息都会根据配置的时间规则或大小规则进行删除。优点是消息可以多次重放，适用于需要多次进行重放的场景。缺点是在某些情况下（比如客户端使用不当）会出现大量的重复消费。&lt;/p>
&lt;p>结合前两个方案，就有了 &lt;strong>ACK 机制和过期机制相结合&lt;/strong>的方案。实现的核心逻辑和方案二比较类似，但保留了 ACK 的概念，不过 ACK 是相对于 Group 概念的。&lt;/p>
&lt;p>当消息完成后，在 Group 维度 ACK 消息，此时消息不会被删除，只是这个 Group 也不会再重复消费到这个消息，而新的 Group 可以重新消费订阅这些数据。所以在 Group 维度避免了重复消费的情况，也可以运行重复订阅。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/a602648653a511924c6608e2529299122d829d0a/content/imag/tech/bigdata/mq/05_storage-messagedata08.svg" alt="messagedata08">&lt;/p>
&lt;p>前面我们虽然反复提到“删除”，但数据实际怎么删除也有讲究。我们知道消息数据是顺序存储在文件中的，会有很多分段数据，一个文件可能会有很多行数据。那么在 ACK 或者数据删除的时候，一个文件中可能既存在可删除数据，也存在不可删除数据。如果我们每次都立即删除数据，需要不断执行“读取文件、找到记录、删除记录、写入文件”的过程，即使批量操作，降低频率，还是得不断地重复这个过程，会导致性能明显下降。&lt;/p>
&lt;p>当前主流的思路是延时删除，以段数据为单位清理，降低频繁修改文件内容和频繁随机读写文件的操作。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/a602648653a511924c6608e2529299122d829d0a/content/imag/tech/bigdata/mq/05_storage-messagedata09.svg" alt="messagedata09">&lt;/p>
&lt;p>只有该段里面的数据都允许删除后，才会把数据删除。而删除该段数据中的某条数据时，会先对数据进行标记删除，比如在内存或 Backlog 文件中记录待删除数据，然后在消费的时候感知这个标记，这样就不会重复消费这些数据。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>消息队列的存储分为元数据存储和消息数据存储两方面。&lt;/p>
&lt;p>元数据的存储主要依赖第三方组件实现，比如 ZooKeeper、etcd 或者自研的简单元数据存储服务等等。在成熟的消息队列架构中，基于简化架构和提升稳定性的考虑，都会考虑在集群内部完成元数据的存储和管理。&lt;/p>
&lt;p>消息数据的存储在功能层面包含数据存储结构设计、数据分段存储、数据存储格式、数据清理机制四个方面。&lt;/p>
&lt;p>消息数据的存储主要包含 Topic 和分区两个维度。Topic 起逻辑组织作用，实际的数据存储是在分区维度完成的。所以在数据存储目录结构上，我们都以分区为最小粒度去设计，至于选择每个分区单独一个存储文件，还是将每个节点上所有分区的数据都存储在同一个文件，方案各有优劣，你可以根据实际情况去选择。&lt;/p>
&lt;p>因为大文件存在性能和资源占用、数据清理成本等问题，一般情况下，我们都需要对数据文件进行分段处理，分段的策略一般都是按照文件大小进行的。&lt;/p>
&lt;p>数据存储格式可以分为基础信息和业务信息两个维度，数据格式需要遵循极简原则，以达到性能和成本的最优。数据的过期策略一般有三种，ACK 删除、根据时间和保留大小删除数据、两者结合。目前业界的实现比较多样，从选择上来看，两者结合的方案更合理。&lt;/p></content></entry><entry><title type="text">MQ004——网络模块</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/bigdata/bigdata_mq004/"/><id>https://quakewang.github.io/tech/bigdata/bigdata_mq004/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-08-04T10:15:04+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="如何设计一个高性能的网络模块">如何设计一个高性能的网络模块？？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>这篇博客我们来扒一下有关消息引擎系统的第二个基础知识点——网络模块。对 MQ 来说，网络模块是核心组件之一，网络模块的性能很大程度上决定了消息传输的能力和整体性能。&lt;/p>……</summary><content type="html">&lt;h1 id="如何设计一个高性能的网络模块">如何设计一个高性能的网络模块？？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>这篇博客我们来扒一下有关消息引擎系统的第二个基础知识点——网络模块。对 MQ 来说，网络模块是核心组件之一，网络模块的性能很大程度上决定了消息传输的能力和整体性能。&lt;/p>
&lt;p>对于 Java 后端的开发人员来说，如果谈到网络模块的开发，大概率都会想到 Netty。Netty 作为 Java 网络编程中最出名的类库，可以说是独当一面的存在。那既然都这么说了的话，关于 MQ 的网络模块选型是不是直接使用 Netty 就可以了？？&lt;/p>
&lt;p>带着这份好奇心，继续往下看看吧。&lt;/p>
&lt;p>选型之前，我们得先知道要解决什么问题。消息引擎系统是需要满足&lt;strong>高吞吐、高可靠、低延时&lt;/strong>，并支持多语言访问的基础软件，网络模块最需要解决的是&lt;strong>性能、稳定性、开发成本&lt;/strong>三个问题。接下来就围绕这三点来思考消息队列网络模块应该怎样设计。&lt;/p>
&lt;h2 id="网络模块的性能瓶颈分析">网络模块的性能瓶颈分析&lt;/h2>
&lt;p>这里就基于最基础的 MQ 访问链路图进行分析。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/59a16955646b5a82ec7e244d3210533d483734cf/content/imag/tech/bigdata/mq/03_mq-framework.svg" alt="mq-framework">&lt;/p>
&lt;p>对于&lt;strong>单个请求&lt;/strong>来说，请求流程是：客户端（生产者 / 消费者）构建请求后， 向服务端发送请求包 —&amp;gt; 服务端接收包后，将包交给业务线程处理 —&amp;gt; 业务线程处理完成后，将结果返回给客户端。其中可能消耗性能的有三个点：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>编解码的速度&lt;/strong>：见上一篇博客——&lt;a href="https://quakewang.github.io/tech/bigdata/bigdata_mq003/#%E7%BC%96%E8%A7%A3%E7%A0%81%E5%AE%9E%E7%8E%B0">MQ003—通信协议&lt;/a>&lt;/li>
&lt;li>&lt;strong>网络延迟&lt;/strong>：即客户端到服务端的网络延迟，这一点取决于网络链路的性能，在软件层面几乎无法优化，与网络模块无关&lt;/li>
&lt;li>&lt;strong>服务端 / 客户端网络模块的处理速度&lt;/strong>：发送 / 接收请求包后，包是否能及时被处理，比如当逻辑线程处理完成后，网络模块是否及时回包。这一点属于性能优化，也是网络模块设计的核心工作，有机会的话会深入探究一下咯。&lt;/li>
&lt;/ul>
&lt;p>对于&lt;strong>并发请求&lt;/strong>来说，在单个请求维度问题的基础上，还需要处理高并发、高 QPS 和高流量等场景带来的性能问题，主要包括以下三个方面：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>高效的连接管理&lt;/strong>：当客户端和服务端之间的 TCP 连接过多，如何高效处理、管理连接；&lt;/li>
&lt;li>&lt;strong>快速处理高并发请求&lt;/strong>：当客户端和服务端之间的 QPS 很高，如何快速处理（接收、返回）请求；&lt;/li>
&lt;li>&lt;strong>大流量场景&lt;/strong>：当客户端和服务端之间的流量很高，如何快速吞吐（读、写）数据。&lt;/li>
&lt;/ul>
&lt;p>大流量场景，某种意义上是高并发处理的一种子场景。因为大流量分为单个请求包大并发小、单个请求包小并发大两种场景，前者的瓶颈主要在于数据拷贝、垃圾回收、CPU 占用等方面，主要依赖语言层面的编码技巧来解决。第二种场景就是我们主要解决的对象。&lt;/p>
&lt;h2 id="高性能网络模块的设计实现">高性能网络模块的设计实现&lt;/h2>
&lt;p>知道了瓶颈在哪里，就具体来看一下如何设计出一个高性能的网络模块。从技术上看，高性能的网络模块设计可以分为如何高效管理大量的 TCP 连接、如何快速处理高并发的请求以及如何提高稳定性和降低开发成本等三个方面。&lt;/p>
&lt;h3 id="基于多路复用技术管理-tcp-连接">基于多路复用技术管理 TCP 连接&lt;/h3>
&lt;p>从技术原理角度思考，高效处理大量 TCP 连接，在消息引擎系统中主要有单条 TCP 连接的复用和多路复用两种技术思路。&lt;/p>
&lt;h4 id="1-单条-tcp-连接的复用">1. 单条 TCP 连接的复用&lt;/h4>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/59a16955646b5a82ec7e244d3210533d483734cf/content/imag/tech/bigdata/mq/04_network-tcp.svg" alt="signle-tcp">&lt;/p>
&lt;p>这是在一条真实的 TCP 连接中，创建信道（channel，可以理解为虚拟连接）的概念。通过编程手段，把信道当做一条 TCP 连接使用，做到 TCP 连接的复用，避免创建大量 TCP 连接导致系统资源消耗过多。&lt;/p>
&lt;p>这种实现的缺点是在协议设计和编码实现的时候有额外的开发工作量，而且近年随着异步 IO、IO 多路复用技术的发展，这种方案有点多余。不过因为语言特性、历史背景等原因，像 RabbitMQ 用的就是这种方案。&lt;/p>
&lt;h4 id="2-io-多路复用">2. IO 多路复用&lt;/h4>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/89caafb5cc608c171eb47a3c4e955ec010c3be95/content/imag/tech/bigdata/mq/04_network-epoll.svg" alt="io-multiplexing">&lt;/p>
&lt;p>像现在主流的 Kafka、RocketMQ、Pulsar 的网络模块都是基于 IO 多路复用的思路开发的。&lt;/p>
&lt;p>IO 多路复用技术，是指通过把多个 IO 的阻塞复用到同一个 selector 的阻塞上，让系统在单线程的情况下可以同时处理多个客户端请求。这样做最大的优势是系统开销小，系统不需要创建额外的进程或者线程，降低了维护的工作量也节省了资源。&lt;/p>
&lt;p>目前支持 IO 多路复用的系统调用有 Select、Poll、Epoll 等，Java NIO 库底层就是基于 Epoll 实现的。&lt;/p>
&lt;p>不过，即使使用了这两种技术，&lt;strong>单机能处理的连接数还是有上限的&lt;/strong>。&lt;/p>
&lt;p>第一个上限是操作系统的 FD 上限，如果连接数超过了 FD的数量，连接会创建失败。第二个上限是系统资源的限制，主要是 CPU 和内存。频繁创建、删除或者创建过多连接会消耗大量的物理资源，导致系统负载过高。&lt;/p>
&lt;p>所以可以发现，&lt;strong>每个消息队列的配置中都会提到连接数的显示和系统 FD 上限调整&lt;/strong>。Linux 中可以通过命令查看系统的 FD 信息：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">// 查看能打开 FD 的数量
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">ulimit&lt;/span> -n // 用户级限制
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat /proc/sys/fs/file-max // 系统级限制
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">// 临时修改最大数量
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">ulimit&lt;/span> -n &lt;span class="m">100000&lt;/span> // 将最大值改为 &lt;span class="m">100000&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>解决了第一个问题连接处理，下面来看如何快速处理高并发请求。&lt;/p>
&lt;h3 id="基于-reactor-模型处理高并发请求">基于 Reactor 模型处理高并发请求&lt;/h3>
&lt;p>先看单个请求的处理。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/89caafb5cc608c171eb47a3c4e955ec010c3be95/content/imag/tech/bigdata/mq/04_network-reactor.svg" alt="reactor-single">&lt;/p>
&lt;p>我们都知道，两点之间直线最短。对于单个请求来说，最快的处理方式就是客户端直接发出请求，服务端接收到包后，直接丢给后面的业务线程处理，当业务线程处理成功后，直接返回给客户端。&lt;/p>
&lt;p>这种处理是最快的，但是还有两个问题需要解决：&lt;/p>
&lt;ul>
&lt;li>如何第一时间拿到包交给后端的业务逻辑处理？？&lt;/li>
&lt;li>当业务逻辑处理完成后，如何立即拿到返回值返回给客户端？？&lt;/li>
&lt;/ul>
&lt;p>可能比较直观的思路就是阻塞等待模型，不断轮询等待请求拿到包，业务逻辑处理完，直接返回结果给客户端。这种处理是最快的。但是阻塞等待模型因为是串行的处理机制，每个请求需要等待上一个请求处理完才能处理，处理效率会比较低。所以，单个请求，最合理的方式就是&lt;strong>异步的事件驱动模型&lt;/strong>，可以通过 Epoll 和异步编程来解决。&lt;/p>
&lt;p>Ok，继续再来看看看高并发请求的情况。在高并发的情况下会有很多连接、请求需要处理，核心思路就是并行、多线程处理。那么如何并行处理呢？？这个时候就需要用到 Reactor 模型了。&lt;/p>
&lt;p>Reactor 模型是一种处理并发服务请求的事件设计模式，当主流程收到请求后，通过多路分离处理的方式，把请求分发给相应的请求处理器处理。如下图所示，Reactor 模式包含 Reactor、Acceptor、Handler 三个角色。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/89caafb5cc608c171eb47a3c4e955ec010c3be95/content/imag/tech/bigdata/mq/04_network-handle-process.svg" alt="reactor">&lt;/p>
&lt;ul>
&lt;li>Reactor：负责监听和分配时间。收到事件后分派给对应的 Handler 处理，事件包括连接建立就绪、读就绪、写就绪等；&lt;/li>
&lt;li>Acceptor：负责处理客户端新连接。Reactor 接收到客户端的连接事件后，会转发给 Acceptor，Acceptor 接收客户端的连接，然后创建对应的 Handler，并向 Reactor 注册此 Handler；&lt;/li>
&lt;li>Handler：请求处理器，负责业务逻辑的处理，即业务处理线程。&lt;/li>
&lt;/ul>
&lt;p>从技术上看，Reactor 模型一般有三种实现方式：&lt;/p>
&lt;ul>
&lt;li>单 Reactor 单线程模型（单 Reactor 单线程）&lt;/li>
&lt;li>单 Reactor 多线程模型 （单 Reactor 多线程）&lt;/li>
&lt;li>主从 Reactor 多线程模型 (多 Reactor 多线程）&lt;/li>
&lt;/ul>
&lt;p>我们具体分析一下，看消息队列更适合哪一种。&lt;/p>
&lt;p>单 Reactor 单线程模型，特点是 Reactor 和 Handler 都是单线程的串行处理。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/7ef0bd09531207220ffe318c1ae10970a064066a/content/imag/tech/bigdata/mq/04_network-handle.svg" alt="reactor-handle">&lt;/p>
&lt;p>优点是所有处理逻辑放在单线程中实现，没有上下文切换、线程竞争、进程通信等问题。缺点是在性能与可靠性方面存在比较严重的问题。&lt;/p>
&lt;p>性能上，因为是单线程处理，无法充分利用 CPU 资源，并且业务逻辑 Handler 的处理是同步的，容易造成阻塞，出现性能瓶颈。可能性主要是因为单 Reactor 是单线程的，如果出现异常不能处理请求，会导致整个系统通信模块不可用。&lt;/p>
&lt;p>&lt;strong>所以单 Reactor 单进程模型不适用于计算密集型的场景，只适用于业务处理非常快速的场景。&lt;/strong>&lt;/p>
&lt;p>相比起来，单 Reactor 多线程模型，业务逻辑处理 Handler 变成了多线程，也是就说，获取到 IO 读写事件之后，业务逻辑是同一批线程在处理。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/89caafb5cc608c171eb47a3c4e955ec010c3be95/content/imag/tech/bigdata/mq/04_network-handle-single.svg" alt="reactor-single">&lt;/p>
&lt;p>优点是 Handler 收到响应后通过 send 把响应结果返回给客户端，降低 Reactor 的性能开销，提升整个应用的吞吐。而且 Handler 使用多线程模式，可以充分利用 CPU 的性能，提高了业务逻辑的处理速度。&lt;/p>
&lt;p>缺点是 Handler 使用多线程模式，带来了多线程竞争资源的开销，同时涉及共享数据的互斥和保护机制，实现比较复杂。另外，单个 Reactor 承担所有事件的监听、分发和响应，对于高并发场景，容易造成性能瓶颈。&lt;/p>
&lt;p>在此基础上，主从 Reactor 多线程模型，是让 Reactor 也变成了多线程。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/0dcae473300c80f5bf469c2280596b4b3a3c8ad0/content/imag/tech/bigdata/mq/04_network-handle-cluster-single.svg" alt="handle-cluster-single">&lt;/p>
&lt;p>当前业界主流 MQ 的网络模型，比如 Kafka、RocketMQ 为了保证性能，都是基于主从 Reactor 多线程模型开发的。&lt;/p>
&lt;p>这种方案，优点是 Reactor 的主线程和子线程分工明确。主线程只负责接收新连接，子线程负责完成后续的业务处理。同时主线程和子线程的交互也很简单，子线程接收主线程的连接后，只要负责处理其对应的业务即可，无须过多关注主线程，可以直接在子线程把处理结果返回给客户端。所以，主从 Reactor 多线程模型适用于高并发场景，Netty 网络通信框架也是采用了这种实现方式。&lt;/p>
&lt;p>缺点是如果基于 NIO 从零开发，开发的复杂度和成本都是比较高的。另外，Acceptor 是一个单线程，如果挂了，如何处理客户端新连接是一个风险点。&lt;/p>
&lt;p>为了解决 Acceptor 的单点问题，有些组件为了保证高可用性，会对主从 Reactor 多线程做一些优化，把 Acceptor 也变为多线程的模式。如下图：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/7ef0bd09531207220ffe318c1ae10970a064066a/content/imag/tech/bigdata/mq/04_network-handle-cluster.svg" alt="handle-cluster">&lt;/p>
&lt;p>到此为止，基于 IO 多路复用技术和 Reactor 模型，我们已经可以解决网络模块的性能问题了，接下来继续深入探究如何提高网络模块的稳定性和降低开发成本。&lt;/p>
&lt;h2 id="基于成熟网络框架提高稳定性并降低开发成本">基于成熟网络框架提高稳定性并降低开发成本&lt;/h2>
&lt;p>这里的“稳定性”主要指代码的稳定性。因为网络模块的特点是编码非常复杂，要考虑的细节和边界条件非常多，一些异常情况的处理也很细节，需要经过长时间的打磨。但相对而言，一旦开发完成，稳定后，代码几乎不需要再改动，因为需求相对固定的。&lt;/p>
&lt;p>在 Java 中，网络模块的核心是一个基础类库——Java NIO 库，它的底层是基于 Unix / Linux IO 复用模型 Epoll 实现的。&lt;/p>
&lt;p>如果我们要基于 Java NIO 库开发一个 Server，需要处理网络的闪断、客户端的重复接入、连接管理、安全认证、编解码、心跳保持、半包读写、异常处理等等细节，工作量非常大。所以在消息队列的网络编程模型中，&lt;strong>为了提高稳定性或者降低成本，选择现成的、成熟的 NIO 框架是一个更好的方案&lt;/strong>。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/0dcae473300c80f5bf469c2280596b4b3a3c8ad0/content/imag/tech/bigdata/mq/04_network-nio.svg" alt="nio">&lt;/p>
&lt;p>而 Netty 就是这样一个基于 Java NIO 封装的成熟框架。所以大部分 Java 开发者一提到网络编程，自然而然会想到 Netty。&lt;/p>
&lt;h2 id="kafka-网络模型">Kafka 网络模型&lt;/h2>
&lt;p>Kafka 的网络层没有用 Netty 作为底层的通信库，而是直接采用 Java NIO 实现网络通信。在网络模型中，也是参照 Reactor 多线程模型，采用多线程、多 Selector 的设计。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/a602648653a511924c6608e2529299122d829d0a/content/imag/tech/bigdata/mq/04_network-kafka.svg" alt="kafka">&lt;/p>
&lt;p>看整个网络层的结果图，Processor 线程和 Handler 线程之间通过 Request Channel 传递数据，Request Channel 中包含一个 Request Queue 和多个Response Queue。每个 Processor 线程对应一个 Response Queue。&lt;/p>
&lt;p>具体流程上：&lt;/p>
&lt;ul>
&lt;li>一个 Acceptor 接收客户端建立连接的请求，创建 Socke 连接并分配给 Processor 处理；&lt;/li>
&lt;li>Processor 线程把读取到的请求存入 RequestQueue 中，Handler 线程从 RequestQueue 队列中取出请求进行处理；&lt;/li>
&lt;li>Handler 线程处理请求产生的响应，会存放到 Processor 对应的 ResponseQueue 中，Processor 线程对其对应的 ResponseQueue 中取出响应信息，并返回给客户端。&lt;/li>
&lt;/ul>
&lt;h2 id="nio-编程和-rpc-框架">NIO 编程和 RPC 框架&lt;/h2>
&lt;p>其实，要想不关心底层的调用细节（比如底层的网络协议和传输协议等），可以直接调用 RPC（Remote Procedure Call）框架来实现。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/0dcae473300c80f5bf469c2280596b4b3a3c8ad0/content/imag/tech/bigdata/mq/04_network-rpc.svg" alt="rpc">&lt;/p>
&lt;p>因为 RPC 调用的是一个远程对象，调用者和被调用者处于不同的节点上，想完成调用，必须实现 4 个能力。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>网络传输协议&lt;/strong>：远端调用底层需要经过网络传输，所以需要选择网络通信协议，比如 TCP。&lt;/li>
&lt;li>&lt;strong>应用通信协议&lt;/strong>：网络传输需要设计好应用层的通信协议，比如 HTTP2 或自定义协议。&lt;/li>
&lt;li>&lt;strong>服务发现&lt;/strong>：调用的是远端对象，需要可以定位到调用的服务器地址以及调用的具体方法。&lt;/li>
&lt;li>&lt;strong>序列化和反序列化&lt;/strong>：网络传输的是二进制数据，因此 RPC 框架需要自带序列化和反序列化的能力。&lt;/li>
&lt;/ul>
&lt;p>细心的话，可以发现 RPC 框架完成的工作等于同学协议和前文提到的网络模块设计两部分的工作。在当前的微服务框架中，RPC 已经是我们很熟悉、很常用且很成熟的技术了。&lt;/p>
&lt;p>那 RPC 框架作为消息队列中的网络模块会有哪些优缺点呢？我们以 gRPC 框架举例分析。gRPC 是 Google 推出的一个 RPC 框架，可以说是 RPC 框架中的典型代表。主要有以下三个优点：&lt;/p>
&lt;ul>
&lt;li>gRPC 内核已经很好地实现了服务发现、连接管理、编解码器等公共部分，我们可以把开发精力集中在消息队列本身，不需要在网络模块消耗太多精力。&lt;/li>
&lt;li>gRPC 几乎支持所有主流编程语言，开发各个消息队列的 SDK 可以节省很多开发成本。&lt;/li>
&lt;li>很多云原生系统，比如 Service Mesh 都集成了 gRPC 协议，基于 HTTP2 的 gRPC 的消息队列很容易被云原生系统中的其他组件所访问，组件间的集成成本很低。&lt;/li>
&lt;/ul>
&lt;p>但是当前主流的消息队列都不支持 gRPC 框架，这是因为如果支持就要做很大的架构改动。而且，gRPC 底层默认是七层的 HTTP2 协议，在性能上，可能比直接基于 TCP 协议实现的方式差一些。但是 HTTP2 本身在性能上做了一些优化，从实际表现来看，性能损耗在大部分场景下是可以接受的。&lt;/p>
&lt;p>所以如果是一个新设计的消息队列或者消息队列的新架构，通过成熟的 RPC 框架来实现网络模块是一个蛮不错的方案。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>MQ 的网络模块主要解决的是性能、稳定性和成本三个方面的问题。&lt;/p>
&lt;p>性能问题，核心是通过 Reactor 模型、IO 多路复用技术解决的。Reactor 模式在 Java 网络编程中用得非常广泛，比如 Netty 就实现了 Reactor 多线程模型。即使不用 Netty 进行网络编程（比如 Kafka 直接基于 Java NIO 编程）的情况下，网络模块也大多是参考或基于 Reactor 模式实现的。因为 Reactor 模式可以结合多路复用、异步调用、多线程等技术解决高并发、大流量场景下的网络模块的性能问题。&lt;/p>
&lt;p>在 Java 技术栈下，网络编程的核心是 Java NIO。但为了解决稳定性和开发成本的问题，建议选择业界成熟的网络框架来实现网络模块，而不是基于原生的 Java NIO 来实现。成熟的框架分为成熟的 NIO 框架（如 Netty）和成熟的 RPC 框架（如 gRPC）。&lt;/p>
&lt;p>目前业界主流的消息队列都是基于 Java NIO 和 Netty 实现的。Netty 是我们网络模块编程的常用选型，大部分情况下，可能还是我们的最终选择。但是 Netty 好用并不意味着所有的 Java 网络编程都必须选择 Java NIO 和 Netty。&lt;/p>
&lt;p>当需要构建一个组件的网络模块的时候，要先知道这个组件的业务特点是什么，需要解决哪些问题，再来考虑使用什么技术。比如在客户端连接数不多、并发不高，流量也很小的场景，只需要一个简单的网络 Server 就够了，完全没必要选择 Java NIO 或 Netty 来实现对应的网络模块。随着技术架构的迭代，基于 RPC 框架的方案也是一个不错的选择。&lt;/p></content></entry><entry><title type="text">MQ003——通信协议</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/bigdata/bigdata_mq003/"/><id>https://quakewang.github.io/tech/bigdata/bigdata_mq003/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-07-18T11:52:35+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="如何为消息引擎系统设计一个好的通信协议">如何为消息引擎系统设计一个好的通信协议？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>经过上面两篇博客的梳理，已经了解了 MQ 的基本概念。从功能上看，一个最基础的消息引擎系统应该具备生产、存储和消费的能力。也就是能够完成“生产者把数据发送到 Broker，Broker 收到数据后，持久化存储数据，最后消费者从 Broker 消费数据”的整个流程。&lt;/p>……</summary><content type="html">&lt;h1 id="如何为消息引擎系统设计一个好的通信协议">如何为消息引擎系统设计一个好的通信协议？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>经过上面两篇博客的梳理，已经了解了 MQ 的基本概念。从功能上看，一个最基础的消息引擎系统应该具备生产、存储和消费的能力。也就是能够完成“生产者把数据发送到 Broker，Broker 收到数据后，持久化存储数据，最后消费者从 Broker 消费数据”的整个流程。&lt;/p>
&lt;p>从整个流程来拆解技术架构，最基础的消息引擎系统应该具备五个模块：&lt;/p>
&lt;ul>
&lt;li>通信协议：用来完成客户端（生产者和消费者）和 Broker 之间的通信，比如生产和消费；&lt;/li>
&lt;li>网络模块：客户端用来发送数据，服务端用来接收数据；&lt;/li>
&lt;li>存储模块：服务端用来完成持久化数据存储；&lt;/li>
&lt;li>生产者：完成生产相关的功能；&lt;/li>
&lt;li>消费者：完成消费相关的功能。&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/6bff996c03c81391f3fd7fb50fc9a5c4c2d73c28/content/imag/tech/bigdata/mq/03_mq-framework.svg" alt="mq-framework">&lt;/p>
&lt;p>其实消息引擎系统，本质上讲就是个 CS 模型，即通过客户端和服务端之间的交互完成生产、消费等行为。那么客户端和服务端之间的通信流程是如何实现的呢？？&lt;/p>
&lt;p>这就是今天的重点——&lt;strong>通信协议&lt;/strong>。为了完成交互，我们第一步就需要确定服务端和客户端是如何通信的。而通信的第一步就是确定使用哪种通信协议进行通信。&lt;/p>
&lt;h2 id="通信协议基础">通信协议基础&lt;/h2>
&lt;p>所有协议的选择和设计都是根据需求来的，我们知道 MQ 的核心特性是&lt;strong>高吞吐、低延时、高可靠&lt;/strong>，所以在协议上至少需要满足：&lt;/p>
&lt;ul>
&lt;li>协议可靠性要高，不能丢数据；&lt;/li>
&lt;li>协议的性能要高，通信的延时要低；&lt;/li>
&lt;li>协议的内容要精简，带宽的利用率要高；&lt;/li>
&lt;li>协议需要具备可扩展能力，方便功能的增减。&lt;/li>
&lt;/ul>
&lt;p>那没有没现成的满足这四个要求的协议呢？&lt;/p>
&lt;p>目前业界的通信协议可以分为&lt;strong>公有协议&lt;/strong>和&lt;strong>私有协议&lt;/strong>两种。公有协议指公开的受到认可的具有规范的协议，比如 JMS、HTTP、STOMP 等。私有协议是指根据自身的功能和需求设计的协议，一般不具备通用性，比如 Kafka、RocketMQ、Puslar 的协议都是私有协议。&lt;/p>
&lt;p>其实 MQ 领域是存在公有的、可直接使用的标准协议的，比如 AMQP、MQTT、OpenMessaging，它们设计的初衷就是为了解决因各个消息队列的协议不一样导致的组件互通、用户使用成本高、重复设计、重复开发成本等问题。但是，公有的标准协议讨论制定需要较长时间，往往无法及时赶上需求的变化，灵活性不足。&lt;/p>
&lt;p>因此大多数消息队列为了自身的功能支持、迭代速度、灵活性考虑，在核心通信协议的选择上不会选择公有协议，都会选择自定义私有协议。那私有协议要怎么设计实现呢？从技术上来看，私有协议设计一般需要包含三个步骤。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>网络通信协议选型&lt;/strong>，指计算机七层网络模型中的协议选择。比如传输层的 TCP/UDP、应用层的 HTTP/WebSocket 等；&lt;/li>
&lt;li>&lt;strong>应用通信协议设计&lt;/strong>，指如何约定客户端和服务端之间的通信规则。比如如何识别请求内容、如何确定请求字段信息等；&lt;/li>
&lt;li>&lt;strong>编解码（序列化 / 反序列化）实现&lt;/strong>，用于将二进制的信息的内容解析为程序可识别的数据格式。&lt;/li>
&lt;/ul>
&lt;h3 id="网络通信协议选型">网络通信协议选型&lt;/h3>
&lt;p>从功能需求出发，为了保证性能和可靠性，几乎所有主流的 MQ 在核心生产、消费链路的协议选择上，都是&lt;strong>基于可靠性高、长连接的 TCP 协议&lt;/strong>。&lt;/p>
&lt;p>四层的 UDP 虽然也是长连接，性能更高，但是因为其不可传输的特性，业界几乎没有消息引擎系统用它通信。&lt;/p>
&lt;p>七层的 HTTP 协议每次通信都需要经历三次握手、四次关闭等步骤，并且协议结构也不够精简。因此在性能（比如耗时）上的表现比较差，不适合高吞吐、大流量、低延时的场景。所以主流协议在核心链路上很少使用 HTTP。&lt;/p>
&lt;h3 id="应用通信协议设计">应用通信协议设计&lt;/h3>
&lt;p>从应用通信协议构成的角度，协议一般会包含协议头和协议提两部分。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>协议头&lt;/strong>包含一些通用信息和数据源信息，比如协议版本、请求标识、请求 ID、客户端 ID 等等；&lt;/li>
&lt;li>&lt;strong>协议体&lt;/strong>主要包含本次通信的业务数据，比如一个字符串、一段 JSON 格式的数据或者原始二进制数据等等。&lt;/li>
&lt;/ul>
&lt;p>从编解码协议的设计角度来看，需要分别针对“请求”和“返回”设计协议，请求协议结构和返回协议结构一般如下图：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/94f8e7ed4fff61ff741ab976fe448e0024ab82d9/content/imag/tech/bigdata/mq/03_protocol.svg" alt="protocol">&lt;/p>
&lt;p>设计的原则是：&lt;strong>请求维度的通用信息放在协议头，消息维度的信息就放在协议体&lt;/strong>。下面结合 Kafka 协议来详细分析一下：&lt;/p>
&lt;h4 id="协议头的设计">协议头的设计&lt;/h4>
&lt;p>协议头的设计，首先要确认协议中需要携带哪些通用的信息。一般情况下，请求头要携带本次请求以及源端的一些信息，返回头要携带请求唯一标识来标识对应哪个请求。&lt;/p>
&lt;p>所以，请求头一般需要携带协议版本、请求标识、请求的 ID、客户端 ID 等信息。而返回头，一般只需要携带本次请求的 ID、本次请求的处理结果（成功或失败）等几个信息。&lt;/p>
&lt;p>接下来，我们分析一下 Kafka 协议的请求头和返回头的内容，以便于对协议头的设计有个更直观的认识。如下图所示，Kafka V2 协议的请求头中携带了四个信息。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/94f8e7ed4fff61ff741ab976fe448e0024ab82d9/content/imag/tech/bigdata/mq/03_request-header.svg" alt="requestheader">&lt;/p>
&lt;ul>
&lt;li>用来标识请求类型的 api_key，如生产、消费、获取元数据；&lt;/li>
&lt;li>用来标识请求协议版本的 api_version，如 V0、V1、V2；&lt;/li>
&lt;li>用来唯一标识该请求 correlation_id，可以理解为请求 ID；&lt;/li>
&lt;li>用来标识客户端的 client_id。&lt;/li>
&lt;/ul>
&lt;p>Kafka V0 协议的返回头只携带了一个信息，即该请求的 correlation_id，用来标识这个返回是哪个请求的。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/94f8e7ed4fff61ff741ab976fe448e0024ab82d9/content/imag/tech/bigdata/mq/03_response-header.svg" alt="requestheader">&lt;/p>
&lt;p>这里有个细节你可能注意到了，请求协议头是 V2 版本，返回协议头是 V0 版本，会不会有点问题呢？&lt;/p>
&lt;p>其实是没有的。因为从协议的角度，一般业务需求的变化（增加或删除）都会涉及请求内容的修改，所以请求的协议变化是比较频繁的，而返回头只要能标识本次对应的请求即可，所以协议的变化比较少。所以，&lt;strong>请求头和返回头的协议版本制定，是建议分开定义的&lt;/strong>，这样在后期的维护升级中会更加灵活。&lt;/p>
&lt;h4 id="协议体的设计">协议体的设计&lt;/h4>
&lt;p>协议体的设计就和业务功能密切相关了。因为协议体是携带本次请求 / 返回的具体内容的，不同接口是不一样的，比如生产、消费、确认，每个接口的功能不一样，结构基本千差万别。&lt;/p>
&lt;p>不过设计上还是有共性的，注意三个点：&lt;strong>极简、向后兼容、协议版本管理&lt;/strong>。如何理解呢？&lt;/p>
&lt;p>协议在实现上首先需要具备向后兼容的能力，后续的变更（如增加或删除）不会影响新老客户端的使用；然后协议内容上要尽量精简（比如字段和数据类型），这样可以降低编解码和传输过程中的带宽的开销，以及其他物理资源的开销；最后需要协议版本管理，方便后续的变更。&lt;/p>
&lt;p>同样为了让你直观感受协议体的设计，我们看 Kafka 生产请求和返回的协议内容：&lt;/p>
&lt;p>Kafka 生产请求协议如下：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/94f8e7ed4fff61ff741ab976fe448e0024ab82d9/content/imag/tech/bigdata/mq/03_request-protocol.svg" alt="request-protocol">&lt;/p>
&lt;p>Kafka 生产返回协议如下：&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/94f8e7ed4fff61ff741ab976fe448e0024ab82d9/content/imag/tech/bigdata/mq/03_response-protocol.svg" alt="response-protocol">&lt;/p>
&lt;p>Kafka 生产请求的协议体包含了事务 ID、acks 信息、请求超时时间、Topic 相关的数据，都是和生产操作相关的。生产返回的协议体包含了限流信息、分区维度的范围信息等。这些字段中的每个字段都是经过多轮迭代、重复设计定下来的，每个字段都用用处。&lt;/p>
&lt;p>所以在协议体的设计上，&lt;strong>最核心的就是要遵循“极简”原则，在满足业务要求的基础上，尽量压缩协议的大小。&lt;/strong>&lt;/p>
&lt;p>接下来我想讨论一下数据类型，在协议设计里，我们很容易忽略的一个事就是数据类型，比如上面 throttle_time_ms 是 INT32，error_code 是 INT16。&lt;/p>
&lt;p>数据类型很简单，用来标识每个字段的类型，不过为什么会有这个东西呢，不能直接用 int、string、char 等基础类型吗？这里有两个原因。&lt;/p>
&lt;ul>
&lt;li>消息队列是多语言通信的。不同语言对于同一类型的定义和实现是不一样的，如果使用同一种基础类型在不同的语言进行解析，可能会出现解析错乱等错误。&lt;/li>
&lt;li>需要尽量精简消息的长度。比如只需要 1 个 byte 就可以表示的内容，如果用 4 个 byte 来表示，就会导致消息的内容更长，消耗更多的物理带宽。&lt;/li>
&lt;/ul>
&lt;p>所以一般在协议设计的时候，我们也需要设计相关的基础数据类型（如何设计可以参考 &lt;a href="https://kafka.apache.org/protocol.html#protocol_types">Kafka 的协议数据类型&lt;/a>或者 &lt;a href="https://protobuf.dev/programming-guides/proto3/#scalar">Protobuf 的数据类型&lt;/a>）。&lt;/p>
&lt;h3 id="编解码实现">编解码实现&lt;/h3>
&lt;p>编解码也称为序列化和反序列，就是数据发送的时候编码，收到数据的时候解码。&lt;/p>
&lt;p>为什么要编解码呢? 如下图所示，因为数据在网络中传输时是二进制的形式，所以在客户端发送数据的时候就要将原始的格式数据编码为二进制数据，以便在 TCP 协议中传输，这一步就是序列化。然后在服务端将收到的二进制数据根据约定好的规范解析成为原始的格式数据，这就是反序列化。&lt;/p>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/94f8e7ed4fff61ff741ab976fe448e0024ab82d9/content/imag/tech/bigdata/mq/03_encode.svg" alt="encode">&lt;/p>
&lt;p>在序列化和反序列化中，最重要的就是 TCP 的粘包与拆包。TCP 是一个“流”协议，是一串数据，没有明显的界限，TCP 层面不知道这段数据的意义，只负责传输。所以应用层就要根据某个规则从流数据中拆出完整的包，解析出有意义的数据，这就是沾包和拆包的作用。&lt;/p>
&lt;p>&lt;strong>沾包 / 拆包的基本思路：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>消息定长；&lt;/li>
&lt;li>在包尾增加回车换行符进行分割，例如 FTP 协议；&lt;/li>
&lt;li>将消息分为消息头和消息体，消息头中包含消息总长度，然后根据长度从流中解析出数据；&lt;/li>
&lt;li>更加复杂的应用层协议，比如 HTTP、WebSocket 等。&lt;/li>
&lt;/ul>
&lt;p>早期，消息队列的协议设计几乎都是自定义实现编解码，如 RabbitMQ、RocektMQ 4.0、Kafka 等。但从 0 实现编解码器比较复杂，随着业界主流编解码框架和编解码协议的成熟，一些消息队列（如 Pulsar 和 RocketMQ 5.0）开始使用业界成熟的编解码框架，如 Google 的 Protobuf。Protobuf 是一个灵活、高效、结构化的编解码框架，业界非常流行，很多商业产品都会用，它支持多语言，编解码性能较高，可扩展性强，产品成熟度高。这些优点，都是我们在设计协议的时候需要重点考虑和实现的，并且我们自定义实现编解码的效果不一定有 Protobuf 好。所以新的消息队列产品或者新架构可以考虑选择 Protobuf 作为编解码框架。&lt;/p>
&lt;p>如果想关注如何在 MQ 中实现自定义编码，可以去深入了解 RocketMQ，它是目前业界唯一一个既支持自定义编解码，又支持成熟编解码框架的消息引擎系统。RocketMQ 5.0 之前支持的 Remoting 协议是自定义编解码，5.0 之后支持的 gRPC 协议是基于 Protobuf 编解码框架。用 Protobuf 的主要原因是它选择 gRPC 框架作为通信框架。而 gRPC 框架中默认编解码器为 Protobuf，编解码操作已经在 gRPC 的库中正确地定义和实现了，不需要单独开发。所以 RocketMQ 可以把重点放在 Rocket 消息队列本身的逻辑上，不需要在协议方面上花费太多精力。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>无论是做业务开发，还是数据开发，都或多或少和 MQ 打过交道，很多程序员可能只停留在如何使用上，其实慢慢尝试往下走一步会有不一样的收获～～&lt;/p>
&lt;p>这篇博客也只是浅谈 MQ 底层关于通信协议设计的讨论。从功能支持、迭代速度、灵活性上考虑，大多数消息队列的核心通信协议都会优先考虑自定义的私有协议。私有协议的设计主要考虑网络通信协议选择、应用通信协议设计、编解码实现三个方面。&lt;/p>
&lt;ul>
&lt;li>网络通信协议选型，基于可靠、低延时的需求，大部分情况下应该选择 TCP。&lt;/li>
&lt;li>应用通信协议设计，分为请求协议和返回协议两方面。协议应该包含协议头和协议体两部分。协议头主要包含一些通用的信息，协议体包含请求维度的信息。&lt;/li>
&lt;li>编解码，也叫序列化和反序列化。在实现上分为自定义实现和使用现成的编解码框架两个路径。&lt;/li>
&lt;/ul>
&lt;p>其中最重要的是应用通信协议部分的设计选型，这部分需要设计协议头和协议体。重要的是要思考协议头和协议体里面分别要放什么，放多了浪费带宽影响传输性能，放少了无法满足业务需求，需要频繁修改协议内容。&lt;/p>
&lt;p>另外，每个字段的类型也有讲究，需要尽量降低每次通信的数据大小。所以应用通信协议的内容设计是非常考验技术功底或者经验的。有一个技巧是，如果需要实现自定义的协议，可以去参考一下业界主流的协议实现，看看都包含哪些元素，各自踩过什么坑。总结分析后，这样一般能设计出一个相对较好的消息队列。&lt;/p>
&lt;h2 id="思考">思考？？&lt;/h2>
&lt;p>为什么业界的消息引擎系统有多种标准的协议呢？？&lt;/p>
&lt;blockquote>
&lt;p>业界的消息队列有多种标准的协议，如 MQTT、AMQP、OpenMessaging。主要是因为业务场景不一样，一套协议标准无法满足多种场景需要。&lt;/p>
&lt;ul>
&lt;li>MQTT 是为了满足物联网领域的通信而设计的，背景是网络环境不稳定、网络带宽小，从而需要极精简的协议结构，并允许可能的数据丢失。&lt;/li>
&lt;li>AMQP 是主要面向业务消息的协议，因为要承载复杂的业务逻辑，所以协议设计上要尽可能丰富，包含多种场景，并且在传输过程中不允许出现数据丢失。因为 AMQP 协议本身的设计具有很多局限，比如功能太简单，所以不太符合移动互联网、云原生架构下的消息需求。&lt;/li>
&lt;li>OpenMessaging 的设计初衷是设计一个符合更多场景的消息队列协议。&lt;/li>
&lt;/ul>
&lt;/blockquote></content></entry><entry><title type="text">MQ002——消息引擎系统的基本常识</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/bigdata/bigdata_mq002/"/><id>https://quakewang.github.io/tech/bigdata/bigdata_mq002/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-07-13T23:55:25+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="消息引擎系统的基本概念">消息引擎系统的基本概念&lt;/h1>
&lt;h2 id="前言什么时候会用-mq">前言——什么时候会用 MQ？？&lt;/h2>
&lt;p>经过上一节 blog 的内容，以及了解了有关 MQ 的前置知识。那么是否会有想过 MQ 会用在哪些场景呢？&lt;/p>……</summary><content type="html">&lt;h1 id="消息引擎系统的基本概念">消息引擎系统的基本概念&lt;/h1>
&lt;h2 id="前言什么时候会用-mq">前言——什么时候会用 MQ？？&lt;/h2>
&lt;p>经过上一节 blog 的内容，以及了解了有关 MQ 的前置知识。那么是否会有想过 MQ 会用在哪些场景呢？&lt;/p>
&lt;p>在现如今的系统架构中，MQ 的定位就是&lt;strong>总线和管道&lt;/strong>，主要起到解耦上下游系统、数据缓存的作用，通俗点就是“削峰填谷”。这个时候肯定会有人会想到 Redis 之类的数据库，与之不同的是它的主要操作就是&lt;strong>生产和消费&lt;/strong>，而不太会关注计算、聚合和查询的逻辑。所以，在业务中不管使用哪款 MQ，其核心的操作永远是生产和消费数据。&lt;/p>
&lt;p>上次提到的订单下单流程就是一个典型的&lt;strong>系统解耦、消息分发&lt;/strong>的场景，一份数据需要被多个下游系统处理。在大数据领域中，比较经典的就是日志采集流程，一般日志数据都很大，而且是实时产生的，直接发到下游，下游系统可能会扛不住崩溃，所以会把数据先缓存到 MQ 中。实际现如今的数仓，不仅仅是日志文件写入 Kafka 之类的消息引擎系统，有时候也会选择被存储在数据库中的业务数据，通过增量同步的方式传入到 MQ，然后再统一采集到 HDFS 上。&lt;/p>
&lt;p>针对以上场景，一款优秀的消息引擎系统必须满足：高性能、高吞吐和低延时等基本特性。&lt;/p>
&lt;h2 id="架构层面的基本概念">架构层面的基本概念&lt;/h2>
&lt;p>&lt;img src="https://raw.githubusercontent.com/QuakeWang/quakewang.github.io/master/content/imag/tech/bigdata/mq/mq002_basic.png" alt="mq002_basic">&lt;/p>
&lt;ul>
&lt;li>
&lt;p>Topic：在大部分 MQ 中，topic 都是指用来组织分区关系的一个逻辑概念。通常情况下，一个 topic 会包含多个分区。在 Kafka 中，发布订阅的是 topic，可以为每个业务、每个应用甚至是每类数据都创建专属的 topic；&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Producer（生产者）：向 topic 发布消息的客户端应用程序称为生产者，生产者程序通常会持续不断地向一个或多个主题发送消息。简单来说就是：指消息的发送方，发送消息的客户端；&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Consumer（消费者）：订阅这些主题消息的客户端应用程序称为消费者，和生产者类似，消费者也能够同时订阅多个注意的消息。说通俗点就是，指消息的接收方，即接收消息的客户端；&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Broker：Broker 负责接收和处理客户端发送过来的请求，以及对消息进行持久化。Broker 本质上是一个进程。Kafka 的服务器端由被称为 Broker 的服务进程构成，即一个 Kafka 集群由多个 Broker 组成。虽然多个 Broker 进程能够运行在同一台机器上，但更常见的做法是将不同的 Broker 分散运行在不同的机器上，这样如果集群中某一台机器宕机，即使在它上面运行的所有 Broker 进程都挂掉了，其他机器上的 Broker 也依然能够对外提供服务。这其实就是 Kafka 提供高可用的手段之一；&lt;/p>
&lt;/li>
&lt;li>
&lt;p>ConsumerGroup/Subscription（消费分组 / 订阅）：一般情况下，消息队列中消费分组和订阅是同一个概念，后面统一用消费分组来称呼。它是用来组织消费者和分区关系的逻辑概念，也有保存消费进度的作用。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Message（消息）：指一条真实的业务数据，消息队列的每条数据一般都叫做一条消息。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Offset/ConsumerOffset/Cursor（位点 / 消费位点 / 游标）：指消费者消费分区的进度，即每个消费者都会去消费分区，为了避免重复消费进度，都会保存消费者消费分区的进度信息。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>ACK/OffsetCommit（确认 / 位点提交）：确认和位点提交一般都是指提交消费进度的操作，即数据消费成功后，提交当前的消费位点，确保不重复消费。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Leader/Follower（领导者 / 追随者，主副本 / 从副本）：Leader 和 Follower 一般是分区维度副本的概念，即集群中的分区一般会有多个副本。此时就会有主从副本的概念，一般是一个主副本配上一个或多个从副本。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Segment（段 / 数据分段）：段是指消息数据在底层具体存储时，分为多个文件存储时的文件，这个文件就叫做分区的数据段。即比如每超过 1G 的文件就新起一个文件来存储，这个文件就是 Segment。基本所有的消息队列都有段的概念，比如 Kakfa 的 Segment、Pulsar 的 Ledger 等等。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>StartOffset/EndOffset（起始位点 / 结束位点）：起始位点和结束位点是分区维度的概念。即数据是顺序写入到分区的，一般从 0 的位置开始往后写，此时起始位点就是 0。因为数据有过期的概念，分区维度较早的数据会被清理。此时起始位点就会往后移，表示当前阶段最早那条有效消息的位点。结束位点是指最新的那条数据的写入位置。因为数据一直在写入分区，所以起始位点和结束位点是一直动态变化的。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>ACL（访问控制技术）：ACL 全称是 Access Control List，用来对集群中的资源进行权限控制，比如控制分区或 Topic 的读和写等。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="功能层面的基础概念">功能层面的基础概念&lt;/h2>
&lt;p>讲完了架构层面的基本概念，我们来看看功能层面的基本概念。&lt;/p>
&lt;p>相比于数据库的基本操作是增删改查，消息队列的基本操作就是生产和消费，即读和写。消息队列一般是不支持客户端修改和删除单条数据的。接下来我们就从功能的角度，来了解一些常见的基本概念。&lt;/p>
&lt;ul>
&lt;li>顺序消息：是指从生产者和消费者的视角来看，生产者按顺序写入 Topic 的消息，在消费者这边能不能按生产者写入的顺序消费到消息，如果能就是顺序消息。&lt;/li>
&lt;li>延时消息 / 定时消息：都是指生产者发送消息到 Broker 时，可以设置这条消息在多久后能被消费到，当时间到了后，消息就会被消费到。延时的意思就是指以 Broker 收到消息的时间为准，多久后消息能被消费者消费，比如消息发送成功后的 30 分钟才能被消费。定时是指可以指定消息在设置的时间才能被看到，比如设置明天的 20:00 才能被消费。从技术上来看，两者是一样的；从客户端的角度，功能上稍微有细微的差别；从内核的角度，一般两种消息是以同一个概念出现的。&lt;/li>
&lt;li>事务消息：消息队列的事务因为在不同的消息队列中的实现方式不一样，所以定义也不太一样。正常情况下，事务表示多个操作的原子性，即一批操作要么一起成功，要么一起失败。在消息队列中，一般指发送一批消息，要么同时成功，要么同时失败。&lt;/li>
&lt;li>消息重试：消息重试分为生产者重试和消费者重试。生产者重试是指当消息发送失败后，可以设置重试逻辑，比如重试几次、多久后重试、重试间隔多少。消费者重试是指当消费的消息处理失败后，会自动重试消费消息。&lt;/li>
&lt;li>消息回溯：是指当允许消息被多次消费，即某条消息消费成功后，这条消息不会被删除，还能再重复到这条消息。&lt;/li>
&lt;li>广播消费：广播听起来是一个主动的，即 Broker 将一条消息广播发送给多个消费者。但是在消息队列中，广播本质上是指一条消息能不能被很多个消费者消费到。只要能被多个消费者消费到，就能起到广播消费的效果，就可以叫做广播消费。&lt;/li>
&lt;li>死信队列：死信队列是一个功能，不是一个像分区一样的实体概念。它是指当某条消息无法处理成功时，则把这条消息写入到死信队列，将这条消息保存起来，从而可以处理后续的消息的功能。大部分情况下，死信队列在消费端使用得比较多，即消费到的消息无法处理成功，则将数据先保存到死信队列，然后可以继续处理其他消息。当然，在生产的时候也会有死信队列的概念，即某条消息无法写入 Topic，则可以先写入到死信队列。从功能上来看，死信队列的功能业务也可以自己去实现。消息队列中死信队列的意思是，消息队列的 SDK 已经集成了这部分功能，从而让业务使用起来就很简单。&lt;/li>
&lt;li>优先级队列：优先级队列是指可以给在一个分区或队列中的消息设置权重，权重大的消息能够被优先消费到。大部分情况下，消息队列的消息处理是 FIFO 先进先出的规则。此时如果某些消息需要被优先处理，基于这个规则就无法实现。所以就有了优先级队列的概念，优先级是消息维度设置的。&lt;/li>
&lt;li>消息过滤：是指可以给每条消息打上标签，在消费的时候可以根据标签信息去消费消息。可以理解为一个简单的查询消息的功能，即通过标签去查询过滤消息。消息过滤主要在消费端生效。&lt;/li>
&lt;li>消息过期 / 删除（TTL）：是指消息队列中的消息会在一定时间或者超过一定大小后会被删除。因为消息队列主要是缓冲作用，所以一般会要求消息在一定的策略后会自动被清理。&lt;/li>
&lt;li>消息轨迹：是指记录一条消息从生产端发送、服务端保存、消费端消费的全生命周期的流程信息。用来追溯消息什么时候被发送、是否发送成功、什么时候发送成功、服务端是否保存成功、什么时候保存成功、被哪些消费者消费、是否消费成功、什么时候被消费等等信息&lt;/li>
&lt;li>消息查询：是指能够根据某些信息查询到消息队列中的信息。比如根据消息 ID 或根据消费位点来查询消息，可以理解为数据库里面的固定条件的 select 操作。&lt;/li>
&lt;li>消息压缩：是指生产端发送消息的时候，是否支持将消息进行压缩，以节省物理资源（比如网卡、硬盘）。压缩可以在 SDK 完成，也可以在 Broker 完成，并没有严格限制。通常来看，压缩在客户端完成会比较合理。&lt;/li>
&lt;li>多租户：是指同一个集群是否有逻辑隔离，比如一个物理集群能否创建两个名称都为 test 的主题。此时一般会有一个逻辑概念 Namespace（命名空间）和 Tenant（租户）来做隔离，一般有这两个概念的就是支持多租户。&lt;/li>
&lt;li>消息持久化：是指消息发送到 Broker 后，会不会持久化存储，比如存储到硬盘。有些消息队列为了保证性能，只会把消息存储在内存，此时节点重启后数据就会丢失。&lt;/li>
&lt;li>消息流控：是指能否对写入集群的消息进行限制。一般会支持 Topic、分区、消费分组、集群等维度的限流。&lt;/li>
&lt;/ul>
&lt;h2 id="总结扩展">总结（扩展）&lt;/h2>
&lt;p>看完了上面的基础概念，下面就以 Kafka 为例，整体来看一下～～&lt;/p>
&lt;p>Kafka 的三层消息架构：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>第一层是主题层，每个主题可以配置 M 个分区，而每个分区又可以配置 N 个副本。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>第二层是分区层，每个分区的 N 个副本中只能有一个充当领导者角色，对外提供服务；其他 N-1 个副本是追随者副本，只是提供数据冗余之用。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>第三层是消息层，分区中包含若干条消息，每条消息的位移从 0 开始，依次递增。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>最后，客户端程序只能与分区的领导者副本进行交互。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Kafka Broker 如何持久化数据？？&lt;/strong>&lt;/p>
&lt;p>总的来说，Kafka 使用消息日志（Log）来保存数据，一个日志就是磁盘上一个只能追加写（Append-only）消息的物理文件。因为只能追加写入，故避免了缓慢的随机 I/O 操作，改为性能较好的顺序 I/O 写操作，这也是实现 Kafka 高吞吐量特性的一个重要手段。不过如果你不停地向一个日志写入消息，最终也会耗尽所有的磁盘空间，因此 Kafka 必然要定期地删除消息以回收磁盘。怎么删除呢？简单来说就是通过日志段（Log Segment）机制。在 Kafka 底层，一个日志又进一步细分成多个日志段，消息被追加写到当前最新的日志段中，当写满了一个日志段后，Kafka 会自动切分出一个新的日志段，并将老的日志段封存起来。Kafka 在后台还有定时任务会定期地检查老的日志段是否能够被删除，从而实现回收磁盘空间的目的。&lt;/p>
&lt;p>&lt;strong>谈谈消费者&lt;/strong>&lt;/p>
&lt;p>这里再重点说说消费者。上一篇博客中提到过两种消息模型，即点对点模型（Peer to Peer，P2P）和发布订阅模型。这里面的点对点指的是同一条消息只能被下游的一个消费者消费，其他消费者则不能染指。在 Kafka 中实现这种 P2P 模型的方法就是引入了消费者组（Consumer Group）。&lt;/p>
&lt;p>所谓的消费者组，指的是多个消费者实例共同组成一个组来消费一组主题。这组主题中的每个分区都只会被组内的一个消费者实例消费，其他消费者实例不能消费它。为什么要引入消费者组呢？**主要是为了提升消费者端的吞吐量。**多个消费者实例同时消费，加速整个消费端的吞吐量（TPS）。BTW 这里的消费者实例可以是运行消费者应用的进程，也可以是一个线程，它们都称为一个消费者实例（Consumer Instance）。&lt;/p>
&lt;p>消费者组里面的所有消费者实例不仅“瓜分”订阅主题的数据，而且更牛掰的的是它们还能彼此协助。假设组内某个实例挂掉了，Kafka 能够自动检测到，然后把这个 Failed 实例之前负责的分区转移给其他活着的消费者。这个过程就是 Kafka 中大名鼎鼎的“重平衡”（Rebalance）。嗯，其实既是大名鼎鼎，也是臭名昭著，因为由重平衡引发的消费者问题比比皆是。事实上，目前很多重平衡的 Bug 社区都无力解决。每个消费者在消费消息的过程中必然需要有个字段记录它当前消费到了分区的哪个位置上，这个字段就是消费者位移（Consumer Offset）。&lt;/p>
&lt;p>注意，这和上面所说的位移完全不是一个概念。上面的“位移”表征的是分区内的消息位置，它是不变的，即一旦消息被成功写入到一个分区上，它的位移值就是固定的了。而消费者位移则不同，它可能是随时变化的，毕竟它是消费者消费进度的指示器嘛。另外每个消费者有着自己的消费者位移，因此一定要区分这两类位移的区别。我个人把消息在分区中的位移称为分区位移，而把消费者端的位移称为消费者位移。&lt;/p></content></entry><entry><title type="text">2023，不想再谈技术？？</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/2023_mid/"/><id>https://quakewang.github.io/tech/2023_mid/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-07-08T22:39:53+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="开发者的落日">开发者的落日？？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>这篇文章，早就想写，但又怕自己资历太浅，见识不够，会的太少，写出来或多或少有点泛泛而谈，或者说蹭热度，毕竟最近遇上互联网寒冬，各个大厂裁员，这也是事实。&lt;/p>……</summary><content type="html">&lt;h1 id="开发者的落日">开发者的落日？？&lt;/h1>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>这篇文章，早就想写，但又怕自己资历太浅，见识不够，会的太少，写出来或多或少有点泛泛而谈，或者说蹭热度，毕竟最近遇上互联网寒冬，各个大厂裁员，这也是事实。&lt;/p>
&lt;p>熬过了💩一样的 2022，原以为今年会好那么一丢丢，但实际发现也就那么回事，整个行业危机似乎在某种程度上来说可以进一步加剧了。那么互联网的红利期是真的过去了吗？&lt;/p>
&lt;blockquote>
&lt;p>这是一切的开始，这也是人类的落日。&lt;/p>
&lt;/blockquote>
&lt;h2 id="谈谈程序员">谈谈程序员&lt;/h2>
&lt;p>从我的角度来说，我一直很喜欢程序员这个行业，从本质上来说，开发的本质是创造，而创造的过程中是连接着未来的。但不知从何时起出现了“码农”这个称呼，因为 CRUD 的操作，这一点从我接触这个行业开始就早已存在，我是极其特别讨厌这个称呼。因为它将整个行业形容成做的东西就是重复劳动，没有意义可言。&lt;/p>
&lt;p>那么这类人可以称之为程序员嚒？至少在我是不认可的，或者说不能当“职业的程序员”。GPT 的出现，可以说是划时代的产品，它将颠覆以往的编程方式，只会这种 CRUD 的操作人，也会进一步被淘汰，因为从本质上来说就没必要存在。但这却是这个行业的普遍现状，又有几个能做中间件开发呢？？&lt;/p>
&lt;p>所以普通的开发者要深刻理解一件事情，如今你暂时拿在手里的看似高薪不是个人能力的体现，是行业带动的结果，那么行业带动的结果。你要感谢两家公司微软和 Google。&lt;/p>
&lt;p>如果整个互联网行业只有 2 家公司可以存活的话，除了微软和Google，任何其他企业都没有资格，只能去死。&lt;/p>
&lt;p>微软大幅降低了编程的门槛，围绕操作系统诞生一整套的生态，把编程变成了一种普通人可以当成工作的玩意儿。从这角度来看，那怕你不懂编程，能读懂英文，也能七七八八理解一段简单的代码。&lt;/p>
&lt;p>Google 站在巨人的肩膀上，从提出分布式这个概念开始，几乎重塑了整个计算机行业。此后，Google 不满足于此，不仅疯狂制定顶层标准，而且在科技界的贡献持续造福全人类。&lt;/p>
&lt;p>可以这么说，互联网行业这些年的高光完全是行业热潮带动普通开发人员受益，而不是个人本身。我们都是风口上的猪而已。&lt;/p>
&lt;p>技术本身门槛的大幅降低导致【技术通货膨胀】，更多的人参与就代表每个人分到更少的蛋糕。回归公司的本质，一个专注于技术但是对业务 0 贡献的开发者为什么能拿高薪趴在电脑前？存量市场的竞争白热化，躺着赚钱的时代终结。不懂业务，疯狂跳槽，盲目转行，仅仅追逐技术能力的提升脱离商业本身的开发者最终会被弃之如敝履。&lt;/p>
&lt;p>综上所述，简单来说就是&lt;strong>普通的技术越来越不值钱&lt;/strong>。&lt;/p>
&lt;h2 id="人人皆可编程low-code">人人皆可编程：Low Code&lt;/h2>
&lt;p>Low Code Development Platform 是指无需编码（0 代码）或通过少量代码就可以快速生成应用程序的开发平台。&lt;/p>
&lt;p>低代码在干什么？我们直接引用行业大佬的一句话：&lt;/p>
&lt;pre tabindex="0">&lt;code>低代码是基于可视化和模型驱动理念，结合云原生与多端体验技术，它能够在多数业务场景下实现大幅度的提效降本，为专业开发者提供了一种全新的高生产力开发范式。另一方面，低代码能够让不懂代码的人，通过“拖拉拽”开发组件，就能完成应用搭建。从意义上讲，低代码可以弥补日益扩大的专业技术人才缺口，同时促成业务与技术深度协作的终极敏捷形态。
&lt;/code>&lt;/pre>&lt;p>看到了么？低代码是让不懂的人可以进行完成相关开发任务。这个领域的发展会迅速淘汰掉企业中的某些混子，然后向外蔓延开来。&lt;/p>
&lt;p>下面以大数据方面来举例（Java 后端开发的脚手架太多了 都是些烂梗 没意思），现如今无论公司规模大小，很多企业都想要整个大数据平台，可能会面临的问题就是，没有专业大数据开发者。如何在节省人力成本的情况下解决这个难题呢？？&lt;/p>
&lt;p>因为大数据的组件框架众多，搭建一套高可用能够落地的集群系统就存在着不小的难度，那么有没有可以一键部署安装的平台呢？？又该如何管理这些大数据组件呢？如果有的话，是否就可以让后端工程师通过写 SQL 完成大数据的工作呢？？还有一点就是各个工作流之间的上下游关系以及整个集群的任务调度是否可以通过“拖拉拽”的方式完成呢？？答案是肯定的。&lt;/p>
&lt;p>显而易见，后端开发写 SQL 算是基本功，只需了解各个大数据组件的用途，再看些行业大牛的分享就足以完成一个低代码版的大数据集群。是否可以进一步来说，其他领域也很难独善其身。&lt;/p>
&lt;p>&lt;strong>丧钟为谁而鸣？&lt;/strong>&lt;/p>
&lt;h2 id="faas-和-paas">FaaS 和 PaaS&lt;/h2>
&lt;p>关于云计算时代的一些名词不做过多解释，可以自行去查资料。&lt;/p>
&lt;p>从 2021 年开始，其实从 2020 年下半年就初露端倪。FaaS 重新被推上风口浪尖，普通开发者应该感到危机。&lt;/p>
&lt;p>FaaS 是 Functions as a Service 的简称，它往往和无服务架构（Serverless Architecture）一同被提起。PaaS 是 Platform as a Service 的简称，是一种云模型，你提供源码，平台将打包、发布、部署、运行、监控和扩缩微服务。&lt;/p>
&lt;p>为了好理解就拿 FaaS 举例。&lt;/p>
&lt;p>大家注意，FaaS 自上而下，和上文的低代码自下而上对普通开发者形成了双重挤压，对普通开发者的生存空间造成了严重影响。&lt;/p>
&lt;p>FaaS 在干什么？FaaS 抛弃了原来大型复杂应用的架构，将整个架构中的单元进行拆分，将各种软、硬件资源等抽象为一种服务提供给开发者使用，让他们不再担心基础设施、资源需求、中间件等等，专注于具体逻辑实现。&lt;/p>
&lt;p>看到了么？FaaS 已经将整个开发者最需要脑子和开发量的工作吃掉了。基础设施和中间件乃至服务器资源的管理，不需要开发者介入。这会导致什么后果？&lt;/p>
&lt;p>大批量【填鸭式】进入这个行业的开发者你们应该感到危机。因为 FaaS 正把你们变成实实在在的【工具人】。&lt;/p>
&lt;p>&lt;strong>所谓工具人，就是低成本、易替换。&lt;/strong>&lt;/p>
&lt;h2 id="谈谈对于大数据的影响">谈谈对于大数据的影响&lt;/h2>
&lt;p>&lt;strong>再次声明，只是个人的思考。我也希望未来几年回过来看自己使劲打自己脸。&lt;/strong>&lt;/p>
&lt;p>数据领域进入平台期，门槛降低，湖仓一体，批流结合。这一点可以看看 Apache 和数据领域的顶级项目和孵化器中的项目。&lt;/p>
&lt;p>未来低代码盛行，类 SQL、拖拉拽大行其道，会导致开发者们离原理越来越远，不懂底层设计，不懂顶层架构，结合上文拿大数据低代码举的例子，这一点应该不难理解，像阿里的 DataWorks 对于用户来说只要会写 SQL 就可以。数据领域天然 Low Code。&lt;/p>
&lt;p>&lt;strong>愚者还在窃喜，智者却在悲伤。&lt;/strong>&lt;/p>
&lt;p>因为而致力于低代码和云计算领域的行业推动者正是微软、Google、阿里云这些行业引领者。&lt;/p>
&lt;p>它们成就了开发者盛世，也会亲手毁掉开发者。&lt;/p>
&lt;p>所谓，成也萧何败也萧何。&lt;/p>
&lt;h2 id="不破不立">不破不立&lt;/h2>
&lt;h3 id="懂原理">懂原理&lt;/h3>
&lt;p>目前数据领域还处于上升期，每过一段时间就会蹦出几个新概念。这些新概念势必会带动一些基础架构部门的发展，因为基础架构部门不向前跑，就会变成运维专家，变成答疑专家，就会自己干掉自己。&lt;/p>
&lt;p>所以，作为引领公司甚至行业的基础开发者，基于业务大胆启用新的技术方案前，只有对原理足够熟悉，才能做到顺利转型。盲目上马，不做出充分调研，技术能力不足会被其他领跑者拖死。可以想一下 MQ 之类的框架，微服务在用，大数据中也是必不可少，那么区别在哪呢？？又有多少用户仅仅是只掌握几条常用命令呢？？&lt;/p>
&lt;h3 id="顶层架构设计">顶层架构设计&lt;/h3>
&lt;p>这一点我是比较佩服顶级的运维，很多开发者嘲笑运维不会写代码，也有可能只是个梗，一个好的运维是可以看见整个集群全貌的，这一部分仅仅只做开发是很难做到的。那么一个好的架构设计，尤其是基于业务系统的合理技术选型和正确的架构设计，这对开发者提出了相当高的要求，技术栈足够深入，场景足够丰富的情况下才能游刃有余。对于那些技术 Leader，这更是巨大的挑战。否则，一将无能，累死千军。要么你足够优秀可以当做火把给全队把路照亮，要么就要有足够的魅力发现和吸引那些是火把的人。&lt;/p>
&lt;h3 id="懂业务">懂业务&lt;/h3>
&lt;p>业务才是开发者的立足根本，对业务足够熟悉，才能最终站稳脚跟。随着开发门槛的不断降低，业务人员，尤其是一些专业的熟悉业务的且可以做分析的业务人员，它们对于开发会逐渐降低依赖，在低代码和套件化足够成熟的未来，只懂开发的开发者会被边缘化。&lt;/p>
&lt;h3 id="参与开源">参与开源&lt;/h3>
&lt;p>参与开源，拥抱未来，这一点不想再解释了。。&lt;/p>
&lt;hr>
&lt;p>所以，2023 年是一切的开始，也是开发者的落日？？&lt;/p></content></entry><entry><title type="text">MQ001——消息引擎系统入门篇</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/bigdata/bigdata_mq001/"/><id>https://quakewang.github.io/tech/bigdata/bigdata_mq001/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-07-06T22:51:59+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="关于消息引擎系统">关于消息引擎系统&lt;/h1>
&lt;h2 id="一前言">一、前言&lt;/h2>
&lt;p>看见标题写的是“消息引擎系统”，咋一看是不是觉得比较陌生，那么换个说法呢，比如“消息队列”、“消息中间件”这些无论是在后端开发还是在大数据中想必都是耳熟能详的啦。但从我的角度来说，更喜欢称呼其为“消息引擎系统”。因为消息队列给出了一个不太明确的暗示，仿佛类似 Kafka 之类的框架是利用队列的方式构建的；而消息中间件的提法有过度夸张“中间件”之嫌，让人搞不清楚这个中间件到底是做什么用途的。&lt;/p>……</summary><content type="html">&lt;h1 id="关于消息引擎系统">关于消息引擎系统&lt;/h1>
&lt;h2 id="一前言">一、前言&lt;/h2>
&lt;p>看见标题写的是“消息引擎系统”，咋一看是不是觉得比较陌生，那么换个说法呢，比如“消息队列”、“消息中间件”这些无论是在后端开发还是在大数据中想必都是耳熟能详的啦。但从我的角度来说，更喜欢称呼其为“消息引擎系统”。因为消息队列给出了一个不太明确的暗示，仿佛类似 Kafka 之类的框架是利用队列的方式构建的；而消息中间件的提法有过度夸张“中间件”之嫌，让人搞不清楚这个中间件到底是做什么用途的。&lt;/p>
&lt;p>像 Kafka 这一类的系统国外有专属的名字叫 Messaging System，国内很多文献将其简单翻译成消息系统。我个人认为并不是很恰当，因为它片面强调了消息主体的作用，而忽视了这类系统引以为豪的消息传递属性，就像引擎一样，具备某种能量转换传输的能力，所以我觉得翻译成消息引擎反倒更加贴切。&lt;/p>
&lt;p>讲到这里，说点题外话。我觉得目前国内在翻译国外专有技术词汇方面做得不够标准化，各种名字和提法可谓五花八门。我举个例子，比如大名鼎鼎的 Raft 算法和 Paxos 算法。了解它的人都知道它们的作用是在分布式系统中让多个节点就某个决定达成共识，都属于 Consensus Algorithm 一族。如果你在搜索引擎中查找 Raft 算法，国内多是称呼它们为一致性算法。实际上我倒觉得翻译成共识算法是最准确的。我们使用“一致性”这个字眼太频繁了，国外的 Consistency 被称为一致性、Consensus 也唤作一致性，甚至是 Coherence 都翻译成一致性。&lt;/p>
&lt;h2 id="二用途">二、用途&lt;/h2>
&lt;p>根据维基百科的定义，消息引擎系统是一组规范。企业利用这组规范在不同系统之间传递语义准确的消息，实现松耦合的异步式数据传递。&lt;/p>
&lt;p>常见的官网不说人话系列，读起来云里雾里的。其实简单来说就是：系统 A 发送消息给消息引擎系统，系统 B 从消息引擎系统中读取 A 发送的消息。&lt;/p>
&lt;p>最基础的消息引擎就是做这点事的！不论是上面哪个版本，它们都提到了两个重要的事实：&lt;/p>
&lt;ul>
&lt;li>消息引擎传输的对象是消息；&lt;/li>
&lt;li>如何传输消息属于消息引擎设计机制的一部分。&lt;/li>
&lt;/ul>
&lt;h2 id="三传输信息">三、传输信息&lt;/h2>
&lt;p>既然消息引擎是用于在不同系统之间传输消息的，那么如何设计待传输消息的格式从来都是一等一的大事。试问一条消息如何做到信息表达业务语义而无歧义，同时它还要能最大限度地提供可重用性以及通用性？稍微停顿几秒去思考一下，如果是你，你要如何设计你的消息编码格式。&lt;/p>
&lt;p>一个比较容易想到的是使用已有的一些成熟解决方案，比如使用 CSV、XML 亦或是 JSON；又或者你可能熟知国外大厂开源的一些序列化框架，比如 Google 的 Protocol Buffer 或 Facebook 的 Thrift。这些方法借助开源框架实现都是不错的选择，那么像 Kafka 这种事如何实现的呢？答案是：它使用的是纯二进制的字节序列。当然消息还是结构化的，只是在使用之前都要将其转换成二进制的字节序列。&lt;/p>
&lt;p>消息设计出来之后还不够，消息引擎系统还要设定具体的传输协议，即用什么方法把消息传输出去。常见的有两种方法：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>点对点模型&lt;/strong>：也叫消息队列模型。说通俗点就是，系统 A 发送的消息只能被系统 B 接收，其他任何系统都不能读取 A 发送的消息。日常生活的例子比如电话客服就属于这种模型：同一个客户呼入电话只能被一位客服人员处理，第二个客服人员不能为该客户服务。&lt;/li>
&lt;li>&lt;strong>发布 / 订阅模型&lt;/strong>：与上面不同的是，它有一个主题（Topic）的概念，你可以理解成逻辑语义相近的消息容器。该模型也有发送方和接收方，只不过提法不同。发送方也称为发布者（Publisher），接收方称为订阅者（Subscriber）。和点对点模型不同的是，这个模型可能存在多个发布者向相同的主题发送消息，而订阅者也可能存在多个，它们都能接收到相同主题的消息。生活中的报纸订阅就是一种典型的发布 / 订阅模型。&lt;/li>
&lt;/ul>
&lt;p>而像现在主流的 MQ 大部分都是同时支持这两种消息引擎模型。好了，现在我们了解了消息引擎系统是做什么的以及怎么做的，但还有个重要的问题是为什么要使用到这类框架呢？&lt;/p>
&lt;h2 id="四削峰填谷">四、削峰填谷&lt;/h2>
&lt;p>写这篇博客的时候，我查询了很多资料和文献，最常见的就是这四个字。所谓的“削峰填谷”就是指缓冲上下游瞬时突发流量，使其更平滑。特别是对那种发送能力很强的上游系统，如果没有消息引擎的保护，“脆弱”的下游系统可能会直接被压垮导致全链路服务“雪崩”。但是，一旦有了消息引擎，它能够有效地对抗上游的流量冲击，真正做到将上游的“峰”填满到“谷”中，避免了流量的震荡。消息引擎系统的另一大好处在于发送方和接收方的松耦合，这也在一定程度上简化了应用的开发，减少了系统间不必要的交互。&lt;/p>
&lt;p>说了这么多，可能你对“削峰填谷”并没有太多直观的感受。接下来用 Kafka 举个例子来说明一下在这中间是怎么去“抗”峰值流量的吧。回想一下，你在某宝是如何购物的。看见想要的商品点击立即购买。之后会进入到付款页面。这个简单的步骤中就可能包含多个子服务，比如点击购买按钮会调用订单系统生成对应的订单，而处理该订单会依次调用下游的多个子系统服务 ，比如调用支付宝的接口，查询你的登录信息，验证商品信息等。显然上游的订单操作比较简单，它的 TPS 要远高于处理订单的下游服务，因此如果上下游系统直接对接，势必会出现下游服务无法及时处理上游订单从而造成订单堆积的情形。特别是当出现类似于秒杀这样的业务时，上游订单流量会瞬时增加，可能出现的结果就是直接压跨下游子系统服务。&lt;/p>
&lt;p>解决此问题的一个普通的做法是我们对上游系统进行限速，但这种做法对上游系统而言显然是不合理的，毕竟问题并不出现在它那里。所以更常见的办法是引入像 Kafka 这样的消息引擎系统来对抗这种上下游系统 TPS 的错配以及瞬时峰值流量。&lt;/p>
&lt;p>还是这个例子，当引入了 Kafka 之后。上游订单服务不再直接与下游子服务进行交互。当新订单生成后它仅仅是向 Kafka Broker 发送一条订单消息即可。类似地，下游的各个子服务订阅 Kafka 中的对应主题，并实时从该主题的各自分区（Partition）中获取到订单消息进行处理，从而实现了上游订单服务与下游订单处理服务的解耦。这样当出现秒杀业务时，Kafka 能够将瞬时增加的订单流量全部以消息形式保存在对应的主题中，既不影响上游服务的 TPS，同时也给下游子服务留出了充足的时间去消费它们。这就是 Kafka 这类消息引擎系统的最大意义所在。&lt;/p>
&lt;h2 id="五结束语">五、结束语&lt;/h2>
&lt;p>其实从广义上讲，消息引擎系统是有缓冲作用、具备类发布和订阅能力的存储引擎。关于 MQ 的演进，无论是从需求发展路径上看是：消息 —&amp;gt; 流 —&amp;gt; 消息和流融合，还是从架构发展角度的单机 —&amp;gt; 分布式 —&amp;gt; 云原生 /Serverless，本质上走的都是降低成本的方向。&lt;/p>
&lt;p>为了降低成本，弹性是最基础的要求。所以消息引擎系统在技术上，对计算弹性的需求提出了计算存储分离架构，对低存储成本的需求提出了分层存储的概念，对资源复用的需求提出了多租户的概念。&lt;/p>
&lt;p>为了吸引用户，现如今常见的消息引擎系统都在尽量提高自己的竞争力，围绕着功能、容灾、多架构、生态建设展开。&lt;/p>
&lt;p>不过要注意，消息和流只是业界的趋势，不是我们作为使用者必然的非此即彼的选择。在开发者实际使用的时候，我也发现很多人会将 Kafka 当做一个业务消息总线在用，也有人使用 RocketMQ 传递大流量的日志，当做大数据架构中的管道在用。&lt;/p>
&lt;p>所以要学会变通，学技术做框架没有现成的，更不会一成不变，要有敏锐的洞察力，才不会被淘汰。&lt;/p>
&lt;p>&lt;strong>补充&lt;/strong>：什么是消息和流？&lt;/p>
&lt;ul>
&lt;li>消息，就是业务信息，在业务机构（比如微服务架构）中用来做信息传递，做系统的消息总线，比如用户提交订单的流程。&lt;/li>
&lt;li>流，就是在大数据框架中用来做大流量时的数据削峰，比如日志的投递流转。&lt;/li>
&lt;/ul></content></entry><entry><title type="text">Rust02——程序员的基本素养，编程必会的基础知识</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/rust/prepare/02_basic/"/><id>https://quakewang.github.io/tech/rust/prepare/02_basic/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-06-28T23:10:01+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h2 id="rust--02--编程开发中必须掌握的基本概念">Rust--02 ｜ 编程开发中，必须掌握的基本概念&lt;/h2>
&lt;h3 id="牢骚话">牢骚话😩&lt;/h3>
&lt;p>&lt;a href="https://quakewang.github.io/tech/rust/prepare/01_memory/">上一讲&lt;/a>我们了解了内存的基本运作方式，简单回顾一下：栈上存放的数据是静态的，固定大小，固定生命周期；堆上存放的数据是动态，不固定大小，不固定生命周期。&lt;/p>……</summary><content type="html">&lt;h2 id="rust--02--编程开发中必须掌握的基本概念">Rust--02 ｜ 编程开发中，必须掌握的基本概念&lt;/h2>
&lt;h3 id="牢骚话">牢骚话😩&lt;/h3>
&lt;p>&lt;a href="https://quakewang.github.io/tech/rust/prepare/01_memory/">上一讲&lt;/a>我们了解了内存的基本运作方式，简单回顾一下：栈上存放的数据是静态的，固定大小，固定生命周期；堆上存放的数据是动态，不固定大小，不固定生命周期。&lt;/p>
&lt;p>这一讲，来梳理一下编程开发过程中一些常见的基础概念。按照习惯，我会将其分为四大类：&lt;strong>数据&lt;/strong>（值和类型、指针和引用）、&lt;strong>代码&lt;/strong>（函数、方法、闭包、接口和虚标）、&lt;strong>运行方式&lt;/strong>（并发并行、同步异步和 Promise / async / await），以及&lt;strong>编程范式&lt;/strong>（泛型编程）。&lt;/p>
&lt;p>有的时候，很多人都说在搞开发，写代码。从某种程度上来说确实如此。如果说，仅仅是把需求翻译成代码也算程序员的话，那么什么才是职业的程序员呢？？怎么做才是职业的程序员呢？？我的答案是从基础做起，夯实软件开发相关的基础知识，而 Rust 恰恰是可以从一定程度上反应程序员是否合格的标准，比如所有权、动态分派以及并发处理等。&lt;/p>
&lt;p>说了点废话，下面开始正片～～&lt;/p>
&lt;h3 id="数据">数据&lt;/h3>
&lt;p>数据是程序操作的对象，不进行数据处理的程序是没有意义的，我们先来重温和数据有关的概念，包括值和类型、指针和引用。&lt;/p>
&lt;h3 id="值和类型">值和类型&lt;/h3>
&lt;p>严谨地说，类型是对值的区分，它包含了值在内存中的&lt;strong>长度、对齐以及值可以进行的操作等信息&lt;/strong>。一个值是符合一个特定类型的数据的某个实体。比如 64u8，它是 u8 类型，对应一个字节大小、取值范围在 0～255 的某个整数实体，这个实体是 64。&lt;/p>
&lt;p>值以类型规定的表达方式（representation）被存储成一组字节流进行访问。比如 64，存储在内存中的表现形式是 0x40，或者 0b 0100 0000。&lt;/p>
&lt;p>这里需要注意的是，&lt;strong>值是无法脱离具体的类型讨论的&lt;/strong>。同样是内存中的一个字节 0x40，如果其类型是 ASCII char，那么其含义就不是 64，而是 @ 符号。&lt;/p>
&lt;p>不管是强类型的语言还是弱类型的语言，语言内部都有其类型的具体表述。一般而言，编程语言的类型可以分为原生类型和组合类型两大类。&lt;/p>
&lt;p>原生类型（primitive type）是编程语言提供的最基础的数据类型。比如字符、整数、浮点数、布尔值、数组（array）、元组（tuple）、指针、引用、函数、闭包等。&lt;strong>所有原生类型的大小都是固定的，因此它们可以被分配到栈上&lt;/strong>。&lt;/p>
&lt;p>组合类型（composite type）或者说复合类型，是指由一组原生类型和其它类型组合而成的类型。组合类型也可以细分为两类：&lt;/p>
&lt;ul>
&lt;li>结构体（structure type）：&lt;strong>多个类型组合在一起共同表达一个值的复杂数据结构&lt;/strong>。比如 Person 结构体，内部包含 name、age、email 等信息。用代数数据类型（algebraic data type）的说法，结构体是 product type。&lt;/li>
&lt;li>标签联合（tagged union）：也叫不相交并集（disjoint union），&lt;strong>可以存储一组不同但固定的类型中的某个类型的对象，具体是哪个类型由其标签决定&lt;/strong>。比如 Haskell 里的 Maybe 类型，或者 Swift 中的 Optional 就是标签联合。用代数数据类型的说法，标签联合是 sum type。&lt;/li>
&lt;/ul>
&lt;p>另外不少语言不支持标签联合，只取其标签部分，提供了枚举类型（enumerate）。枚举是标签联合的子类型，但功能比较弱，无法表达复杂的结构。&lt;/p>
&lt;h3 id="指针和引用">指针和引用&lt;/h3>
&lt;p>在内存中，一个值被存储到内存中的某个位置，这个位置对应一个内存地址。而指针是一个持有内存地址的值，可以通过解引用（dereference）来访问它指向的内存地址，理论上可以解引用到任意数据类型。&lt;/p>
&lt;p>引用（reference）和指针非常类似，不同的是，&lt;strong>引用的解引用访问是受限的，它只能解引用到它引用数据的类型，不能用作它用&lt;/strong>。比如，指向 42u8 这个值的一个引用，它解引用的时候只能使用 u8 数据类型。&lt;/p>
&lt;p>所以，指针的使用限制更少，但也会带来更多的危害。如果没有用正确的类型解引用一个指针，那么会引发各种各样的内存问题，造成系统崩溃或者潜在的安全漏洞。&lt;/p>
&lt;p>刚刚讲过，&lt;strong>指针和引用是原生类型，它们可以分配在栈上&lt;/strong>。&lt;/p>
&lt;p>根据指向数据的不同，某些引用除了需要一个指针指向内存地址之外，还需要内存地址的长度和其它信息。&lt;/p>
&lt;p>如上一讲提到的指向 “hello world” 字符串的指针，还包含字符串长度和字符串的容量，一共使用了 3 个 word，在 64 位 CPU 下占用 24 个字节，这样&lt;strong>比正常指针携带更多信息的指针，我们称之为胖指针（fat pointer）&lt;/strong>。很多数据结构的引用，内部都是由胖指针实现的。&lt;/p>
&lt;h3 id="代码">代码&lt;/h3>
&lt;p>数据是程序操作的对象，而代码是程序运行的主体，也是我们开发者把物理世界中的需求转换成数字世界中逻辑的载体。我们会讨论函数和闭包、接口和虚表。&lt;/p>
&lt;h3 id="函数方法和闭包">函数、方法和闭包&lt;/h3>
&lt;p>函数是编程语言的基本要素，它是对完成某个功能的一组相关语句和表达式的封装。&lt;strong>函数也是对代码中重复行为的抽象&lt;/strong>。在现代编程语言中，函数往往是一等公民，这意味着函数可以作为参数传递，或者作为返回值返回，也可以作为复合类型中的一个组成部分。&lt;/p>
&lt;p>在面向对象的编程语言中，在类或者对象中定义的函数，被称为方法（method）。方法往往和对象的指针发生关系，比如 Python 对象的 self 引用，或者 Java 对象的 this 引用。&lt;/p>
&lt;p>而闭包是将函数或者说代码和其环境一起存储的一种数据结构。&lt;strong>闭包引用的上下文中的自由变量，会被捕获到闭包的结构中，成为闭包类型的一部分&lt;/strong>。&lt;/p>
&lt;h3 id="接口和虚表">接口和虚表&lt;/h3>
&lt;p>接口是一个软件系统开发的核心部分，它反映了系统的设计者对系统的抽象理解。&lt;strong>作为一个抽象层，接口将使用方和实现方隔离开来，使两者不直接有依赖关系，大大提高了复用性和扩展性&lt;/strong>。&lt;/p>
&lt;p>很多编程语言都有接口的概念，允许开发者面向接口设计，比如 Java 的 interface 和 Rust 的 trait 等。&lt;/p>
&lt;p>我们可以看一下如下场景：在 HTTP 中，Request/Response 的服务处理模型其实就是一个典型的接口，只需要按照服务接口定义出不同输入下，从 Request 到 Response 具体该如何映射，通过这个接口，系统就可以在合适的场景下，把符合要求的 Request 分派给对应的服务。&lt;/p>
&lt;p>面向接口的设计是软件开发中的重要能力，而 Rust 尤其重视接口的能力。当我们在运行期使用接口来引用具体类型的时候，代码就具备了运行时多态的能力。但是，在运行时，一旦使用了关于接口的引用，变量原本的类型被抹去，就无法单纯从一个指针分析出这个引用具备什么样的能力。&lt;/p>
&lt;p>因此，&lt;strong>在生成这个引用的时候，我们需要构建胖指针，除了指向数据本身外，还需要指向一张覆盖了这个接口所支持方法的列表&lt;/strong>。这个列表，也就是所谓的虚表（virtual table）。&lt;/p>
&lt;p>由于虚表记录了数据能够执行的接口，所以在运行期，我们想对一个接口有不同实现，可以根据上下文动态分派。&lt;/p>
&lt;p>比如我想为一个编辑器的 Formatter 接口实现不同语言的格式化工具。我们可以在编辑器加载时，把所有支持的语言和其格式化工具放入一个哈希表中，哈希表的 key 为语言类型，value 为每种格式化工具 Formatter 接口的引用。这样，当用户在编辑器打开某个文件的时候，我们可以根据文件类型，找到对应 Formatter 的引用，来进行格式化操作。&lt;/p>
&lt;h3 id="运行方式">运行方式&lt;/h3>
&lt;p>程序在加载后，&lt;strong>代码以何种方式运行，往往决定着程序的执行效率&lt;/strong>。所以我们接下来讨论并发、并行、同步、异步以及异步中的几个重要概念 Promise/async/await。&lt;/p>
&lt;h3 id="并发concurrency和并行parallel">并发（concurrency）和并行（parallel）&lt;/h3>
&lt;p>并发和并行是软件开发中经常遇到的概念。&lt;/p>
&lt;p>并发是同时与多件事情打交道的能力，比如系统可以在任务 A 做到一定程度后，保存该任务的上下文，挂起并切换到任务 B，然后过段时间再切换回任务 A。&lt;/p>
&lt;p>并行是同时处理多件事情的方式，也就是说，任务 A 和任务 B 可以在同一个时间下工作，无需上下文切换。&lt;/p>
&lt;p>&lt;strong>并发是一种能力，而并行是一种手段&lt;/strong>。当系统拥有了并发的能力后，代码如果跑在多个 CPU core 上，就可以并行运行。所以我们平时都谈论高并发处理，而不会说高并行处理。&lt;/p>
&lt;h3 id="同步和异步">同步和异步&lt;/h3>
&lt;p>同步是指一个任务开始执行后，后续的操作会阻塞，直到这个任务结束。在软件中，我们大部分的代码都是同步操作，比如 CPU，只有流水线中的前一条指令执行完成，才会执行下一条指令。一个函数 A 先后调用函数 B 和 C，也会执行完 B 之后才执行 C。同步执行保证了代码的因果关系（causality），是程序正确性的保证。然而在遭遇 I/O 处理时，高效 CPU 指令和低效 I/O 之间的巨大鸿沟，成为了软件的性能杀手。下图&lt;a href="https://gist.github.com/hellerbarde/2843375">对比了 CPU、内存、I/O 设备、和网络的延迟&lt;/a>：&lt;/p>
&lt;p>&lt;img src="https://camo.githubusercontent.com/76ced2beb27b9053184da0cd7bf2d671f901d1521fcb35cb00d55631d22d5b85/687474703a2f2f692e696d6775722e636f6d2f6b307431652e706e67" alt="延迟">&lt;/p>
&lt;p>我们可以看到和内存访问相比，I/O 操作的访问速度低了两个数量级，一旦遇到 I/O 操作，CPU 就只能闲置来等待 I/O 设备运行完毕。因此，操作系统为应用程序提供了异步 I/O，让应用可以在当前 I/O 处理完毕之前，将 CPU 时间用作其它任务的处理。&lt;/p>
&lt;p>所以，&lt;strong>异步是指一个任务开始执行后，与它没有因果关系的其它任务可以正常执行，不必等待前一个任务结束&lt;/strong>。&lt;/p>
&lt;p>在异步操作里，异步处理完成后的结果，一般用 Promise 来保存，它是一个对象，用来描述在未来的某个时刻才能获得的结果的值，一般存在三个状态：&lt;/p>
&lt;ul>
&lt;li>初始状态，Promise 还未运行；&lt;/li>
&lt;li>等待（pending）状态，Promise 已运行，但还未结束；&lt;/li>
&lt;li>结束状态， Promise 成功解析出一个值，或者执行失败。&lt;/li>
&lt;/ul>
&lt;p>如果你对 Promise 这个词不太熟悉，在很多支持异步的语言中，Promise 也叫 Future / Delay / Deferred 等。除了这个词以外，我们也经常看到 async/await 这对关键字。&lt;/p>
&lt;p>一般来说，&lt;strong>async 定义了一个可以并发执行的任务，而 await 则触发了这个任务并发执行&lt;/strong>。大多数编程语言中，async/await 是一个语法糖（syntactic sugar），它使用状态机将 Promise 包装起来，让异步调用的使用感觉和同步调用非常类似，也让代码更容易阅读。&lt;/p>
&lt;h3 id="编程范式">编程范式&lt;/h3>
&lt;p>为了在不断迭代时，更好地维护代码，我们还会&lt;strong>引入各种各样的编程范式，来提升代码的质量&lt;/strong>。所以最后来谈谈泛型编程。&lt;/p>
&lt;p>如果你来自于弱类型语言，如 C / Python / JavaScript，那泛型编程是你需要重点掌握的概念和技能。泛型编程包含两个层面，数据结构的泛型和使用泛型结构代码的泛型化。&lt;/p>
&lt;p>（&lt;em>强类型和弱类型的定义一直不太明确，&lt;a href="https://en.wikipedia.org/wiki/Strong_and_weak_typing">wikipedia&lt;/a> 上也没有一个标准的说法。。按照习惯一般是看类型在调用时是否会发生隐式转换，所以说 python 是弱类型。不过 wikipedia 在介绍 &lt;a href="https://en.wikipedia.org/wiki/Python_(programming_language)">python&lt;/a> 时确实说它是 strongly typed。但如果按照类型是否会隐式转换，Rust 是强类型，Python 和 C 是弱类型&lt;/em>）&lt;/p>
&lt;h3 id="数据结构的泛型">数据结构的泛型&lt;/h3>
&lt;p>首先是数据结构的泛型，它也往往被称为参数类型或者参数多态，比如下面这个数据结构：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">struct&lt;/span> &lt;span class="nc">Connection&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">S&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">io&lt;/span>: &lt;span class="nc">S&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">state&lt;/span>: &lt;span class="nc">State&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>它有一个参数 S，其内部的域 io 的类型是 S，S 具体的类型只有在使用 Connection 的上下文中才得到绑定。&lt;/p>
&lt;p>可以把参数化数据结构理解成一个产生类型的函数，&lt;strong>在“调用”时，它接受若干个使用了具体类型的参数，返回携带这些类型的类型&lt;/strong>。比如我们为 S 提供 TcpStream 这个类型，那么就产生 Connection这个类型，其中 io 的类型是 TcpStream。&lt;/p>
&lt;p>读到这里可能会产生疑惑，如果 S 可以是任意类型，那我们怎么知道 S 有什么行为？如果我们要调用 io.send() 发送数据，编译器怎么知道 S 包含这个方法？&lt;/p>
&lt;p>这是个好问题，&lt;strong>我们需要用接口对 S 进行约束&lt;/strong>。所以我们经常看到，支持泛型编程的语言，会提供强大的接口编程能力，后续有时间可以聊聊 Rust 的 trait，再详细探讨这个问题。&lt;/p>
&lt;p>数据结构的泛型是一种高级抽象，就像我们人类用数字抽象具体事物的数量，又发明了代数来进一步抽象具体的数字一样。它带来的好处是我们可以延迟绑定，让数据结构的通用性更强，适用场合更广阔；也大大减少了代码的重复，提高了可维护性。&lt;/p>
&lt;h3 id="代码的规范化">代码的规范化&lt;/h3>
&lt;p>泛型编程的另一个层面是使用泛型结构后代码的泛型化。当我们使用泛型结构编写代码时，相关的代码也需要额外的抽象。&lt;/p>
&lt;p>这里用我们熟悉的二分查找的例子解释会比较清楚：&lt;/p>
&lt;p>&lt;img src="https://github.com/QuakeWang/quakewang.github.io/blob/master/content/tech/rust/images/02_binary-search.png?raw=true" alt="binary-search">&lt;/p>
&lt;p>左边用 C 撰写的二分查找，标记的几处操作隐含着和 int[] 有关，所以如果对不同的数据类型做二分查找，实现也要跟着改变。右边 C++ 的实现，对这些地方做了抽象，让我们可以用同一套代码二分查找迭代器（iterator）的数据类型。&lt;/p>
&lt;p>同样的，这样的代码可以在更广阔的场合使用，更简洁容易维护。&lt;/p>
&lt;h3 id="小结">小结&lt;/h3>
&lt;p>本节内容讨论了四类基本概念：数据、代码、运行方式和编程范式。&lt;/p>
&lt;p>&lt;img src="https://github.com/QuakeWang/quakewang.github.io/blob/master/content/tech/rust/images/02_basic-concepts.png?raw=true" alt="basic-concepts">&lt;/p>
&lt;p>&lt;strong>值&lt;/strong>无法离开类型单独讨论，&lt;strong>类型&lt;/strong>一般分为原生类型和组合类型。&lt;strong>指针和引用&lt;/strong>都指向值的内存地址，只不过二者在解引用时的行为不一样。引用只能解引用到原来的数据类型，而指针没有这个限制，然而，不受约束的指针解引用，会带来内存安全方面的问题。&lt;/p>
&lt;p>&lt;strong>函数&lt;/strong>是代码中重复行为的抽象，&lt;strong>方法&lt;/strong>是对象内部定义的函数，而&lt;strong>闭包&lt;/strong>是一种特殊的函数，它会捕获函数体内使用到的上下文中的自由变量，作为闭包成员的一部分。&lt;/p>
&lt;p>而&lt;strong>接口&lt;/strong>将调用者和实现者隔离开，大大促进了代码的复用和扩展。面向接口编程可以让系统变得灵活，当使用接口去引用具体的类型时，就需要&lt;strong>虚表&lt;/strong>来辅助运行时代码的执行。有了虚表，我们可以很方便地进行动态分派，它是运行时多态的基础。&lt;/p>
&lt;p>在代码的运行方式中，&lt;strong>并发&lt;/strong>是&lt;strong>并行&lt;/strong>的基础，是同时与多个任务打交道的能力；并行是并发的体现，是同时处理多个任务的手段。&lt;strong>同步&lt;/strong>阻塞后续操作，&lt;strong>异步&lt;/strong>允许后续操作。被广泛用于异步操作的 Promise 代表未来某个时刻会得到的结果，async/await 是 Promise 的封装，一般用状态机来实现。&lt;/p>
&lt;p>&lt;strong>泛型编程&lt;/strong>通过参数化让数据结构像函数一样延迟绑定，提升其通用性，类型的参数可以用接口约束，使类型满足一定的行为，同时，在使用泛型结构时，我们的代码也需要更高的抽象度。&lt;/p></content></entry><entry><title type="text">Rust01——内存：栈与堆？？</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/rust/prepare/01_memory/"/><id>https://quakewang.github.io/tech/rust/prepare/01_memory/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-06-14T00:00:00+00:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h2 id="rust--01--内存值放在栈上还是堆上">RUST--01 ｜ 内存：值，放在栈上还是堆上？？&lt;/h2>
&lt;h3 id="牢骚话">牢骚话&lt;/h3>
&lt;p>学习 Java、Python 或者 Scala 的时候，通常都会从最基本的语法讲起，为何谈起 Rust 却偏偏要从这些较为抽象的基础知识谈起呢？其实不然，从我自己的经历来说，吃过基础知识没学透，后期回来补课的痛苦。。。&lt;/p>……</summary><content type="html">&lt;h2 id="rust--01--内存值放在栈上还是堆上">RUST--01 ｜ 内存：值，放在栈上还是堆上？？&lt;/h2>
&lt;h3 id="牢骚话">牢骚话&lt;/h3>
&lt;p>学习 Java、Python 或者 Scala 的时候，通常都会从最基本的语法讲起，为何谈起 Rust 却偏偏要从这些较为抽象的基础知识谈起呢？其实不然，从我自己的经历来说，吃过基础知识没学透，后期回来补课的痛苦。。。&lt;/p>
&lt;p>比如，以最基础的内存为例，很多人其实并没有搞懂什么时候数据应该放在栈上，什么时候应该在堆上，直到工作中实际出现问题了，才意识到数据的存放方式居然会严重影响并发安全，无奈回头重新补基础，时间精力的耗费都很大。&lt;/p>
&lt;p>作为一名开发者，会遇见很多工具、框架和语言，但这类东西无论怎么变，底层的逻辑都是通用的，正所谓“万变不离其宗”。&lt;/p>
&lt;p>在学习一门新的语言中，最基本的概念就是代码中的变量和值，而存放它们的地方是内存，那么你真的有了解过内存吗？？&lt;/p>
&lt;h3 id="内存">内存&lt;/h3>
&lt;p>从写代码开始，我们就无时无刻不和内存在打交道。比如下面这行代码：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;hello world&amp;#34;&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">to_string&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>首先，“hello world”作为一个字符串常量（string literal），在编译时被存入可执行文件的 .RODATA 段（GCC）或者 .RDATA 段（VC++），然后在程序加载时，获得一个固定的内存地址。当执行“hello world”.to_string() 时，在堆上，一块新的内存被分配出来，并把“hello world”逐个字节拷贝过去。&lt;/p>
&lt;p>当我们把堆上的数据赋值给 s 时，s 作为分配在栈上的一个变量，它需要知道堆上内存的地址，另外由于堆上的数据大小不确定且可以增长，我们还需要知道它的长度以及它现在有多大。&lt;/p>
&lt;p>最终，为了表述这个字符串，我们使用了三个 word：&lt;/p>
&lt;ul>
&lt;li>第一个表示指针;&lt;/li>
&lt;li>第二个表示字符串的当前长度（11）;&lt;/li>
&lt;li>第三个表示这片内存的总容量（11）;&lt;/li>
&lt;/ul>
&lt;p>在 64 位系统下，三个 word 是 24 个字节。也可以看下图，更直观一些：&lt;/p>
&lt;p>&lt;img src="https://quakewang.github.io/tech/rust/images/01_A_memory.jpg" alt="memory">&lt;/p>
&lt;p>刚才例子中的字符串的内容在堆上，而指向字符串的指针等信息在栈上，那么有个问题就是：&lt;strong>数据什么时候可以放在栈上，什么时候需要放在堆上呢&lt;/strong>？&lt;/p>
&lt;p>这个问题也是比较考验程序员的基本功是否扎实的，很多使用自动内存管理语言比如 Java/Python 的开发者，可能有一些模糊的印象或者规则：&lt;/p>
&lt;ul>
&lt;li>基本类型（primitive type）存储在栈上，对象存储在堆上；&lt;/li>
&lt;li>少量数据存储在栈上，大量的数据存储在堆上。&lt;/li>
&lt;/ul>
&lt;p>这么回答，虽然对，但并没有抓到实质。如果在工作中只背规则套公式，一遇到特殊情况就容易懵，但是如果明白公式背后的推导逻辑，即使忘了，也很快能通过简单思考找到答案，所以接下来我们深挖堆和栈的设计原理，看看它们到底是如何工作的。（btw 如果连公式都不会背的话 emmmm。。。。。dddd😁）&lt;/p>
&lt;h3 id="栈">栈&lt;/h3>
&lt;p>栈是程序运行的基础。每当一个函数被调用时，一块连续的内存就会在栈顶被分配出来，这块内存被称为帧（frame）。&lt;/p>
&lt;p>栈是自顶向下增长的，一个程序的调用栈最底部，除去入口帧（entry frame），就是 main() 函数对应的帧，而随着 main() 函数一层层调用，栈会一层层扩展；调用结束，栈又会一层层回溯，把内存释放回去。&lt;/p>
&lt;p>在调用的过程中，&lt;strong>一个新的帧会分配足够的空间存储寄存器的上下文&lt;/strong>。在函数里使用到的通用寄存器会在栈保存一个副本，当这个函数调用结束，通过副本，可以恢复出原本的寄存器的上下文，就像什么都没有经历一样。此外，函数所需要使用到的局部变量，也都会在帧分配的时候被预留出来。&lt;/p>
&lt;p>整个过程可以再看看这张图辅助理解：&lt;/p>
&lt;p>&lt;img src="https://quakewang.github.io/tech/rust/images/01_B_stack.jpg" alt="stack">&lt;/p>
&lt;p>那一个函数运行时，怎么确定究竟需要多大的帧呢？这要归功于编译器。在编译并优化代码的时候，一个函数就是一个最小的编译单元。&lt;/p>
&lt;p>在这个函数里，编译器得知道要用到哪些寄存器、栈上要放哪些局部变量，而这些都要在编译时确定。所以编译器就需要明确每个局部变量的大小，以便于预留空间。&lt;/p>
&lt;p>于是乎我们可以这么理解：&lt;strong>在编译时，一切无法确定大小或者大小可以改变的数据，都无法安全地放在栈上，最好放在堆上&lt;/strong>。比如一个函数，参数是字符串：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fn&lt;/span> &lt;span class="nf">say_name&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>: &lt;span class="nb">String&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// 调用
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">say_name&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Lindsey&amp;#34;&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">to_string&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">say_name&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Rosie&amp;#34;&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">to_string&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>字符串的数据结构，在编译时大小不确定，运行时执行到具体的代码才知道大小。比如上面的代码，“Lindsey”和“Rosie”的长度不一样，say_name() 函数只有在运行的时候，才知道参数的具体的长度。所以，我们无法把字符串本身放在栈上，只能先将其放在堆上，然后在栈上分配对应的指针，引用堆上的内存。&lt;/p>
&lt;h3 id="放在栈上的问题">放在栈上的问题&lt;/h3>
&lt;p>从刚才的图中也可以直观看到，栈上的内存分配是非常高效的。只需要改动栈指针（stack pointer），就可以预留相应的空间；把栈指针改动回来，预留的空间又会被释放掉。预留和释放只是动动寄存器，不涉及额外计算、不涉及系统调用，因而效率很高。&lt;/p>
&lt;p>所以理论上说，只要可能，我们应该把变量分配到栈上，这样可以达到更好的运行速度。那为什么在实际工作中，我们又要避免把大量的数据分配在栈上呢？&lt;strong>这主要是考虑到调用栈的大小，避免栈溢出（stack overflow）&lt;/strong>。&lt;/p>
&lt;p>一旦当前程序的调用栈超出了系统允许的最大栈空间，无法创建新的帧，来运行下一个要执行的函数，就会发生栈溢出，这时程序会被系统终止，产生崩溃信息。过大的栈内存分配是导致栈溢出的原因之一，更广为人知的原因是递归函数没有妥善终止。一个递归函数会不断调用自己，每次调用都会形成一个新的帧，如果递归函数无法终止，最终就会导致栈溢出。&lt;/p>
&lt;h3 id="堆">堆&lt;/h3>
&lt;p>栈虽然使用起来很高效，但它的局限也显而易见。&lt;strong>当需要动态大小的内存时，只能使用堆&lt;/strong>，比如可变长度的数组、列表、哈希表、字典，它们都分配在堆上。&lt;/p>
&lt;p>堆上分配内存时，一般都会预留一些空间，这是最佳实践。比如你创建一个列表，并往里添加两个值：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-rust" data-lang="rust">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">let&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">mut&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">arr&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">Vec&lt;/span>::&lt;span class="n">new&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">arr&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">arr&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这个列表实际预留的大小是 4，并不等于其长度 2。这是因为堆上内存分配会使用 libc 提供的 malloc() 函数，其内部会请求操作系统的系统调用，来分配内存。系统调用的代价是昂贵的，所以要避免频繁地 malloc()。&lt;/p>
&lt;p>对上面的代码来说，如果说需要多少就分配多少，那列表每次新增值，都要新分配一大块的内存，先拷贝已有数据，再把新的值添加进去，最后释放旧的内存，这样效率很低。所以在堆内存分配时，预留的空间大小 4 会大于需要的实际大小 2。&lt;/p>
&lt;p>除了动态大小的内存需要被分配到堆上外，&lt;strong>动态生命周期的内存也需要分配到堆上&lt;/strong>。&lt;/p>
&lt;p>上文中我们讲到，栈上的内存在函数调用结束之后，所使用的帧被回收，相关变量对应的内存也都被回收待用。所以栈上内存的生命周期是不受开发者控制的，并且局限在当前调用栈。而堆上分配出来的每一块内存需要显式地释放，这就使堆上内存有更加灵活的生命周期，可以在不同的调用栈之间共享数据。&lt;/p>
&lt;p>&lt;img src="https://quakewang.github.io/tech/rust/images/01_C_heap.jpg" alt="heap">&lt;/p>
&lt;h3 id="放在堆上的问题">放在堆上的问题&lt;/h3>
&lt;p>然而，堆内存的这种灵活性也给内存管理带来很多挑战。&lt;/p>
&lt;p>如果手工管理堆内存的话，堆上内存分配后忘记释放，就会造成&lt;strong>内存泄漏&lt;/strong>。一旦有内存泄漏，程序运行得越久，就越吃内存，最终会因为占满内存而被操作系统终止运行。&lt;/p>
&lt;p>如果堆上内存被多个线程的调用栈引用，该内存的改动要特别小心，需要加锁以独占访问，来避免潜在的问题。比如说，一个线程在遍历列表，而另一个线程在释放列表中的某一项，就可能访问野指针，导致&lt;strong>堆越界&lt;/strong>（heap out of bounds）。而堆越界是第一大内存安全问题。&lt;/p>
&lt;p>如果堆上内存被释放，但栈上指向堆上内存的相应指针没有被清空，就有可能&lt;strong>发生使用已释放内存（use after free）的情况&lt;/strong>，程序轻则崩溃，重则隐含安全隐患。根据&lt;a href="https://github.com/Microsoft/MSRC-Security-Research/blob/master/presentations/2019_02_BlueHatIL/2019_01%20-%20BlueHatIL%20-%20Trends%2C%20challenge%2C%20and%20shifts%20in%20software%20vulnerability%20mitigation.pdf">微软安全反应中心（MSRC）的研究&lt;/a>，这是第二大内存安全问题。&lt;/p>
&lt;h3 id="小结">小结&lt;/h3>
&lt;p>对于&lt;strong>存入栈上的值，它的大小在编译期就需要确定&lt;/strong>。栈上存储的变量生命周期在当前调用栈的作用域内，无法跨调用栈引用。&lt;/p>
&lt;p>&lt;strong>堆可以存入大小未知或者动态伸缩的数据类型&lt;/strong>。堆上存储的变量，其生命周期从分配后开始，一直到释放时才结束，因此堆上的变量允许在多个调用栈之间引用。但也导致堆变量的管理非常复杂，手工管理会引发很多内存安全性问题，而自动管理，无论是 &lt;a href="https://en.wikipedia.org/wiki/Tracing_garbage_collection">GC&lt;/a> 还是 &lt;a href="https://en.wikipedia.org/wiki/Automatic_Reference_Counting">ARC&lt;/a>，都有性能损耗和其它问题。&lt;/p>
&lt;p>一句话对比总结就是：&lt;strong>栈上存放的数据是静态的，固定大小，固定生命周期；堆上存放的数据是动态的，不固定大小，不固定生命周期&lt;/strong>。&lt;/p></content></entry><entry><title type="text">MySQL——Access denied for user 'root'@'localhost' (using password: YES) 问题解决</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/mysql_access_problem/"/><id>https://quakewang.github.io/tech/mysql_access_problem/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-04-18T00:00:00+00:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h3 id="遇见问题">遇见问题？？&lt;/h3>
&lt;p>这两天在折腾一个数仓测试环境的迁移，MySQL 自然是必不可少缺少的咯，因为是测试环境，配置都是按最方便的来做，配置过程可参考：&lt;a href="https://github.com/QuakeWang/BigData-HowToConfigure/blob/main/005_MySQL/mysql.md">MySQL 安装&lt;/a>&lt;/p>……</summary><content type="html">&lt;h3 id="遇见问题">遇见问题？？&lt;/h3>
&lt;p>这两天在折腾一个数仓测试环境的迁移，MySQL 自然是必不可少缺少的咯，因为是测试环境，配置都是按最方便的来做，配置过程可参考：&lt;a href="https://github.com/QuakeWang/BigData-HowToConfigure/blob/main/005_MySQL/mysql.md">MySQL 安装&lt;/a>&lt;/p>
&lt;p>前一天使用都是正常的，结果第二天不知什么原因，在使用 Maxwell 进行增量同步业务数据到 HDFS 过程中，爆出以下错误：&lt;code>java.sql.SQLException: Access denied for user 'root'@'aliyun001' (using password: YES)&lt;/code> 这是一个常见的错误，遇到好几次，所以记录以下。&lt;/p>
&lt;h3 id="解决">解决？？？&lt;/h3>
&lt;p>遇到该问题，立刻就尝试使用&lt;code>mysql -u root -p&lt;/code>来登录数据库看看，结果仍然报错，同上。这时意识到数据库是进不去了。。。&lt;/p>
&lt;p>于是乎，可以先设置跳过密码：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">vim /etc/my.cnf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">skip-grant-tables &lt;span class="c1">#在[mysqld]下面添加这一行，忽略权限表&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>重启 MySQL：&lt;code>sudo systemctl restart mysqld.service&lt;/code>&lt;/p>
&lt;p>进入之后选择 &lt;code>use mysql&lt;/code>，然后 &lt;code>select user, host from user;&lt;/code> 出现的结果令人惊讶！！！没有 root 用户了？？？没有就自己造一个！！！&lt;/p>
&lt;p>养成好习惯先刷新一下：&lt;code>flush privileges;&lt;/code>&lt;/p>
&lt;p>创建&lt;code>create user 'root'@'localhost' identified by '123456';&lt;/code>，然后报错：&lt;code>ERROR 1396 (HY000): Operation CREATE USER failed for 'root'@'localhost'&lt;/code>，估计应该是没删干净？？再删一下：&lt;code>drop user root@'localhost';&lt;/code>，刷新一下；这个时候再创建就 ok 了~~&lt;/p>
&lt;p>有了 root 用户之后，再给权限：&lt;code>mysql&amp;gt; GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION; #赋予所有库所有表操作权限&lt;/code>；刷新一下~~&lt;/p>
&lt;p>再回到 &lt;code>/etc/my.cnf&lt;/code> 删除 &lt;code>skip-grant-tables&lt;/code>。重启数据库，这个时候就可以正常使用了~~&lt;/p>
&lt;p>如果为了方便还可以再设置一下 host 为 %：&lt;code>update user set host=&amp;quot;%&amp;quot; where user=&amp;quot;root&amp;quot;;&lt;/code>&lt;/p></content></entry><entry><title type="text">Windows 配置 Scala 开发环境</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/bigdata/scala/"/><id>https://quakewang.github.io/tech/bigdata/scala/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2023-03-23T00:00:00+00:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="windows-配置-scala-开发环境">Windows 配置 Scala 开发环境&lt;/h1>
&lt;h2 id="零前言">零、前言&lt;/h2>
&lt;p>谈起现如今的大数据开发框架，那么 Spark 想必是众所周知的。而 Spark 就是使用 Scala 语言编写的。所以问题来了，该如何配置一套 Scala 的环境呢？&lt;/p>……</summary><content type="html">&lt;h1 id="windows-配置-scala-开发环境">Windows 配置 Scala 开发环境&lt;/h1>
&lt;h2 id="零前言">零、前言&lt;/h2>
&lt;p>谈起现如今的大数据开发框架，那么 Spark 想必是众所周知的。而 Spark 就是使用 Scala 语言编写的。所以问题来了，该如何配置一套 Scala 的环境呢？&lt;/p>
&lt;p>其实，有了 Java 的底子之后，配置一套 Scala 开发环境并不是很难，因为 Scala 一门以 JVM 为运行环境并将面向对象和函数式编程的最佳特性结合在一起的
静态类型编程语言，支持面向对象和函数式编程。&lt;/p>
&lt;h2 id="一scala-环境搭建">一、Scala 环境搭建&lt;/h2>
&lt;p>前文提到，Scala 是运行在 JVM 上的，所以首先先保证开发环境已经配置了 JDK，这里不做过多赘述。（我使用的 JDK1.8）&lt;/p>
&lt;p>1.下载所需要的 Scala 版本，&lt;a href="https://www.scala-lang.org/download/">download&lt;/a>；&lt;/p>
&lt;p>2.将下载好的 zip 文件压解至无中文的目录下，最好也不要有空格；&lt;/p>
&lt;p>3.打开 Windows 的系统属性中的环境变量，配置 Scala 的环境变量：&lt;code>SCALA_HOME&lt;/code> 以及所属目录：&lt;code>D:\DevelopmentTool\scala-2.12.11&lt;/code>&lt;/p>
&lt;p>&lt;img src="../../../imag/tech/scala/scala01_home.png" alt="scala01_home">&lt;/p>
&lt;p>配置 path 路径，将 bin 目录添加至系统环境 &lt;code>%SCALA_HOME%\bin&lt;/code>&lt;/p>
&lt;p>&lt;img src="../../../imag/tech/scala/scala02_path.png" alt="scala02_path">&lt;/p>
&lt;p>4.测试&lt;/p>
&lt;p>打开 terminal 终端，输入 scala 出现如下图所示表示配置好环境~~&lt;/p>
&lt;p>&lt;img src="../../../imag/tech/scala/scala03_test.png" alt="scala03_test">&lt;/p>
&lt;h2 id="二在-idea-中配置-scala-开发环境">二、在 IDEA 中配置 Scala 开发环境&lt;/h2>
&lt;p>IDEA 懂的都懂 好用就完事了！！！下面将演示如何在 IDEA 集成 Scala 开发环境。&lt;/p>
&lt;p>1.在 Setting 的 plugins 中搜素 Scala -&amp;gt;点击 Install-&amp;gt;点击 ok-&amp;gt;点击 apply，重启 IDEA；&lt;/p>
&lt;p>&lt;img src="../../../imag/tech/scala/scala04_idea.png" alt="scala04_idea">&lt;/p>
&lt;p>2.创建一个 projet，默认是不支持 Scala 的开发。需要手动引入 Scala 框架，在项目上，点击右键-&amp;gt; Add Framework Support... -&amp;gt;选择 Scala-&amp;gt;点击 OK。&lt;/p>
&lt;p>&lt;img src="../../../imag/tech/scala/scala05_idea.png" alt="scala05_idea">&lt;/p>
&lt;p>&lt;em>注意：如果是第一次引入框架，Use libary 看不到，需要选择你的 Scala 安装目录，然后工具就会自动识别，就会显示 user libary。&lt;/em>&lt;/p>
&lt;p>3.测试&lt;/p>
&lt;p>&lt;img src="../../../imag/tech/scala/scala06_idea.png" alt="scala06_idea">&lt;/p>
&lt;p>以上我们已经完成了 Scala 的开发环境，可以完成一些基础的相关。&lt;/p>
&lt;h2 id="三配置-spark-开发环境">三、配置 Spark 开发环境&lt;/h2>
&lt;p>1.创建 Spark 项目，添加相关依赖：&lt;/p>
&lt;pre tabindex="0">&lt;code>&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.apache.spark&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;spark-core_2.12&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;3.0.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code>&lt;/pre>&lt;p>添加依赖之后，就可以使用 Spark 相关的 API，但是在运行过程中，控制台可以会出现一些神奇的错误，如下所示：&lt;/p>
&lt;pre tabindex="0">&lt;code>ERROR Shell: Failed to locate the winutils binary in the hadoop binary path
java.io.IOException: Could not locate executable null\bin\winutils.exe in the Hadoop binaries.
at org.apache.hadoop.util.Shell.getQualifiedBinPath(Shell.java:382)
at org.apache.hadoop.util.Shell.getWinUtilsPath(Shell.java:397)
at org.apache.hadoop.util.Shell.&amp;lt;clinit&amp;gt;(Shell.java:390)
at org.apache.hadoop.util.StringUtils.&amp;lt;clinit&amp;gt;(StringUtils.java:80)
at org.apache.hadoop.security.SecurityUtil.getAuthenticationMethod(SecurityUtil.java:611)
at org.apache.hadoop.security.UserGroupInformation.initialize(UserGroupInformation.java:274)
at org.apache.hadoop.security.UserGroupInformation.ensureInitialized(UserGroupInformation.java:262)
at org.apache.hadoop.security.UserGroupInformation.loginUserFromSubject(UserGroupInformation.java:807)
at org.apache.hadoop.security.UserGroupInformation.getLoginUser(UserGroupInformation.java:777)
at org.apache.hadoop.security.UserGroupInformation.getCurrentUser(UserGroupInformation.java:650)
at org.apache.spark.util.Utils$.$anonfun$getCurrentUserName$1(Utils.scala:2412)
at scala.Option.getOrElse(Option.scala:189)
at org.apache.spark.util.Utils$.getCurrentUserName(Utils.scala:2412)
at org.apache.spark.SparkContext.&amp;lt;init&amp;gt;(SparkContext.scala:303)
at org.erxi.spark.core.rdd.operator.transform.AdClickCount$.main(AdClickCount.scala:8)
at org.erxi.spark.core.rdd.operator.transform.AdClickCount.main(AdClickCount.scala)
&lt;/code>&lt;/pre>&lt;p>这是因为在程序中使用了 Hadoop 相关的内容，比如写入文件到 HDFS。出现这个问题并不是程序的错误，而是windows 系统用到了 hadoop 相关的服务，解决办法是通过配置关联到 windows 的系统依赖就可以了。&lt;/p>
&lt;h2 id="2解决异常">2.解决异常&lt;/h2>
&lt;p>安装 Spark：到官网 &lt;a href="https://spark.apache.org/downloads.html">https://spark.apache.org/downloads.html&lt;/a> 选择合适的版本下载，注意 Spark 与Hadoop 版本选择要相对应，建议下载预编译（Pre-built）好的版本，省得麻烦。解压文件，然后与配置 Scala 环境类似配置对应的 &lt;code>SPARK_HOME&lt;/code> 与 path 变量 &lt;code>%SPARK_HOME%\bin&lt;/code>;&lt;/p>
&lt;p>安装 Hadoop：到官网 &lt;a href="https://hadoop.apache.org/releases.html">https://hadoop.apache.org/releases.html&lt;/a> 下载与上边的 Spark 对应的版本。后与配置 Scala 环境类似配置对应的 &lt;code>HADOOP_HOME&lt;/code> 与 path 变量 &lt;code>%HADOOP_HOME%\bin&lt;/code>。&lt;/p>
&lt;p>除此之外，还需要到这里 &lt;a href="https://github.com/cdarlint/winutils">https://github.com/cdarlint/winutils&lt;/a> 下载对应版本的 bin 目录中的 hadoop.dll 和 winutils.exe，复制到 hadoop 目录的 bin 目录下。&lt;/p>
&lt;p>完成上述操作之后，已经在 Windows 环境下搭建了可用于测试的 Spark 环境和 Hadoop 环境。最后还需要再 IDEA 中导入一下 HADOOP_HOME，这样运行程序就不会报错啦~~&lt;/p>
&lt;p>在 IDEA 中配置 Run Configuration，添加 HADOOP_HOME 变量：&lt;/p>
&lt;p>&lt;img src="../../../imag/tech/scala/spark01_edit.png" alt="spark01_edit">&lt;/p>
&lt;p>&lt;img src="../../../imag/tech/scala/spark02_conf.png" alt="spark02_conf">&lt;/p></content></entry><entry><title type="text">设计模式（一）-- 单例模式 001</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/designpattern/001_signle%E4%B8%80/"/><id>https://quakewang.github.io/tech/designpattern/001_signle%E4%B8%80/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2022-02-28T00:00:00+00:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h2 id="单例模式">单例模式&lt;/h2>
&lt;h3 id="为什么使用单例模式">为什么使用单例模式？&lt;/h3>
&lt;p>&lt;strong>单例设计模式&lt;/strong>：一个类只允许创建一个对象（或者实例），那么这个类就是一个单例类，这种设计模式就叫做单例设计模式，简称单例模式。&lt;/p>……</summary><content type="html">&lt;h2 id="单例模式">单例模式&lt;/h2>
&lt;h3 id="为什么使用单例模式">为什么使用单例模式？&lt;/h3>
&lt;p>&lt;strong>单例设计模式&lt;/strong>：一个类只允许创建一个对象（或者实例），那么这个类就是一个单例类，这种设计模式就叫做单例设计模式，简称单例模式。&lt;/p>
&lt;p>单例模式的概念并不是很难，一看就能明白。接下来我们思考一下，为什么需要单例这种设计模式？它能解决哪些问题？&lt;/p>
&lt;h3 id="实战案例处理资源访问冲突">实战案例：处理资源访问冲突&lt;/h3>
&lt;p>咱们先来看第一个例子。该例子中，我们自定义实现了一个往文件中打印日志的 Logger 类。具体的实现代码如下：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">Logger&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FileWriter&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">Logger&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">File&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">file&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">File&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/Users/zhangsan/log.txt&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FileWriter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">file&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// true表示追加写入&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// Logger类的应用示例：&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">UserController&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Logger&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">logger&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Logger&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">login&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">username&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">password&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// ...省略业务逻辑代码...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">username&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34; logined!&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">OrderController&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Logger&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">logger&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Logger&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">create&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OrderVo&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">order&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// ...省略业务逻辑代码...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Created an order: &amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">order&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">toString&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>上述代码的功能并不是很复杂，但请停下来思考一下，这段代码存在什么问题。&lt;/p>
&lt;p>细心的同学可能已经注意到了，所有的日子都写入到同一个文件 &lt;code>/Users/zhangsan/log.txt&lt;/code> 中。在 UserController 和 OrderController 中，分别创建了两个 Logger 对象。在 Web 容器的 Servlet 多线程环境下，如果两个 Servlet 线程同时分别执行 login() 和 create() 两个函数，并且同时写日子到 log.txt 文件中，那么就有可能存在日子信息相互覆盖的情况。&lt;/p>
&lt;p>为什么会出现相互覆盖呢？可以这样类比着理解。在多线程环境下，如果两个线程同时给同一个共享变量加 1，因为共享变量是竞争资源，所以，共享变量最后的结果有可能并不是加 2，而是只加了 1。同理，这里的 log.txt 文件也是竞争资源，两个线程同时往里面写数据，就有可能会存在相互覆盖的情况。&lt;/p>
&lt;p>&lt;img src="../../../imag/DesignPattern/single01.png" alt="single01">&lt;/p>
&lt;p>那么该如何来解决和这个问题呢？通常的思路应该是加锁：给 log() 函数加互斥锁（Java 中可以通过 synchronized 的关键字），同一时刻只允许一个线程调用 log() 函数。具体的代码实现如下：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">Logger&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FileWriter&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">Logger&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">File&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">file&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">File&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/Users/zhangsan/log.txt&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FileWriter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">file&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// true表示追加写入&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">synchronized&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mesasge&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>不过，仔细思考一下，这真的能解决多线程写入日志时相互覆盖的问题吗？答案是否定的！这是因为这种锁是一个对象级别的锁，一个对象在不同的线程下同时调用 log() 函数，会被强制要求顺序执行。但是，不同的对象之间并不能共享同一把锁。在不同的线程下，通过不同的对象调用执行 log() 函数，锁并不会起作用，任然有可能存在写入日志相互覆盖的问题。&lt;/p>
&lt;p>&lt;img src="../../../imag/DesignPattern/single02.png" alt="subgle02">&lt;/p>
&lt;p>这里稍微补充一下，在刚刚的讲解和给出代码的中，故意“隐藏”了一个事实：我们给 log() 函数加不加对象级别的锁，其实都没有关系。因为 FileWriter 本身就是现场安全的，它的内部实现中本身就假了对象级别的锁，因此，在外层调用 write() 函数的时候，再加对象级别的锁实际上是多此一举。因为不同的 Logger 对象不共享 FileWriter 对象，所以 FileWriter 对象级别的锁也解决不了数据写入相互覆盖的问题。&lt;/p>
&lt;p>那么问题来了，该如何解决呢？实际上，要解决这个问题并不是很难。我们只需要吧对象级别的锁换成类级别的锁就可以了。让所有的对象都共享同一把锁。这样就避免了不同对象之间同时调用 log() 函数，而导致的日志覆盖的问题。具体的代码实现如下所示：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">Logger&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FileWriter&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">Logger&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">File&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">file&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">File&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/Users/zhangsan/log.txt&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FileWriter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">file&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// true表示追加写入&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">synchronized&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// 类级别的锁&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mesasge&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>除了使用类级别锁之外，实际上，解决资源竞争问题的办法还有很多，分布式锁是最常听到的一种解决方案。不过，实现一个安全可靠、无 bug、高性能的分布式锁，并不是件容易的事情。除此之外，并发队列（比如 Java 中的 BlockingQueue）也可以解决这个问题：多个线程同时往并发队列里写日志，一个单独的线程负责将并发队列中的数据，写入到日志文件。这种方式实现起来也稍微有点复杂。&lt;/p>
&lt;p>相对于这两种解决方案，单例模式的解决思路就简单一些了。单例模式相对于之前类级别锁的好处是，不用创建那么多 Logger 对象，一方面节省内存空间，另一方面节省系统文件句柄（对于操作系统来说，文件句柄也是一种资源，不能随便浪费）。&lt;/p>
&lt;p>我们将 Logger 设计成一个单例类，程序中只允许创建一个 Logger 对象，所有的线程共享使用的这一个 Logger 对象，共享一个 FileWriter 对象，而 FileWriter 本身是对象级别线程安全的，也就避免了多线程情况下写日志会互相覆盖的问题。&lt;/p>
&lt;p>按照这个设计思路，我们实现了 Logger 单例类。具体代码如下所示：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">Logger&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FileWriter&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Logger&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Logger&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">Logger&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">File&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">file&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">File&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/Users/shangsan/log.txt&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">FileWriter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">file&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// true表示追加写入&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Logger&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getInstance&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mesasge&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// Logger类的应用示例：&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">UserController&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">login&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">username&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">password&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// ...省略业务逻辑代码...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getInstance&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">username&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34; logined!&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">OrderController&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">create&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OrderVo&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">order&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// ...省略业务逻辑代码...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">Logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getInstance&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Created a order: &amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">order&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">toString&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="实战案例二表示全局唯一类">实战案例二：表示全局唯一类&lt;/h3>
&lt;p>从业务概念上，如果有些数据在系统中只应保持一份，那就适合设计为单例类。&lt;/p>
&lt;p>比如，配置信息类。在系统中，我们只有一个配置文件，当配置文件被加载到内存后，以对象的形式存在，也理所应当只有一份。&lt;/p>
&lt;p>再比如，唯一递增 ID 号码生成器，如果程序中有两个对象，就会存在生成重复 ID 的情况，所以我们应该将 ID 生成器类设计为单例。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">java.util.concurrent.atomic.AtomicLong&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// AtomicLong 是一个 Java 并发库中提供的一个原子变量类型,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// 它将一些线程不安全需要加锁的复合操作封装为了线程安全的原子操作，&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// 比如下面会用到的 incrementAndGet().&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AtomicLong&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AtomicLong&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">0&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">IdGenerator&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getInstance&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getId&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">incrementAndGet&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// IdGenerator 使用举例&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kt">long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getInstance&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">getId&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="如何实现一个单例">如何实现一个单例？&lt;/h3>
&lt;p>概括起来，要实现一个单例，我们需要关注的无外乎下面几个：&lt;/p>
&lt;ul>
&lt;li>构造函数需要是 private 访问权限的，这样才能避免外部通过 new 创建实例；&lt;/li>
&lt;li>考虑对象创建时的线程安全问题；&lt;/li>
&lt;li>考虑是否支持延迟加载；&lt;/li>
&lt;li>考虑 getInstance() 是否加锁（性能是否高）。&lt;/li>
&lt;/ul>
&lt;h4 id="1-饿汉式">1. 饿汉式&lt;/h4>
&lt;p>饿汉式的实现方式比较简单。在类加载的时候，instance 静态实例就已经创建并初始化好了，所以 instance 实例的创建过程是线程安全的。不过，这样的实现方式不支持延迟加载（在真正用到 IdGenerator 的时候，再创建实例），从名字中我们也可以看出这一点。具体的代码实现如下所示：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AtomicLong&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AtomicLong&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">0&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">IdGenerator&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getInstance&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getId&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">incrementAndGet&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>有人觉得这种实现方式不好，因为不支持延迟加载，如果实例占用资源多（比如占用内存多）或初始化耗时长（比如需要加载各种配置文件），提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。不过，从我的角度来说，并不是很认同这种观点。&lt;/p>
&lt;p>如果初始化耗时长，那我们最好不要等到真正要用它的时候，才去执行这个耗时长的初始化过程，这会影响到系统的性能（比如，在响应客户端接口请求的时候，做这个初始化操作，会导致此请求的响应时间变长，甚至超时）。采用饿汉式实现方式，将耗时的初始化操作，提前到程序启动的时候完成，这样就能避免在程序运行的时候，再去初始化导致的性能问题。&lt;/p>
&lt;p>如果实例占用资源多，按照 fail-fast 的设计原则（有问题及早暴露），那我们也希望在程序启动时就将这个实例初始化好。如果资源不够，就会在程序启动的时候触发报错（比如 Java 中的 PermGen Space OOM），我们可以立即去修复。这样也能避免在程序运行一段时间后，突然因为初始化这个实例占用资源过多，导致系统崩溃，影响系统的可用性。&lt;/p>
&lt;h4 id="2-懒汉式">2. 懒汉式&lt;/h4>
&lt;p>有饿汉式，对应的，就有懒汉式。懒汉式相对于饿汉式的优势是支持延迟加载。具体的代码实现如下所示：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AtomicLong&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AtomicLong&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">0&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">IdGenerator&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">synchronized&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getInstance&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">==&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getId&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">incrementAndGet&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>不过懒汉式的缺点也很明显，我们给 getInstance() 这个方法加了一把大锁（synchronzed），导致这个函数的并发度很低。量化一下的话，并发度是 1，也就相当于串行操作了。而这个函数是在单例使用期间，一直会被调用。如果这个单例类偶尔会被用到，那这种实现方式还可以接受。但是，如果频繁地用到，那频繁加锁、释放锁及并发度低等问题，会导致性能瓶颈，这种实现方式就不可取了。&lt;/p>
&lt;h4 id="3-双重检测">3. 双重检测&lt;/h4>
&lt;p>饿汉式不支持延迟加载，懒汉式有性能问题，不支持高并发。那我们再来看一种既支持延迟加载、又支持高并发的单例实现方式，也就是双重检测实现方式。&lt;/p>
&lt;p>在这种实现方式中，只要 instance 被创建之后，即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了。所以，这种实现方式解决了懒汉式并发度低的问题。具体的代码实现如下所示：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AtomicLong&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AtomicLong&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">0&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">IdGenerator&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getInstance&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">==&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">synchronized&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// 此处为类级别的锁&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">==&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getId&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">incrementAndGet&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>网上有人说，这种实现方式有些问题。因为指令重排序，可能会导致 IdGenerator 对象被 new 出来，并且赋值给 instance 之后，还没来得及初始化（执行构造函数中的代码逻辑），就被另一个线程使用了。&lt;/p>
&lt;p>要解决这个问题，我们需要给 instance 成员变量加上 volatile 关键字，禁止指令重排序才行。实际上，只有很低版本的 Java 才会有这个问题。我们现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题（解决的方法很简单，只要把对象 new 操作和初始化操作设计为原子操作，就自然能禁止重排序）。关于这点的详细解释，跟特定语言有关，我就不展开讲了，感兴趣的同学可以自行研究一下。&lt;/p>
&lt;h4 id="4-静态内部类">4. 静态内部类&lt;/h4>
&lt;p>我们再来看一种比双重检测更加简单的实现方法，那就是利用 Java 的静态内部类。它有点类似饿汉式，但又能做到了延迟加载。具体是怎么做到的呢？我们先来看它的代码实现。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AtomicLong&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AtomicLong&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">0&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">IdGenerator&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">SingletonHolder&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">static&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getInstance&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">SingletonHolder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">instance&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getId&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">incrementAndGet&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>SingletonHolder 是一个静态内部类，当外部类 IdGenerator 被加载的时候，并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时，SingletonHolder 才会被加载，这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性，都由 JVM 来保证。所以，这种实现方法既保证了线程安全，又能做到延迟加载。&lt;/p>
&lt;h4 id="5-枚举">5. 枚举&lt;/h4>
&lt;p>最后，我们介绍一种最简单的实现方式，基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性，保证了实例创建的线程安全性和实例的唯一性。具体的代码如下所示：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">enum&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IdGenerator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">INSTANCE&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AtomicLong&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AtomicLong&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">0&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">long&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getId&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">incrementAndGet&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="重点回顾">重点回顾&lt;/h3>
&lt;h4 id="1-单例的定义">1. 单例的定义&lt;/h4>
&lt;p>单例设计模式（Singleton Design Pattern）理解起来非常简单。一个类只允许创建一个对象（或者叫实例），那这个类就是一个单例类，这种设计模式就叫作单例设计模式，简称单例模式。&lt;/p>
&lt;h4 id="2-单例的用处">2. 单例的用处&lt;/h4>
&lt;p>从业务概念上，有些数据在系统中只应该保存一份，就比较适合设计为单例类。比如，系统的配置信息类。除此之外，我们还可以使用单例解决资源访问冲突的问题。&lt;/p>
&lt;h4 id="3-单例的实现">3. 单例的实现&lt;/h4>
&lt;p>单例有下面几种经典的实现方式。&lt;/p>
&lt;ul>
&lt;li>饿汉式&lt;/li>
&lt;/ul>
&lt;p>饿汉式的实现方式，在类加载的期间，就已经将 instance 静态实例初始化好了，所以，instance 实例的创建是线程安全的。不过，这样的实现方式不支持延迟加载实例。&lt;/p>
&lt;ul>
&lt;li>懒汉式&lt;/li>
&lt;/ul>
&lt;p>懒汉式相对于饿汉式的优势是支持延迟加载。这种实现方式会导致频繁加锁、释放锁，以及并发度低等问题，频繁的调用会产生性能瓶颈。&lt;/p>
&lt;ul>
&lt;li>双重检测&lt;/li>
&lt;/ul>
&lt;p>双重检测实现方式既支持延迟加载、又支持高并发的单例实现方式。只要 instance 被创建之后，再调用 getInstance() 函数都不会进入到加锁逻辑中。所以，这种实现方式解决了懒汉式并发度低的问题。&lt;/p>
&lt;ul>
&lt;li>静态内部类&lt;/li>
&lt;/ul>
&lt;p>利用 Java 的静态内部类来实现单例。这种实现方式，既支持延迟加载，也支持高并发，实现起来也比双重检测简单。&lt;/p>
&lt;ul>
&lt;li>枚举&lt;/li>
&lt;/ul>
&lt;p>最简单的实现方式，基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性，保证了实例创建的线程安全性和实例的唯一性。&lt;/p></content></entry><entry><title type="text">Hadoop001——入门篇</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/bigdata/bigdata_hadoop001/"/><id>https://quakewang.github.io/tech/bigdata/bigdata_hadoop001/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2021-08-15T00:00:00+00:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="hadoop-极简入门">Hadoop 极简入门&lt;/h1>
&lt;h2 id="零前言">零、前言&lt;/h2>
&lt;p>在 2021 年初的时候，Apache 退休了一些 Hadoop 生态圈的子项目。再加上其 MapReduce 思想最为人诟病，因为不太友好的编写代码方式，需要高昂的维护成本以及较低的运行效率，唱衰 Hadoop 的声音（甚至对于整个大数据生态的质疑声）日益高涨。。&lt;/p>……</summary><content type="html">&lt;h1 id="hadoop-极简入门">Hadoop 极简入门&lt;/h1>
&lt;h2 id="零前言">零、前言&lt;/h2>
&lt;p>在 2021 年初的时候，Apache 退休了一些 Hadoop 生态圈的子项目。再加上其 MapReduce 思想最为人诟病，因为不太友好的编写代码方式，需要高昂的维护成本以及较低的运行效率，唱衰 Hadoop 的声音（甚至对于整个大数据生态的质疑声）日益高涨。。&lt;/p>
&lt;p>然而，MapReduce 作为一种编程范式，恐怕并没有那么容易被淘汰。纵使很多人说：你看 Spark 速度又快又稳定，这不是可以淘汰掉 Hadoop 的 MapReduce 吗？但是真的是这样吗？？&lt;/p>
&lt;p>所谓的快和慢都是相对而言的。某些互联网公司每天的离线调度任务动辄数十万起，这么庞大的基于 MapReduce 的离线计算如果要是用 Spark 来替代，与之相对应的是高昂的服务器成本。&lt;/p>
&lt;p>因此，我们可以说原来用 Hadoop MapReduce 能做的事情被更好更快的其他计算引擎来替代了，而不是 MapReduce 被淘汰了。而且后来的计算引擎也大都有借鉴 Map、Reduce 这类的概念！&lt;/p>
&lt;hr>
&lt;h2 id="一长话短说">一、长话短说&lt;/h2>
&lt;h3 id="11-hadoop-是什么">1.1 Hadoop 是什么？？&lt;/h3>
&lt;p>&lt;a href="https://hadoop.apache.org/">Hadoop&lt;/a> 是 Apache Software Foundation 开源的，根据 Google 开源的三篇大数据论文设计的，一个能够允许大量数据在计算机集群中，通过使用简单的编程模型进行分布式处理的框架。其设计的规模可从单一的服务器到数千台服务器，每一个均可提供局部运算和存储功能。Hadoop 并不依赖昂贵的硬件以支持高可用性。Hadoop 可以检测并处理应用层上的错误，并可以把错误转移到其他服务器上(让它错误，我在用别的服务器顶上就可以了)，所以 Hadoop 提供一个基于计算机集群的、高效性的服务。&lt;/p>
&lt;h3 id="12-主要优势">1.2 主要优势&lt;/h3>
&lt;p>主要拥有以下优势：&lt;/p>
&lt;ol>
&lt;li>高可靠性：Hadoop 底层维护多个数据副本，所以即使 Hadoop 某个计算元素或存储出现故障，也不会导致数据丢失；&lt;/li>
&lt;li>高扩展性：在集群间分配任务数据，可方便地扩展数以千计的结点；&lt;/li>
&lt;li>高效性：在 MapReduce 的思想下，Hadoop 是并行工作的，以加快任务处理的速度；&lt;/li>
&lt;li>高容错性：能够自动将失败任务重新分配。&lt;/li>
&lt;/ol>
&lt;h3 id="13-发展">1.3 发展&lt;/h3>
&lt;p>经过多年的发展，Hadoop 这个单词的意思也随之发生改变，由之前一个具体项目的名称，到现在提到 Hadoop 大多是指大数据的生态圈，包括许多现在火的一腿的项目，例如 Spark、Hive、HBase 等等。&lt;/p>
&lt;p>如同 Spring 框架有着最基础的几个模块 Context、Bean 和 Core。其余的模块和项目都是基于这些模块构建的。Hadoop 与之大体一样，也有最基础的几个模块：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Common&lt;/strong>：支持其它模块的公用工具包；&lt;/li>
&lt;li>&lt;strong>HDFS&lt;/strong>：一个可高吞吐访问应用数据的分布式文件系统；&lt;/li>
&lt;li>&lt;strong>Yarn&lt;/strong>：一个管理集群服务资源和任务调度的框架；&lt;/li>
&lt;li>&lt;strong>MapReduce&lt;/strong>：基于 Yarn 对于大数据集群进行并行计算的系统。&lt;/li>
&lt;/ul>
&lt;p>其他的，像 Hbase、Hive 等等都是在这几个模块基础上的高级抽象。&lt;strong>Common&lt;/strong> 模块是 Hadoop 最为基础的模块，负责为其余模块提供了像 I/O、操作文件系统、序列化和远程方法调用等最为基础的实现。（如果想深入了解 Hadoop 具体实现的小朋友，可以挑战自己阅读一下 Common 的源码~~）&lt;/p>
&lt;hr>
&lt;h2 id="二hdfs-基础概念">二、HDFS 基础概念&lt;/h2>
&lt;p>HDFS 是 “Hadoop Distributed File System”的首字母缩写，是一个分布式文件系统，说简单点就是为了存储文件。但是和其他的文件系统的不同之处是 HDFS 设计为运行在低成本的硬件上（因此在学习 Hadoop 入门的时候，可以使用 Linux 虚拟机搭建一套集群出来玩玩），且提供高可靠性的服务器。HDFS 设计满足大数据量，高吞吐的应用情况。&lt;/p>
&lt;p>为了更好地理解分布式文件系统，咱们先看看下面的这些概念：&lt;/p>
&lt;h3 id="21-文件">2.1 文件&lt;/h3>
&lt;p>咦？谈起文件，想必大家都很熟悉，在不同的行业中，文件也有着不同的意思。在计算机科学领域，文件是在存储设备中是 N 个字节序列。而从计算机使用者的角度而言，文件是对所有 I/O 设备的抽象。每个 I/O 设备都可以视为文件，包括磁盘、键盘和网络等等。文件这个简单而精致的概念其内涵是十分丰富的，它向应用程序提供了一个统一的视角，来看待系统中可能含有的各式各样的 I/O 设备。&lt;/p>
&lt;h3 id="22-文件系统">2.2 文件系统&lt;/h3>
&lt;p>那么一台计算机上肯定不止一个文件，成千上万的文件怎么管理呢？因此需要我们需要一种对文件进行管理的东西，即文件系统。文件系统是一种在计算机上存储和组织数据的方法，它使得对其访问和查找变得容易，文件系统使用文件和树形目录的抽象逻辑概念代替了硬盘和光盘等物理设备使用数据块的概念，用户使用文件系统来保存数据而不必关心数据实际保存在硬盘的地址为多少的数据块上，只需要记住这个文件的所属目录和文件名。在写入新数据之前，用户不必关心硬盘上的那个块地址没有被使用，硬盘上的存储空间管理(分配和释放)功能由文件系统自动完成，用户只需要记住数据被写入到了哪个文件中即可。&lt;/p>
&lt;h3 id="23-分布式文件系统">2.3 分布式文件系统&lt;/h3>
&lt;p>相对于单机的文件系统而言，分布式文件系统（Distributed file system）。是一种允许文件通过网络在多台主机上分享的文件系统，可让多计算机上的多用户分享文件和存储空间。&lt;/p>
&lt;p>在这样的文件系统中，客户端并非直接访问底层的数据存储区块和磁盘。而是通过网络，基于单机文件系统并借由特定的通信协议的帮助，来实现对于文件系统的读写。&lt;/p>
&lt;p>分布式文件系统需要拥有的最基本的能力是通过畅通网络 I/O 来实现数据的复制与容错。也就是说，一方面一个文件是分为多个数据块分布在多个设备中。另一方面，数据块有多个副本分布在不同的设备上。即使有一小部分的设备出现离线和宕机等情况，整体来说文件系统仍然可以持续运作而不会有数据损失。&lt;/p>
&lt;p>注意：分布式文件系统和分布式数据存储的界线是模糊的，但一般来说，分布式文件系统是被设计用在局域网，比较强调的是传统文件系统概念的延伸，并通过软件方法来达成容错的目的。而分布式数据存储，则是泛指应用分布式运算技术的文件和数据库等提供数据存储服务的系统。&lt;/p>
&lt;h3 id="24-hdfs">2.4 HDFS&lt;/h3>
&lt;p>HDFS 正是 Hadoop 中负责分布式文件系统的。HDFS 采用master/slave 架构。一个 HDFS 集群是由一个Namenode（可以理解为资本家老板） 和一定数目的 Datanodes（打工人） 组成。Namenode 是一个中心服务器，负责管理文件系统的命名空间以及文件的访问控制。集群中的 Datanode 一般是一个设备上部署一个，负责管理它所在节点上的存储。HDFS 暴露了文件系统的命名空间，用户能够以文件的形式在上面存储数据。&lt;/p>
&lt;p>实际上，一个文件会被分成一个或多个数据块，这些块存储在一组 Datanode 上。Namenode 执行文件系统的命名空间操作，比如打开、关闭、重命名文件或目录。它也负责确定数据块到具体 Datanode 设备的映射。Datanode 负责处理文件系统客户端的读写请求。在 Namenode 的统一调度下进行数据块的创建、删除和复制。为了保证文件系统的高可靠，往往需要另一个 Standby 的 Namenode 在 Actived Namenode 出现问题后，立刻接管文件系统。&lt;/p>
&lt;h4 id="hdfs-架构概述">HDFS 架构概述&lt;/h4>
&lt;ol>
&lt;li>NameNode（nn）：存储文件的 &lt;strong>元数据&lt;/strong>，如文件名、文件目录结构、文件属性（生成时间、副本数、文件权限），以及每个文件的 &lt;strong>块列表&lt;/strong> 和 &lt;strong>块所在的 DataNode&lt;/strong> 等等；&lt;/li>
&lt;li>DataNode（dn）：在本地文件系统 &lt;strong>存储文件块数据&lt;/strong>。以及 &lt;strong>块数据的校验和&lt;/strong>；&lt;/li>
&lt;li>SecondaryNameNode（2nn）：每隔一段时间对 &lt;strong>NameNode&lt;/strong> 元数据备份（把 NameNode 当做老板的话，SecondaryNameNode 就相当于小秘，但小秘毕竟是小秘备份的数据肯定没有老板完全，所以在掌握 Zookeeper 之后可以配置 HA，也就是说两个 NameNode 互相备份）。&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="三mapreduce-基础概念">三、MapReduce 基础概念&lt;/h2>
&lt;p>MapReduce 是一个使用简单的软件框架，基于它写出来的应用程序能够运行在由上千个商用机器组成的大型集群上，并以一种可靠容错的方式并行处理上 T 级别的数据集。&lt;/p>
&lt;p>一个 MapReduce 作业(job)通常会把输入的数据集切分为若干独立的数据块，由 map 任务(task)以完全并行的方式处理它们。框架会对 map 的输出先进行排序， 然后把结果输入给 reduce 任务。通常作业的输入和输出都会被存储在文件系统中。 整个框架负责任务的调度和监控，以及重新执行已经失败的任务。&lt;/p>
&lt;p>通常，MapReduce 框架和 HDFS 是运行在一相同的设备集群上的，也就是说，计算设备和存储设备通常在一起。这种配置允许框架在那些已经存好数据的设备上高效地调度任务，这可以使整个集群的网络带宽被非常高效地利用。&lt;/p>
&lt;p>MapReduce 框架由一个单独的 master JobTracker 和每个集群设备一个 slave TaskTracker 共同组成。master 负责调度构成一个作业的所有任务，这些任务分布在不同的 slave 上，master 监控它们的执行，重新执行已经失败的任务。而 slave 仅负责执行由 master 指派的任务。&lt;/p>
&lt;p>用户编写的 MapReduce 应用程序应该指明输入/输出的文件位置(路径)，并通过实现合适的接口或抽象类提供 map 和 reduce 函数。再加上其他作业的参数，就构成了作业配置(job configuration)。然后，job client 提交作业(jar 包/可执行程序等)和配置信息给 JobTracker，后者负责分发这些软件和配置信息给 slave、调度任务并监控它们的执行，同时提供状态和诊断信息给 job-client。&lt;/p>
&lt;p>简单来说，MapReduce 将计算过程分为两个阶段：Map 和 Reduce；&lt;/p>
&lt;ol>
&lt;li>Map 阶段并行处理数据；&lt;/li>
&lt;li>Reduce 阶段对 Map 结果进行汇总。&lt;/li>
&lt;/ol>
&lt;p>一个 Map 函数就是对一些独立元素组成的概念上的列表的每一个元素进行指定的操作。事实上，每个元素都是被独立操作的，而原始列表没有被更改，因为这里创建了一个新的列表来保存操作结果。这就是说，Map操作是可以高度并行的。而 Reduce 函数指的是对 Map 函数的结果（中间经过洗牌的过程，会把 map 的结果进行分组）分组后多个列表的元素进行适当的归并。&lt;/p>
&lt;hr>
&lt;h2 id="四yarn-基础概念">四、Yarn 基础概念&lt;/h2>
&lt;p>YARN(Yet Another Resource Negotiator)是 Hadoop 的设备资源管理器，它是一个通用资源管理系统，MapReduce 和其他上层应用提供统一的资源管理和调度，它为集群在利用率、资源统一管理和数据共享等方面提供了巨大的帮助。&lt;/p>
&lt;p>Yarn由ResourceManager、NodeManager、ApplicationMaster 和 Containe 四个概念构成。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>ResourceManager（RM）&lt;/strong>：整个集群资源（内存、CPU 等）的老大；&lt;/li>
&lt;li>&lt;strong>NodeManager（NM）&lt;/strong>：单个结点服务器资源老大；&lt;/li>
&lt;li>&lt;strong>ApplicationMaster（AM）&lt;/strong>：单个任务运行的老大；&lt;/li>
&lt;li>&lt;strong>Container&lt;/strong>：容器，相当于一台独立的服务器，里面封装了任务运行任务所需要的资源，如内存、CPU、磁盘、网络等。&lt;/li>
&lt;/ol>
&lt;p>了解了上面的大致概念之后，再细细分析一下：&lt;/p>
&lt;h3 id="41-resourcemanager">4.1 ResourceManager&lt;/h3>
&lt;p>ResourceManager 是一个全局的资源管理器，负责整个系统的资源管理和分配。它主要由两个组件构成：调度器(Scheduler)和应用程序管理器(Applications Manager)。&lt;/p>
&lt;p>调度器根据容量、队列等限制条件，将系统中的资源分配给各个正在运行的 MapReduce 程序。应用程序管理器负责管理整个系统中所有 MapReduce程序，包括提交、与调度器协商资源以启动 ApplicationMaster、监控 ApplicationMaster 运行状态并在失败时重新启动它等。&lt;/p>
&lt;h3 id="42-nodemanager">4.2 NodeManager&lt;/h3>
&lt;p>NodeManager 是每个设备上的资源和任务管理器，一方面，它会定时地向 ResourceManager 汇报本设备上的资源使用情况和各个Container 的运行状态；另一方面，它接收并处理来自ApplicationMaster 的 Container 启动/停止等各种请求。&lt;/p>
&lt;h3 id="43-applicationmaster">4.3 ApplicationMaster&lt;/h3>
&lt;p>用户提交的每个 MapReduce 程序均包含一个 ApplicationMaster，主要功能包括：与 ResourceManager 调度器协商以获取资源(用 Container 表示)；将得到的任务进一步分配给内部的任务(资源的二次分配)；与 NodeManager 通信以启动/停止任务；监控所有任务运行状态，并在任务运行失败时重新为任务申请资源以重启任务。&lt;/p>
&lt;h3 id="44-container">4.4 Container&lt;/h3>
&lt;p>Container 是 YARN 中的资源抽象，它封装了某个设备上的多维度资源，如内存、CPU、磁盘、网络等，当 AM 向 RM 申请资源时，RM 为AM 返回的资源便是用 Container 表示。&lt;/p>
&lt;hr>
&lt;h2 id="五结束语">五、结束语&lt;/h2>
&lt;p>本文走马观花的介绍了 Hadoop 相关内容。主要目的是给大家一个对大数据的分布式解决方案的感官印象，为后面的大数据相关文章提供一个基础的理解。&lt;/p>
&lt;p>最后要强调的是，思考大数据方向的问题是一定要记住分布式的概念，因为你的数据并不在一个设备中甚至不再一个集群中，而且计算也是分布的。所以在设计大数据应用程序时，要花时间思考程序和算法在单机应用和分布式应用所产生的不同(e.g. 加权平均值)。&lt;/p></content></entry><entry><title type="text">Maven 配置问题汇总</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/datastructes/question001/"/><id>https://quakewang.github.io/tech/datastructes/question001/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2021-07-10T00:00:00+00:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="maven-常见问题处理方法">Maven 常见问题处理方法&lt;/h1>
&lt;h3 id="一npm-install-node-sass---unsafe-perm-failed">一、'npm install node-sass --unsafe-perm' failed&lt;/h3>
&lt;p>&lt;strong>报错信息：&lt;/strong>&lt;/p>……</summary><content type="html">&lt;h1 id="maven-常见问题处理方法">Maven 常见问题处理方法&lt;/h1>
&lt;h3 id="一npm-install-node-sass---unsafe-perm-failed">一、'npm install node-sass --unsafe-perm' failed&lt;/h3>
&lt;p>&lt;strong>报错信息：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>INFO&lt;span class="o">]&lt;/span> BUILD FAILURE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>INFO&lt;span class="o">]&lt;/span> ------------------------------------------------------------------------
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>INFO&lt;span class="o">]&lt;/span> Total time: 02:01 min
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>INFO&lt;span class="o">]&lt;/span> Finished at: 2021-07-10
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>INFO&lt;span class="o">]&lt;/span> ------------------------------------------------------------------------
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>ERROR&lt;span class="o">]&lt;/span> Failed to execute goal com.github.eirslett:frontend-maven-plugin:1.6:npm &lt;span class="o">(&lt;/span>npm install node-sass --unsafe-perm&lt;span class="o">)&lt;/span> on project dolphinscheduler-ui: Failed to run
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">task: &lt;span class="s1">&amp;#39;npm install node-sass --unsafe-perm&amp;#39;&lt;/span> failed. java.io.IOException: Cannot run program &lt;span class="s2">&amp;#34;dolphinscheduler-dev\dolphinscheduler-ui\node\node.exe&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">(&lt;/span>in directory &lt;span class="s2">&amp;#34;dolphinscheduler-dev\dolphinscheduler-ui&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>: CreateProcess &lt;span class="nv">error&lt;/span>&lt;span class="o">=&lt;/span>193, %1 不是有效的 Win32 应用程序。 -&amp;gt; &lt;span class="o">[&lt;/span>Help 1&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>ERROR&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>ERROR&lt;span class="o">]&lt;/span> To see the full stack trace of the errors, re-run Maven with the -e switch.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>ERROR&lt;span class="o">]&lt;/span> Re-run Maven using the -X switch to &lt;span class="nb">enable&lt;/span> full debug logging.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>ERROR&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>ERROR&lt;span class="o">]&lt;/span> For more information about the errors and possible solutions, please &lt;span class="nb">read&lt;/span> the following articles:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>ERROR&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>Help 1&lt;span class="o">]&lt;/span> http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>ERROR&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>ERROR&lt;span class="o">]&lt;/span> After correcting the problems, you can resume the build with the &lt;span class="nb">command&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>ERROR&lt;span class="o">]&lt;/span> mvn &amp;lt;args&amp;gt; -rf :dolphinscheduler-ui
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>错误分析：&lt;/strong>&lt;/p>
&lt;p>当执行 &lt;code>mvn -U install package -Prelease -Dmaven.test.skip=true&lt;/code> 的时候，由于前端 Module 的 pom.xml 对于 NodeJs 没有配置对应的镜像，并且有某堵墙的存在，懂的都懂不多说哈，从而导致无法能够成功的拉去对应的资源，因此需要在对应的 pom.xml 文件中添加相关配置即可。&lt;/p>
&lt;p>&lt;strong>解决方法：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-xml" data-lang="xml">&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;execution&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;id&amp;gt;&lt;/span>install node and npm&lt;span class="nt">&amp;lt;/id&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;goals&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;goal&amp;gt;&lt;/span>install-node-and-npm&lt;span class="nt">&amp;lt;/goal&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;/goals&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;configuration&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;nodeVersion&amp;gt;&lt;/span>${node.version}&lt;span class="nt">&amp;lt;/nodeVersion&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;npmVersion&amp;gt;&lt;/span>${npm.version}&lt;span class="nt">&amp;lt;/npmVersion&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;nodeDownloadRoot&amp;gt;&lt;/span>https://npm.taobao.org/mirrors/node/&lt;span class="nt">&amp;lt;/nodeDownloadRoot&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;npmDownloadRoot&amp;gt;&lt;/span>https://registry.npm.taobao.org/npm/-/&lt;span class="nt">&amp;lt;/npmDownloadRoot&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;/configuration&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;/execution&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;em>注：&lt;/em> 其中 &lt;strong>nodeDownloadRoot&lt;/strong> 和 &lt;strong>npmDownloadRoot&lt;/strong> 为添加的淘宝镜像，如果添加该配置还无法解决问题，可以尝试把 node 和 npm 的 version 置换成本机所安装的版本即可。&lt;/p>
&lt;hr>
&lt;h3 id="二could-not-transfer-artifact-orgspringframeworkbootspring-boot-starter-parentpom2118release-fromto-central">二、Could not transfer artifact org.springframework.boot:spring-boot-starter-parent:pom:2.1.18.RELEASE from/to central&lt;/h3>
&lt;p>&lt;strong>报错信息：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">Caused by: org.apache.maven.project.ProjectBuildingException: Some problems were encountered &lt;span class="k">while&lt;/span> processing the POMs:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>ERROR&lt;span class="o">]&lt;/span> Non-resolvable import POM: Could not transfer artifact org.springframework.boot:spring-boot-starter-parent:pom:2.1.18.RELEASE from/to central &lt;span class="o">(&lt;/span>http://repo.maven.apache.org/maven2&lt;span class="o">)&lt;/span>: Failed to transfer http://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-parent/2.1.18.RELEASE/spring-boot-starter-parent-2.1.18.RELEASE.pom. Error code 501, HTTPS Required @ org.apache.dolphinscheduler:dolphinscheduler:1.3.6-SNAPSHOT, D:&lt;span class="se">\i&lt;/span>deaProjects&lt;span class="se">\d&lt;/span>olphinscheduler-dev&lt;span class="se">\p&lt;/span>om.xml, line 165, column &lt;span class="m">25&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at org.apache.maven.project.DefaultProjectBuilder.build&lt;span class="o">(&lt;/span>DefaultProjectBuilder.java:176&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at org.apache.maven.project.DefaultProjectBuilder.build&lt;span class="o">(&lt;/span>DefaultProjectBuilder.java:102&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at io.airlift.resolver.ArtifactResolver.getMavenProject&lt;span class="o">(&lt;/span>ArtifactResolver.java:177&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ... &lt;span class="m">44&lt;/span> more
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Caused by: org.apache.maven.model.building.ModelBuildingException: &lt;span class="m">1&lt;/span> problem was encountered &lt;span class="k">while&lt;/span> building the effective model &lt;span class="k">for&lt;/span> org.apache.dolphinscheduler:dolphinscheduler-registry-zookeeper:1.3.6-SNAPSHOT
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>ERROR&lt;span class="o">]&lt;/span> Non-resolvable import POM: Could not transfer artifact org.springframework.boot:spring-boot-starter-parent:pom:2.1.18.RELEASE from/to central &lt;span class="o">(&lt;/span>http://repo.maven.apache.org/maven2&lt;span class="o">)&lt;/span>: Failed to transfer http://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-parent/2.1.18.RELEASE/spring-boot-starter-parent-2.1.18.RELEASE.pom. Error code 501, HTTPS Required @ org.apache.dolphinscheduler:dolphinscheduler:1.3.6-SNAPSHOT, D:&lt;span class="se">\i&lt;/span>deaProjects&lt;span class="se">\d&lt;/span>olphinscheduler-dev&lt;span class="se">\p&lt;/span>om.xml, line 165, column &lt;span class="m">25&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at org.apache.maven.model.building.DefaultModelProblemCollector.newModelBuildingException&lt;span class="o">(&lt;/span>DefaultModelProblemCollector.java:195&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at org.apache.maven.model.building.DefaultModelBuilder.build&lt;span class="o">(&lt;/span>DefaultModelBuilder.java:419&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at org.apache.maven.model.building.DefaultModelBuilder.build&lt;span class="o">(&lt;/span>DefaultModelBuilder.java:371&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at org.apache.maven.model.building.DefaultModelBuilder.build&lt;span class="o">(&lt;/span>DefaultModelBuilder.java:362&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at org.apache.maven.model.building.DefaultModelBuilder.build&lt;span class="o">(&lt;/span>DefaultModelBuilder.java:232&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> at org.apache.maven.project.DefaultProjectBuilder.build&lt;span class="o">(&lt;/span>DefaultProjectBuilder.java:142&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ... &lt;span class="m">46&lt;/span> more
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Process finished with &lt;span class="nb">exit&lt;/span> code &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>错误分析：&lt;/strong>&lt;/p>
&lt;p>在网上拷贝的所有阿里云镜像比如：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-xml" data-lang="xml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;mirror&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;id&amp;gt;&lt;/span>nexus-aliyun&lt;span class="nt">&amp;lt;/id&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;mirrorOf&amp;gt;&lt;/span>central&lt;span class="nt">&amp;lt;/mirrorOf&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;name&amp;gt;&lt;/span>Nexus aliyun&lt;span class="nt">&amp;lt;/name&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;url&amp;gt;&lt;/span>http://maven.aliyun.com/nexus/content/groups/public&lt;span class="nt">&amp;lt;/url&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;/mirror&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>查看官网之后发现：阿里不再支持http下载，只支持https。&lt;/p>
&lt;p>因此，先将maven镜像配置如下：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-xml" data-lang="xml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;mirror&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;id&amp;gt;&lt;/span>aliyunmaven&lt;span class="nt">&amp;lt;/id&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;mirrorOf&amp;gt;&lt;/span>*&lt;span class="nt">&amp;lt;/mirrorOf&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;name&amp;gt;&lt;/span>阿里云公共仓库&lt;span class="nt">&amp;lt;/name&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;url&amp;gt;&lt;/span>https://maven.aliyun.com/repository/public&lt;span class="nt">&amp;lt;/url&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;/mirror&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>然后还出现了一个问题，由于使用了HTTPS，存在着 SSL 证书验证的问题，因此需要在 IDEA 中添加了一行配置 Maven —&amp;gt; Importing —&amp;gt; VM options for importer:&lt;/p>
&lt;p>&lt;code>-Dmaven.wagon.http.ssl.allowall=true&lt;/code>&lt;/p>
&lt;p>一般到这里问题理论上是可以正常解决了，但是由于 Windows 的环境会出现许多神奇的问题，如果项目还依然报错，可以尝试删除本地包，重新构建。&lt;/p></content></entry><entry><title type="text">TypeScript学习笔记02</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/typescript02/"/><id>https://quakewang.github.io/tech/typescript02/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2020-12-22T00:00:00+00:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="第二章面向对象">第二章：面向对象&lt;/h1>
&lt;p>面向对象是学习编程过程中一个非常重要的思想，但是它却被很多人理解成了一个比较难，比较深奥的问题，其实不然。其实面向对象在理解之后还是很简单的，简而言之就是程序之中所有的操作都需要通过对象来完成。&lt;/p>……</summary><content type="html">&lt;h1 id="第二章面向对象">第二章：面向对象&lt;/h1>
&lt;p>面向对象是学习编程过程中一个非常重要的思想，但是它却被很多人理解成了一个比较难，比较深奥的问题，其实不然。其实面向对象在理解之后还是很简单的，简而言之就是程序之中所有的操作都需要通过对象来完成。&lt;/p>
&lt;ul>
&lt;li>举例来说：
&lt;ul>
&lt;li>操作浏览器要使用window对象&lt;/li>
&lt;li>操作网页要使用document对象&lt;/li>
&lt;li>操作控制台要使用console对象&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>一切操作都要通过对象，也就是所谓的面向对象，那么对象到底是什么呢？这就要先说到程序是什么，计算机程序的本质就是对现实事物的抽象，抽象的反义词是具体，比如：照片是对一个具体的人的抽象，汽车模型是对具体汽车的抽象等等。程序也是对事物的抽象，在程序中我们可以表示一个人、一条狗、一把枪、一颗子弹等等所有的事物。一个事物到了程序中就变成了一个对象。&lt;/p>
&lt;p>在程序中所有的对象都被分成了两个部分数据和功能，以人为例，人的姓名、性别、年龄、身高、体重等属于数据，人可以说话、走路、吃饭、睡觉这些属于人的功能。数据在对象中被成为属性，而功能就被称为方法。所以简而言之，在程序中一切皆是对象。&lt;/p>
&lt;h2 id="1类class">1、类（class）&lt;/h2>
&lt;p>要想面向对象，操作对象，首先便要拥有对象，那么下一个问题就是如何创建对象。要创建对象，必须要先定义类，所谓的类可以理解为对象的模型，程序中可以根据类创建指定类型的对象，举例来说：可以通过Person类来创建人的对象，通过Dog类创建狗的对象，通过Car类来创建汽车的对象，不同的类可以用来创建不同的对象。&lt;/p>
&lt;ul>
&lt;li>
&lt;p>定义类：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="err">类名&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">属性名&lt;/span>: &lt;span class="kt">类型&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">参数&lt;/span>: &lt;span class="kt">类型&lt;/span>&lt;span class="p">){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">属性名&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="err">参数&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">方法名&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">....&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">age&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">age&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">sayHello&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`大家好，我是&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>使用类：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">p&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;孙悟空&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">sayHello&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="2面向对象的特点">2、面向对象的特点&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>封装&lt;/p>
&lt;ul>
&lt;li>
&lt;p>对象实质上就是属性和方法的容器，它的主要作用就是存储属性和方法，这就是所谓的封装&lt;/p>
&lt;/li>
&lt;li>
&lt;p>默认情况下，对象的属性是可以任意的修改的，为了确保数据的安全性，在TS中可以对属性的权限进行设置&lt;/p>
&lt;/li>
&lt;li>
&lt;p>只读属性（readonly）：&lt;/p>
&lt;ul>
&lt;li>如果在声明属性时添加一个readonly，则属性便成了只读属性无法修改&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>TS中属性具有三种修饰符：&lt;/p>
&lt;ul>
&lt;li>public（默认值），可以在类、子类和对象中修改&lt;/li>
&lt;li>protected ，可以在类、子类中修改&lt;/li>
&lt;li>private ，可以在类中修改&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>public&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">public&lt;/span> &lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 写或什么都不写都是public
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">public&lt;/span> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 可以在类中修改
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">age&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">age&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">sayHello&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`大家好，我是&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Employee&lt;/span> &lt;span class="kr">extends&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">super&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">age&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">//子类中可以修改
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">p&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;孙悟空&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;猪八戒&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="c1">// 可以通过对象修改
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>protected&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">protected&lt;/span> &lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">protected&lt;/span> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 可以修改
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">age&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">age&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">sayHello&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`大家好，我是&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Employee&lt;/span> &lt;span class="kr">extends&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">super&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">age&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">//子类中可以修改
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">p&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;孙悟空&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;猪八戒&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="c1">// 不能修改
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>private&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">private&lt;/span> &lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">private&lt;/span> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 可以修改
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">age&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">age&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">sayHello&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`大家好，我是&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Employee&lt;/span> &lt;span class="kr">extends&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">super&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">age&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">//子类中不能修改
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">p&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;孙悟空&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;猪八戒&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="c1">// 不能修改
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>属性存取器&lt;/p>
&lt;ul>
&lt;li>
&lt;p>对于一些不希望被任意修改的属性，可以将其设置为private&lt;/p>
&lt;/li>
&lt;li>
&lt;p>直接将其设置为private将导致无法再通过对象修改其中的属性&lt;/p>
&lt;/li>
&lt;li>
&lt;p>我们可以在类中定义一组读取、设置属性的方法，这种对属性读取或设置的属性被称为属性的存取器&lt;/p>
&lt;/li>
&lt;li>
&lt;p>读取属性的方法叫做setter方法，设置属性的方法叫做getter方法&lt;/p>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">private&lt;/span> &lt;span class="nx">_name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">get&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">_name&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">set&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">p1&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;孙悟空&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">p1&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 通过getter读取name属性
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">p1&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;猪八戒&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 通过setter修改name属性
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>静态属性&lt;/p>
&lt;ul>
&lt;li>
&lt;p>静态属性（方法），也称为类属性。使用静态属性无需创建实例，通过类即可直接使用&lt;/p>
&lt;/li>
&lt;li>
&lt;p>静态属性（方法）使用static开头&lt;/p>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Tools&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">static&lt;/span> &lt;span class="nx">PI&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">3.1415926&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">static&lt;/span> &lt;span class="nx">sum&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">num1&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">num2&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">num1&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">num2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">Tools&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">PI&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">Tools&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">sum&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">123&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">456&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>this&lt;/p>
&lt;ul>
&lt;li>在类中，使用this表示当前对象&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>继承&lt;/p>
&lt;ul>
&lt;li>
&lt;p>继承时面向对象中的又一个特性&lt;/p>
&lt;/li>
&lt;li>
&lt;p>通过继承可以将其他类中的属性和方法引入到当前类中&lt;/p>
&lt;ul>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Animal&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">age&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">age&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Dog&lt;/span> &lt;span class="kr">extends&lt;/span> &lt;span class="nx">Animal&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">bark&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">在汪汪叫！`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">dog&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Dog&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;旺财&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">dog&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bark&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>通过继承可以在不修改类的情况下完成对类的扩展&lt;/p>
&lt;/li>
&lt;li>
&lt;p>重写&lt;/p>
&lt;ul>
&lt;li>
&lt;p>发生继承时，如果子类中的方法会替换掉父类中的同名方法，这就称为方法的重写&lt;/p>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Animal&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">age&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">age&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">run&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`父类中的run方法！`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Dog&lt;/span> &lt;span class="kr">extends&lt;/span> &lt;span class="nx">Animal&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">bark&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">在汪汪叫！`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">run&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`子类中的run方法，会重写父类中的run方法！`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">dog&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Dog&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;旺财&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">dog&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bark&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>在子类中可以使用super来完成对父类的引用&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>抽象类（abstract class）&lt;/p>
&lt;ul>
&lt;li>
&lt;p>抽象类是专门用来被其他类所继承的类，它只能被其他类所继承不能用来创建实例&lt;/p>
&lt;/li>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">abstract&lt;/span> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Animal&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">abstract&lt;/span> &lt;span class="nx">run&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">void&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">bark&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;动物在叫~&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Dog&lt;/span> &lt;span class="kr">extends&lt;/span> &lt;span class="nx">Animals&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">run&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;狗在跑~&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>使用abstract开头的方法叫做抽象方法，抽象方法没有方法体只能定义在抽象类中，继承抽象类时抽象方法必须要实现&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="3接口interface">3、接口（Interface）&lt;/h2>
&lt;p>接口的作用类似于抽象类，不同点在于接口中的所有方法和属性都是没有实值的，换句话说接口中的所有方法都是抽象方法。接口主要负责定义一个类的结构，接口可以去限制一个对象的接口，对象只有包含接口中定义的所有属性和方法时才能匹配接口。同时，可以让一个类去实现接口，实现接口时类中要保护接口中的所有属性。&lt;/p>
&lt;ul>
&lt;li>
&lt;p>示例（检查对象类型）：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">interface&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">sayHello&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="k">void&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">fn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">per&lt;/span>: &lt;span class="kt">Person&lt;/span>&lt;span class="p">){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">per&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">sayHello&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fn&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="s1">&amp;#39;孙悟空&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">sayHello() {console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`Hello, 我是 &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">)}});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>示例（实现）&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">interface&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">sayHello&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="k">void&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Student&lt;/span> &lt;span class="kr">implements&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kr">public&lt;/span> &lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">sayHello() {&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;大家好，我是&amp;#39;&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="4泛型generic">4、泛型（Generic）&lt;/h2>
&lt;p>定义一个函数或类时，有些情况下无法确定其中要使用的具体类型（返回值、参数、属性的类型不能确定），此时泛型便能够发挥作用。&lt;/p>
&lt;ul>
&lt;li>
&lt;p>举个例子：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">arg&lt;/span>: &lt;span class="kt">any&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kt">any&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">arg&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>上例中，test函数有一个参数类型不确定，但是能确定的时其返回值的类型和参数的类型是相同的，由于类型不确定所以参数和返回值均使用了any，但是很明显这样做是不合适的，首先使用any会关闭TS的类型检查，其次这样设置也不能体现出参数和返回值是相同的类型&lt;/p>
&lt;/li>
&lt;li>
&lt;p>使用泛型：&lt;/p>
&lt;/li>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">test&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">T&lt;/span>&lt;span class="p">&amp;gt;(&lt;/span>&lt;span class="nx">arg&lt;/span>: &lt;span class="kt">T&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">T&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">arg&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>这里的&lt;code>&amp;lt;T&amp;gt;&lt;/code>就是泛型，T是我们给这个类型起的名字（不一定非叫T），设置泛型后即可在函数中使用T来表示该类型。所以泛型其实很好理解，就表示某个类型。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>那么如何使用上边的函数呢？&lt;/p>
&lt;ul>
&lt;li>
&lt;p>方式一（直接使用）：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>使用时可以直接传递参数使用，类型会由TS自动推断出来，但有时编译器无法自动推断时还需要使用下面的方式&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>方式二（指定类型）：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">test&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">number&lt;/span>&lt;span class="p">&amp;gt;(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>也可以在函数后手动指定泛型&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>可以同时指定多个泛型，泛型间使用逗号隔开：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">test&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">T&lt;/span>&lt;span class="err">,&lt;/span> &lt;span class="na">K&lt;/span>&lt;span class="p">&amp;gt;(&lt;/span>&lt;span class="nx">a&lt;/span>: &lt;span class="kt">T&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">b&lt;/span>: &lt;span class="kt">K&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">K&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">b&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">test&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">number&lt;/span>&lt;span class="err">,&lt;/span> &lt;span class="na">string&lt;/span>&lt;span class="p">&amp;gt;(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;hello&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>使用泛型时，完全可以将泛型当成是一个普通的类去使用&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>类中同样可以使用泛型：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">class&lt;/span> &lt;span class="nx">MyClass&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">T&lt;/span>&lt;span class="p">&amp;gt;{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">prop&lt;/span>: &lt;span class="kt">T&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">prop&lt;/span>: &lt;span class="kt">T&lt;/span>&lt;span class="p">){&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">prop&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">prop&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>除此之外，也可以对泛型的范围进行约束&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">interface&lt;/span> &lt;span class="nx">MyInter&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">length&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">test&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">T&lt;/span> &lt;span class="na">extends&lt;/span> &lt;span class="na">MyInter&lt;/span>&lt;span class="p">&amp;gt;(&lt;/span>&lt;span class="nx">arg&lt;/span>: &lt;span class="kt">T&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kt">number&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">arg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>使用T extends MyInter表示泛型T必须是MyInter的子类，不一定非要使用接口类和抽象类同样适用。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul></content></entry><entry><title type="text">Typescript 学习笔记01</title><link rel="alternate" type="text/html" href="https://quakewang.github.io/tech/typescript01/"/><id>https://quakewang.github.io/tech/typescript01/</id><updated>2026-07-03T16:08:01+00:00</updated><published>2020-12-17T19:18:52+08:00</published><author><name>二夕</name><uri>https://quakewang.github.io/</uri><email>wangfuzheng0814@foxmail.com</email></author><rights>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</rights><summary type="html">&lt;h1 id="第一章-快速入门">第一章 快速入门&lt;/h1>
&lt;h2 id="0typescript简介">0、TypeScript简介&lt;/h2>
&lt;ol>
&lt;li>TypeScript是JavaScript的超集。&lt;/li>
&lt;li>它对JS进行了扩展，向JS中引入了类型的概念，并添加了许多新的特性。&lt;/li>
&lt;li>TS代码需要通过编译器编译为JS，然后再交由JS解析器执行。&lt;/li>
&lt;li>TS完全兼容JS，换言之，任何的TS代码都可以直接当成JS使用。&lt;/li>
&lt;li>相较于JS而言，TS拥有了静态类型，更加严格的语法，更强大的功能；TS可以在代码执行前就完成代码的检查，减小了运行时异常的出现的几率；TS代码可以编译为任意版本的JS代码，可有效解决不同JS运行环境的兼容问题；同样的功能，TS的代码量要大于JS，但由于TS的代码结构更加清晰，变量类型更加明确，在后期代码的维护中TS却远远胜于JS。&lt;/li>
&lt;/ol>
&lt;h2 id="1typescript-开发环境搭建">1、TypeScript 开发环境搭建&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>下载Node.js&lt;/p>……</summary><content type="html">&lt;h1 id="第一章-快速入门">第一章 快速入门&lt;/h1>
&lt;h2 id="0typescript简介">0、TypeScript简介&lt;/h2>
&lt;ol>
&lt;li>TypeScript是JavaScript的超集。&lt;/li>
&lt;li>它对JS进行了扩展，向JS中引入了类型的概念，并添加了许多新的特性。&lt;/li>
&lt;li>TS代码需要通过编译器编译为JS，然后再交由JS解析器执行。&lt;/li>
&lt;li>TS完全兼容JS，换言之，任何的TS代码都可以直接当成JS使用。&lt;/li>
&lt;li>相较于JS而言，TS拥有了静态类型，更加严格的语法，更强大的功能；TS可以在代码执行前就完成代码的检查，减小了运行时异常的出现的几率；TS代码可以编译为任意版本的JS代码，可有效解决不同JS运行环境的兼容问题；同样的功能，TS的代码量要大于JS，但由于TS的代码结构更加清晰，变量类型更加明确，在后期代码的维护中TS却远远胜于JS。&lt;/li>
&lt;/ol>
&lt;h2 id="1typescript-开发环境搭建">1、TypeScript 开发环境搭建&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>下载Node.js&lt;/p>
&lt;ul>
&lt;li>64位：https://nodejs.org/dist/v14.15.1/node-v14.15.1-x64.msi&lt;/li>
&lt;li>32位：https://nodejs.org/dist/v14.15.1/node-v14.15.1-x86.msi&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>安装Node.js&lt;/p>
&lt;/li>
&lt;li>
&lt;p>使用npm全局安装typescript&lt;/p>
&lt;ul>
&lt;li>进入命令行&lt;/li>
&lt;li>输入：npm i -g typescript&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>创建一个ts文件&lt;/p>
&lt;/li>
&lt;li>
&lt;p>使用tsc对ts文件进行编译&lt;/p>
&lt;ul>
&lt;li>
&lt;p>进入命令行&lt;/p>
&lt;/li>
&lt;li>
&lt;p>进入ts文件所在目录&lt;/p>
&lt;/li>
&lt;li>
&lt;p>执行命令：tsc xxx.ts&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h2 id="2基本类型">2、基本类型&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>类型声明&lt;/p>
&lt;ul>
&lt;li>
&lt;p>类型声明是TS非常重要的一个特点&lt;/p>
&lt;/li>
&lt;li>
&lt;p>通过类型声明可以指定TS中变量（参数、形参）的类型&lt;/p>
&lt;/li>
&lt;li>
&lt;p>指定类型后，当为变量赋值时，TS编译器会自动检查值是否符合类型声明，符合则赋值，否则报错&lt;/p>
&lt;/li>
&lt;li>
&lt;p>简而言之，类型声明给变量设置了类型，使得变量只能存储某种类型的值&lt;/p>
&lt;/li>
&lt;li>
&lt;p>语法：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">变量&lt;/span>: &lt;span class="kt">类型&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">变量&lt;/span>: &lt;span class="kt">类型&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="err">值&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">fn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">参数&lt;/span>: &lt;span class="kt">类型&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">参数&lt;/span>: &lt;span class="kt">类型&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="err">类型&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>自动类型判断&lt;/p>
&lt;ul>
&lt;li>TS拥有自动的类型判断机制&lt;/li>
&lt;li>当对变量的声明和赋值是同时进行的，TS编译器会自动判断变量的类型&lt;/li>
&lt;li>所以如果你的变量的声明和赋值时同时进行的，可以省略掉类型声明&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>类型：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: center">类型&lt;/th>
&lt;th style="text-align: center">例子&lt;/th>
&lt;th style="text-align: center">描述&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: center">number&lt;/td>
&lt;td style="text-align: center">1, -33, 2.5&lt;/td>
&lt;td style="text-align: center">任意数字&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">string&lt;/td>
&lt;td style="text-align: center">'hi', &amp;quot;hi&amp;quot;, &lt;code>hi&lt;/code>&lt;/td>
&lt;td style="text-align: center">任意字符串&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">boolean&lt;/td>
&lt;td style="text-align: center">true、false&lt;/td>
&lt;td style="text-align: center">布尔值true或false&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">字面量&lt;/td>
&lt;td style="text-align: center">其本身&lt;/td>
&lt;td style="text-align: center">限制变量的值就是该字面量的值&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">any&lt;/td>
&lt;td style="text-align: center">*&lt;/td>
&lt;td style="text-align: center">任意类型&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">unknown&lt;/td>
&lt;td style="text-align: center">*&lt;/td>
&lt;td style="text-align: center">类型安全的any&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">void&lt;/td>
&lt;td style="text-align: center">空值（undefined）&lt;/td>
&lt;td style="text-align: center">没有值（或undefined）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">never&lt;/td>
&lt;td style="text-align: center">没有值&lt;/td>
&lt;td style="text-align: center">不能是任何值&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">object&lt;/td>
&lt;td style="text-align: center">{name:'孙悟空'}&lt;/td>
&lt;td style="text-align: center">任意的JS对象&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">array&lt;/td>
&lt;td style="text-align: center">[1,2,3]&lt;/td>
&lt;td style="text-align: center">任意JS数组&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">tuple&lt;/td>
&lt;td style="text-align: center">[4,5]&lt;/td>
&lt;td style="text-align: center">元素，TS新增类型，固定长度数组&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: center">enum&lt;/td>
&lt;td style="text-align: center">enum{A, B}&lt;/td>
&lt;td style="text-align: center">枚举，TS中新增类型&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/li>
&lt;li>
&lt;p>number&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">decimal&lt;/span>: &lt;span class="kt">number&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">hex&lt;/span>: &lt;span class="kt">number&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mh">0xf00d&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">binary&lt;/span>: &lt;span class="kt">number&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="nx">b1010&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">octal&lt;/span>: &lt;span class="kt">number&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="nx">o744&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">big&lt;/span>: &lt;span class="kt">bigint&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">100&lt;/span>&lt;span class="nx">n&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>boolean&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">isDone&lt;/span>: &lt;span class="kt">boolean&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>string&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">color&lt;/span>: &lt;span class="kt">string&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;blue&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">color&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;red&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">fullName&lt;/span>: &lt;span class="kt">string&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sb">`Bob Bobbington`&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">age&lt;/span>: &lt;span class="kt">number&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">37&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">sentence&lt;/span>: &lt;span class="kt">string&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sb">`Hello, my name is &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">fullName&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sb">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sb"> I&amp;#39;ll be &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">age&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> years old next month.`&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>字面量&lt;/p>
&lt;ul>
&lt;li>
&lt;p>也可以使用字面量去指定变量的类型，通过字面量可以确定变量的取值范围&lt;/p>
&lt;/li>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">color&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;red&amp;#39;&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="s1">&amp;#39;blue&amp;#39;&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="s1">&amp;#39;black&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">num&lt;/span>: &lt;span class="kt">1&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="mi">3&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="mi">4&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>any&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">d&lt;/span>: &lt;span class="kt">any&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">d&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;hello&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">d&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>unknown&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">notSure&lt;/span>: &lt;span class="kt">unknown&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">notSure&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;hello&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>void&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">unusable&lt;/span>: &lt;span class="kt">void&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">undefined&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>never&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">message&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kt">never&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">throw&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nb">Error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">message&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>object（没啥用）&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">obj&lt;/span>: &lt;span class="kt">object&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{};&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>array&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">list&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">[]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">list&lt;/span>: &lt;span class="kt">Array&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">number&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>tuple&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">x&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">number&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">x&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;hello&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>enum&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">enum&lt;/span> &lt;span class="nx">Color&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Red&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Green&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Blue&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">c&lt;/span>: &lt;span class="kt">Color&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">Color&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Green&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">enum&lt;/span> &lt;span class="nx">Color&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Red&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Green&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Blue&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">c&lt;/span>: &lt;span class="kt">Color&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">Color&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Green&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">enum&lt;/span> &lt;span class="nx">Color&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Red&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Green&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Blue&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">c&lt;/span>: &lt;span class="kt">Color&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">Color&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Green&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>类型断言&lt;/p>
&lt;ul>
&lt;li>
&lt;p>有些情况下，变量的类型对于我们来说是很明确，但是TS编译器却并不清楚，此时，可以通过类型断言来告诉编译器变量的类型，断言有两种形式：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>第一种&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">someValue&lt;/span>: &lt;span class="kt">unknown&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;this is a string&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">strLength&lt;/span>: &lt;span class="kt">number&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">someValue&lt;/span> &lt;span class="kr">as&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>第二种&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">someValue&lt;/span>: &lt;span class="kt">unknown&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;this is a string&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">strLength&lt;/span>: &lt;span class="kt">number&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&amp;lt;&lt;/span>&lt;span class="nt">string&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="nx">someValue&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="3编译选项">3、编译选项&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>自动编译文件&lt;/p>
&lt;ul>
&lt;li>
&lt;p>编译文件时，使用 -w 指令后，TS编译器会自动监视文件的变化，并在文件发生变化时对文件进行重新编译。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-powershell" data-lang="powershell">&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tsc&lt;/span> &lt;span class="n">xxx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">ts&lt;/span> &lt;span class="n">-w&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>自动编译整个项目&lt;/p>
&lt;ul>
&lt;li>
&lt;p>如果直接使用tsc指令，则可以自动将当前项目下的所有ts文件编译为js文件。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>但是能直接使用tsc命令的前提时，要先在项目根目录下创建一个ts的配置文件 tsconfig.json&lt;/p>
&lt;/li>
&lt;li>
&lt;p>tsconfig.json是一个JSON文件，添加配置文件后，只需只需 tsc 命令即可完成对整个项目的编译&lt;/p>
&lt;/li>
&lt;li>
&lt;p>配置选项：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>include&lt;/p>
&lt;ul>
&lt;li>
&lt;p>定义希望被编译文件所在的目录&lt;/p>
&lt;/li>
&lt;li>
&lt;p>默认值：[&amp;quot;**/*&amp;quot;]&lt;/p>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;include&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;src/**/*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tests/**/*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>上述示例中，所有src目录和tests目录下的文件都会被编译&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>exclude&lt;/p>
&lt;ul>
&lt;li>
&lt;p>定义需要排除在外的目录&lt;/p>
&lt;/li>
&lt;li>
&lt;p>默认值：[&amp;quot;node_modules&amp;quot;, &amp;quot;bower_components&amp;quot;, &amp;quot;jspm_packages&amp;quot;]&lt;/p>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;exclude&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;./src/hello/**/*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>上述示例中，src下hello目录下的文件都不会被编译&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>extends&lt;/p>
&lt;ul>
&lt;li>
&lt;p>定义被继承的配置文件&lt;/p>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;extends&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="s2">&amp;#34;./configs/base&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>上述示例中，当前配置文件中会自动包含config目录下base.json中的所有配置信息&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>files&lt;/p>
&lt;ul>
&lt;li>
&lt;p>指定被编译文件的列表，只有需要编译的文件少时才会用到&lt;/p>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;files&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;core.ts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;sys.ts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;types.ts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;scanner.ts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;parser.ts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;utilities.ts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;binder.ts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;checker.ts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;tsc.ts&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>列表中的文件都会被TS编译器所编译&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>compilerOptions&lt;/p>
&lt;ul>
&lt;li>
&lt;p>编译选项是配置文件中非常重要也比较复杂的配置选项&lt;/p>
&lt;/li>
&lt;li>
&lt;p>在compilerOptions中包含多个子选项，用来完成对编译的配置&lt;/p>
&lt;ul>
&lt;li>
&lt;p>项目选项&lt;/p>
&lt;ul>
&lt;li>
&lt;p>target&lt;/p>
&lt;ul>
&lt;li>
&lt;p>设置ts代码编译的目标版本&lt;/p>
&lt;/li>
&lt;li>
&lt;p>可选值：&lt;/p>
&lt;ul>
&lt;li>ES3（默认）、ES5、ES6/ES2015、ES7/ES2016、ES2017、ES2018、ES2019、ES2020、ESNext&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;compilerOptions&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;target&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ES6&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>如上设置，我们所编写的ts代码将会被编译为ES6版本的js代码&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>lib&lt;/p>
&lt;ul>
&lt;li>
&lt;p>指定代码运行时所包含的库（宿主环境）&lt;/p>
&lt;/li>
&lt;li>
&lt;p>可选值：&lt;/p>
&lt;ul>
&lt;li>ES5、ES6/ES2015、ES7/ES2016、ES2017、ES2018、ES2019、ES2020、ESNext、DOM、WebWorker、ScriptHost ......&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;compilerOptions&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;target&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ES6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;lib&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;ES6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;DOM&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;outDir&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;dist&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;outFile&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;dist/aa.js&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>module&lt;/p>
&lt;ul>
&lt;li>
&lt;p>设置编译后代码使用的模块化系统&lt;/p>
&lt;/li>
&lt;li>
&lt;p>可选值：&lt;/p>
&lt;ul>
&lt;li>CommonJS、UMD、AMD、System、ES2020、ESNext、None&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;compilerOptions&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;module&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;CommonJS&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>outDir&lt;/p>
&lt;ul>
&lt;li>
&lt;p>编译后文件的所在目录&lt;/p>
&lt;/li>
&lt;li>
&lt;p>默认情况下，编译后的js文件会和ts文件位于相同的目录，设置outDir后可以改变编译后文件的位置&lt;/p>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;compilerOptions&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;outDir&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;dist&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>设置后编译后的js文件将会生成到dist目录&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>outFile&lt;/p>
&lt;ul>
&lt;li>
&lt;p>将所有的文件编译为一个js文件&lt;/p>
&lt;/li>
&lt;li>
&lt;p>默认会将所有的编写在全局作用域中的代码合并为一个js文件，如果module制定了None、System或AMD则会将模块一起合并到文件之中&lt;/p>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;compilerOptions&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;outFile&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;dist/app.js&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>rootDir&lt;/p>
&lt;ul>
&lt;li>
&lt;p>指定代码的根目录，默认情况下编译后文件的目录结构会以最长的公共目录为根目录，通过rootDir可以手动指定根目录&lt;/p>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;compilerOptions&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;rootDir&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;./src&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>allowJs&lt;/p>
&lt;ul>
&lt;li>是否对js文件编译&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>checkJs&lt;/p>
&lt;ul>
&lt;li>
&lt;p>是否对js文件进行检查&lt;/p>
&lt;/li>
&lt;li>
&lt;p>示例：&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;compilerOptions&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;allowJs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;checkJs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>removeComments&lt;/p>
&lt;ul>
&lt;li>是否删除注释&lt;/li>
&lt;li>默认值：false&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>noEmit&lt;/p>
&lt;ul>
&lt;li>不对代码进行编译&lt;/li>
&lt;li>默认值：false&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>sourceMap&lt;/p>
&lt;ul>
&lt;li>是否生成sourceMap&lt;/li>
&lt;li>默认值：false&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>严格检查&lt;/p>
&lt;ul>
&lt;li>strict
&lt;ul>
&lt;li>启用所有的严格检查，默认值为true，设置后相当于开启了所有的严格检查&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>alwaysStrict
&lt;ul>
&lt;li>总是以严格模式对代码进行编译&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>noImplicitAny
&lt;ul>
&lt;li>禁止隐式的any类型&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>noImplicitThis
&lt;ul>
&lt;li>禁止类型不明确的this&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>strictBindCallApply
&lt;ul>
&lt;li>严格检查bind、call和apply的参数列表&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>strictFunctionTypes
&lt;ul>
&lt;li>严格检查函数的类型&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>strictNullChecks
&lt;ul>
&lt;li>严格的空值检查&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>strictPropertyInitialization
&lt;ul>
&lt;li>严格检查属性是否初始化&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>额外检查&lt;/p>
&lt;ul>
&lt;li>noFallthroughCasesInSwitch
&lt;ul>
&lt;li>检查switch语句包含正确的break&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>noImplicitReturns
&lt;ul>
&lt;li>检查函数没有隐式的返回值&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>noUnusedLocals
&lt;ul>
&lt;li>检查未使用的局部变量&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>noUnusedParameters
&lt;ul>
&lt;li>检查未使用的参数&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>高级&lt;/p>
&lt;ul>
&lt;li>allowUnreachableCode
&lt;ul>
&lt;li>检查不可达代码&lt;/li>
&lt;li>可选值：
&lt;ul>
&lt;li>true，忽略不可达代码&lt;/li>
&lt;li>false，不可达代码将引起错误&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>noEmitOnError
&lt;ul>
&lt;li>有错误的情况下不进行编译&lt;/li>
&lt;li>默认值：false&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="4webpack">4、webpack&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>通常情况下，实际开发中我们都需要使用构建工具对代码进行打包，TS同样也可以结合构建工具一起使用，下边以webpack为例介绍一下如何结合构建工具使用TS。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>步骤：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>初始化项目&lt;/p>
&lt;ul>
&lt;li>进入项目根目录，执行命令 &lt;code> npm init -y&lt;/code>
&lt;ul>
&lt;li>主要作用：创建package.json文件&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>下载构建工具&lt;/p>
&lt;ul>
&lt;li>&lt;code>npm i -D webpack webpack-cli webpack-dev-server typescript ts-loader clean-webpack-plugin&lt;/code>
&lt;ul>
&lt;li>共安装了7个包
&lt;ul>
&lt;li>webpack
&lt;ul>
&lt;li>构建工具webpack&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>webpack-cli
&lt;ul>
&lt;li>webpack的命令行工具&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>webpack-dev-server
&lt;ul>
&lt;li>webpack的开发服务器&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>typescript
&lt;ul>
&lt;li>ts编译器&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>ts-loader
&lt;ul>
&lt;li>ts加载器，用于在webpack中编译ts文件&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>html-webpack-plugin
&lt;ul>
&lt;li>webpack中html插件，用来自动创建html文件&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>clean-webpack-plugin
&lt;ul>
&lt;li>webpack中的清除插件，每次构建都会先清除目录&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>根目录下创建webpack的配置文件webpack.config.js&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">path&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;path&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">HtmlWebpackPlugin&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;html-webpack-plugin&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">CleanWebpackPlugin&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;clean-webpack-plugin&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">optimization&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">minimize&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">false&lt;/span> &lt;span class="c1">// 关闭代码压缩，可选
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">entry&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;./src/index.ts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">devtool&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;inline-source-map&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">devServer&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">contentBase&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;./dist&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">output&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">path&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">path&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">resolve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">__dirname&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;dist&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">filename&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;bundle.js&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">environment&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">arrowFunction&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">false&lt;/span> &lt;span class="c1">// 关闭webpack的箭头函数，可选
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">resolve&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">extensions&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;.ts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;.js&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">module&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">rules&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">test&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sr">/\.ts$/&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">use&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">loader&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;ts-loader&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">exclude&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sr">/node_modules/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">plugins&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">new&lt;/span> &lt;span class="nx">CleanWebpackPlugin&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">new&lt;/span> &lt;span class="nx">HtmlWebpackPlugin&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">title&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="s1">&amp;#39;TS测试&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>根目录下创建tsconfig.json，配置可以根据自己需要&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;compilerOptions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;target&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ES2015&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;module&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ES2015&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;strict&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>修改package.json添加如下配置&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">...略...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;scripts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;test&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;echo \&amp;#34;Error: no test specified\&amp;#34; &amp;amp;&amp;amp; exit 1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;build&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;webpack&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;start&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;webpack serve --open chrome.exe&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">...略...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>在src下创建ts文件，并在并命令行执行&lt;code>npm run build&lt;/code>对代码进行编译，或者执行&lt;code>npm start&lt;/code>来启动开发服务器&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;h2 id="5babel">5、Babel&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>经过一系列的配置，使得TS和webpack已经结合到了一起，除了webpack，开发中还经常需要结合babel来对代码进行转换以使其可以兼容到更多的浏览器，在上述步骤的基础上，通过以下步骤再将babel引入到项目中。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>安装依赖包：&lt;/p>
&lt;ul>
&lt;li>&lt;code>npm i -D @babel/core @babel/preset-env babel-loader core-js&lt;/code>&lt;/li>
&lt;li>共安装了4个包，分别是：
&lt;ul>
&lt;li>@babel/core
&lt;ul>
&lt;li>babel的核心工具&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>@babel/preset-env
&lt;ul>
&lt;li>babel的预定义环境&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>@babel-loader
&lt;ul>
&lt;li>babel在webpack中的加载器&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>core-js
&lt;ul>
&lt;li>core-js用来使老版本的浏览器支持新版ES语法&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>修改webpack.config.js配置文件&lt;/p>
&lt;ul>
&lt;li>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...&lt;/span>&lt;span class="nx">略&lt;/span>&lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">module&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">rules&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">test&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sr">/\.ts$/&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">use&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">loader&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;babel-loader&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">options&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">presets&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;@babel/preset-env&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;targets&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;chrome&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;58&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;ie&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;11&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;corejs&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="s2">&amp;#34;3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;useBuiltIns&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;usage&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">loader&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;ts-loader&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">exclude&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sr">/node_modules/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...&lt;/span>&lt;span class="nx">略&lt;/span>&lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>如此一来，使用ts编译后的文件将会再次被babel处理，使得代码可以在大部分浏览器中直接使用，可以在配置选项的targets中指定要兼容的浏览器版本。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul></content></entry></feed>