<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-CN">
  <generator uri="https://jekyllrb.com/">Jekyll</generator>
  <title type="html">Libing Song&apos;s Blog (中文)</title>
  <subtitle>宋利兵 (Libing Song) 的中文文章。</subtitle>
  <link href="https://songlibing.github.io/feed-zh.xml" rel="self" type="application/atom+xml"/>
  <link href="https://songlibing.github.io/" rel="alternate" type="text/html"/>
  <updated>2026-07-21T19:36:00+08:00</updated>
  <id>https://songlibing.github.io/feed-zh.xml</id>
  <author>
    <name>宋利兵 (Libing Song)</name>
  </author>
  <entry>
    <title type="html">MySQL 大事务和 DDL 的复制延迟优化</title>
    <link href="https://songlibing.github.io/posts/mysql-large-transaction-ddl-replication/" rel="alternate" type="text/html" title="MySQL 大事务和 DDL 的复制延迟优化"/>
    <published>2026-07-17T17:00:00+08:00</published>
    <updated>2026-07-20T17:55:25+08:00</updated>
    <id>https://songlibing.github.io/posts/mysql-large-transaction-ddl-replication/</id>
    <author>
      <name>宋利兵 (Libing Song)</name>
    </author>
    <category term="MySQL"/>
    <category term="复制"/>
    <category term="DDL"/>
    <category term="大事务"/>
    <category term="主从延迟"/>
    <summary type="html">本文也有英文版：English version。 MySQL官方从MySQL-5.6开始优化复制延迟问题，最先实现了 Schema 级别的 Binlog 并发应用。然而这个并发能力并不能解决日常的复制延迟。然后官方在 MySQL-5.7上实现了基于提交顺序的并发回放策略(Commit Order), 这种策略依赖主上的事务并发执行数量。主上并发执行的事务非常多的情况下，从库上回放的速度才会快。如果主上并发少，从库上回放速度仍然会变慢，产生复制延迟。因此官方又在 MySQL-5.7 上又实现了行级的并发策略(Writeset), 这种并发策略无论主上并发事务的多少，都能在从库上快速的并发回放。 我们很早以前就在线上全面使用了基于 writeset 的复制策略，writeset 的使用解决了大概60%的复制延迟问题。另外有30%多的复制延迟问题源自于大事务和 DDL。在 MySQL 的复制体系里，大事务和 DDL 的复制延迟可谓是最难以解决的问题。去年，我们在 AliSQL 上实现了Binlog 实时复制(Binlog Realtime Replication 简称BRR)的机制，彻底解决了这个难题。 Binlog 实时复制的原理 大事务和 DDL 的复制延迟原因如上图所示。Binlog 复制是以事务为单位，只有事务执行完了才会写入 Binlog 文件，然后传输到从库执行( DDL 可以看做一个事务），只有从库执行完毕，业务才能看到。如果事务执行了很长时间，在从库也需要执行同样的时间，就会产生复制延迟。延迟的时间就是从库执行事务的时间。实际上延迟可能会更大：其一对于大事务来说，因为产生的 Binlog Events 非常大，还有一块是传输导致的延迟。其二在大事务尤其是 DDL 执行期间，可能会阻塞其他的事务回放，导致更多的 Relay Log 堆积。大事务和 DDL 回放完成后，这些堆积的事务也需要一定的时间回放，才能够追平。 优化的思路也非常直观，那就是让从库和主库同时开始执行大事务、DDL，主库提交后，通知从库提交事务。通过这套机制，大事务和 DDL 的复制延迟可以控制在1秒以内。以下是优化前、优化后大事务产生的延迟对比，采用了实时复制的机制后大事务不再导致复制延迟，DDL 也一样。 这个功能已于2025年在线上默认开启，截止目前累积3000+实例使用了该功能。大事务实时复制执行了约30万次，DDL实时复制执行了约6万次。 实时复制的实现 实时复制的核心思想只有一句话：主库执行开始就把 Binlog Events 或 DDL 发给从库，从库同步执行；主库最终提交或回滚，从库跟着提交或回滚。...</summary>
    <content type="html" xml:base="https://songlibing.github.io/posts/mysql-large-transaction-ddl-replication/">&lt;blockquote class=&quot;prompt-tip&quot;&gt;
  &lt;p&gt;本文也有英文版：&lt;a href=&quot;https://songlibing.github.io/posts/mysql-large-transaction-ddl-replication-en/&quot;&gt;English version&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;MySQL官方从MySQL-5.6开始优化复制延迟问题，最先实现了 Schema 级别的 Binlog 并发应用。然而这个并发能力并不能解决日常的复制延迟。然后官方在 MySQL-5.7上实现了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;基于提交顺序的并发回放策略(Commit Order)&lt;/code&gt;, 这种策略依赖主上的事务并发执行数量。主上并发执行的事务非常多的情况下，从库上回放的速度才会快。如果主上并发少，从库上回放速度仍然会变慢，产生复制延迟。因此官方又在 MySQL-5.7 上又实现了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;行级的并发策略(Writeset)&lt;/code&gt;, 这种并发策略无论主上并发事务的多少，都能在从库上快速的并发回放。&lt;/p&gt;

&lt;p&gt;我们很早以前就在线上全面使用了基于 writeset 的复制策略，writeset 的使用解决了大概60%的复制延迟问题。另外有30%多的复制延迟问题源自于大事务和 DDL。在 MySQL 的复制体系里，大事务和 DDL 的复制延迟可谓是最难以解决的问题。去年，我们在 AliSQL 上实现了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Binlog 实时复制(Binlog Realtime Replication 简称BRR)&lt;/code&gt;的机制，彻底解决了这个难题。&lt;/p&gt;

&lt;h2 id=&quot;binlog-实时复制的原理&quot;&gt;Binlog 实时复制的原理&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-repl-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;大事务和 DDL 的复制延迟原因如上图所示。Binlog 复制是以事务为单位，只有事务执行完了才会写入 Binlog 文件，然后传输到从库执行( DDL 可以看做一个事务），只有从库执行完毕，业务才能看到。如果事务执行了很长时间，在从库也需要执行同样的时间，就会产生复制延迟。延迟的时间就是从库执行事务的时间。实际上延迟可能会更大：其一对于大事务来说，因为产生的 Binlog Events 非常大，还有一块是传输导致的延迟。其二在大事务尤其是 DDL 执行期间，可能会阻塞其他的事务回放，导致更多的 Relay Log 堆积。大事务和 DDL 回放完成后，这些堆积的事务也需要一定的时间回放，才能够追平。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-repl-2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;优化的思路也非常直观，那就是让从库和主库同时开始执行大事务、DDL，主库提交后，通知从库提交事务。通过这套机制，大事务和 DDL 的复制延迟可以控制在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1秒&lt;/code&gt;以内。以下是优化前、优化后大事务产生的延迟对比，采用了实时复制的机制后大事务不再导致复制延迟，DDL 也一样。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-repl-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这个功能已于2025年在线上默认开启，截止目前累积3000+实例使用了该功能。大事务实时复制执行了约30万次，DDL实时复制执行了约6万次。&lt;/p&gt;

&lt;h2 id=&quot;实时复制的实现&quot;&gt;实时复制的实现&lt;/h2&gt;

&lt;p&gt;实时复制的核心思想只有一句话：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;主库执行开始就把 Binlog Events 或 DDL 发给从库，从库同步执行；主库最终提交或回滚，从库跟着提交或回滚&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;实时复制分为实时传输和实时应用两部分。实时传输将主库上大事务实时产生的 Binlog Events 流式的发送到从库上，这一部分在《&lt;a href=&quot;https://songlibing.github.io/posts/mysql-large-transaction-binlog-transmission-en/&quot;&gt;MySQL大事务的Binlog传输优化&lt;/a&gt;》做了介绍。实时应用则是实时的将这些传输过来的 Binlog Events 回放到从库，为此引入了一组额外的回放线程。如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-repl-4.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;事务在主库执行的过程中，产生的 Binlog Events 会先暂存在 Binlog Cache 里；如果这是一个大事务（Binlog Cache 大小超过阈值），主库上的 Dump 线程会读取 Binlog Cache 临时文件，把Binlog Cache 里的 Binlog Events 直接发送到从库。从库收到后，写入一个专门的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr Cache&lt;/code&gt;（不是 Relay Log 文件），由一组新的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr Worker&lt;/code&gt; 线程实时应用。&lt;/p&gt;

&lt;p&gt;对 DDL 来说，Binlog Events 的产生时机比大事务更晚——DDL 是在提交阶段才把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Query_log_event&lt;/code&gt; 写入 Binlog Cache。所以 BRR 对 DDL 做了一个特别处理：在主库开始DDL执行后，直接构造 Query_log_event 放到一个内存 Buffer &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ddl_query_buffer&lt;/code&gt; 里，Dump 线程从这个 Buffer 里读事件发给从库，从库上同样是通过 Brr Worker 实时执行 DDL。&lt;/p&gt;

&lt;p&gt;这样一来，DDL 和大事务的从库执行就从&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;主库执行完再执行&lt;/code&gt;变成了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;主从并行执行&lt;/code&gt;，最终延迟只剩下网络传输和提交的耗时，通常在十毫秒量级。&lt;/p&gt;

&lt;p&gt;下面我们从主库端和从库端两个视角，看看 BRR 是怎么实现的。&lt;/p&gt;

&lt;h3 id=&quot;brr-的整体架构&quot;&gt;BRR 的整体架构&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-repl-5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;主库端&quot;&gt;主库端&lt;/h4&gt;

&lt;p&gt;当一个大事务或者 DDL 需要进行实时复制时，会产生一个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx&lt;/code&gt; 注册到&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx_manager&lt;/code&gt;里。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_binlog_sender&lt;/code&gt; 是 Dump 线程的扩展，负责从 Brr_trx 读取 Events 并推送到从库。Dump 线程原本只做一件事：从 Binlog 文件读事件发给从库。BRR 给它增加了第二个职责：轮询每个活跃的 Brr_trx 从他们的 Binlog Cache 的临时文件 或者 ddl_query_buffer 里读 Binlog Events 发给从库。&lt;/p&gt;

&lt;p&gt;实时传输借用原有的 Dump 通道传输信息，为了区分 BRR 流量和普通流量，这里借鉴了 Semisync 的机制，给每个Event 添加了一个额外的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BRR Header&lt;/code&gt;。 通过 BRR Header 里的信息可以区分是 BRR 流量还是普通复制流量。 为了不让 BRR 事件把普通的 Binlog Events 通道堵死，BRR 做了流量控制。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-repl-6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;从库端&quot;&gt;从库端&lt;/h4&gt;

&lt;p&gt;从库 IO 线程根据 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BRR Header&lt;/code&gt; 把事件分成 BRR 事件和 Normal 事件两类：BRR 事件走 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt;，Normal 事件按原路走 Relay Log。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt; 是 BRR 事务在从库对应的存储，一个 BRR 事务对应一个 Brr_cache。从库 IO 线程收到一个 BRR 事件后，根据 BRR Header 里的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;brr_index&lt;/code&gt; 找到对应的 Brr_cache（如果是第一个事件，就创建一个新的 Brr_cache，并唤醒 Brr Worker），然后把 Events 写入 Brr_cache 的临时文件，更新可读位置。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_rpl_info&lt;/code&gt; 负责管理这些 BRR 事务。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BRR Worker&lt;/code&gt; 线程专门用来应用 BRR 事务。Brr Worker 在空闲时会找到一个未开始应用的 Brr_cache，把自己设为它的 owner。之后 Brr Worker 就绑定在这个 Brr_cache 上，循环读 Binlog Events 回放，直到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_log_event&lt;/code&gt;（说明主库已经提交），或者收到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BRR_ROLLBACK_EVENT&lt;/code&gt; (主库回滚)才结束。&lt;/p&gt;

&lt;h3 id=&quot;gtid_executed-快照&quot;&gt;gtid_executed 快照&lt;/h3&gt;

&lt;p&gt;主库上这些&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;未提交&lt;/code&gt;的 BRR 事务和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;已经提交&lt;/code&gt;的事务在从库上并行的执行，如果 BRR 事务和已经提交的事务有依赖关系，那么 BRR 事务的 Binlog Events 必须要等依赖的事务在从库回放完成后才能开始。否则就可能导致死锁、复制中断，严重的可能数据不一致。比如下面这个例子：&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;INTO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;VAUES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;  
&lt;span class=&quot;k&quot;&gt;UPDATE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t1&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;大事务&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;UPDATE 是大事务，它必须要在 INSERT 回放完成后，才能开始执行。如果 UPDATE 先执行，则在更新 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pk1&lt;/code&gt; 这条记录时因为记录不存在而报错。&lt;/p&gt;

&lt;p&gt;BRR 机制里使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed 快照&lt;/code&gt; 作为前后依赖关系的判定。当主库某个 DDL 或大事务开始执行时，主库当前的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed&lt;/code&gt; 值就代表了这个事务在主库看到的所有前序事务。只要从库的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed&lt;/code&gt; 也追上了这个值（是它的超集），就说明这个事务在从库依赖的所有前序事务都已经回放完毕，可以开始应用了。&lt;/p&gt;

&lt;p&gt;为此，BRR 引入了一个新的 event 类型 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_gtid_executed_log_event&lt;/code&gt;，body 存放一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed&lt;/code&gt; 集合。主库在特定的时刻会打 gtid_executed 快照，写入 BRR 通道；从库 Brr Worker 读到这个快照的 Binlog Events，会等待这个快照里的所有 Gtid 对应的事务都执行完毕，然后再继续应用后续操作。&lt;/p&gt;

&lt;h3 id=&quot;大事务实时复制&quot;&gt;大事务实时复制&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-repl-7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;brr_trx-的创建与更新&quot;&gt;Brr_trx 的创建与更新&lt;/h4&gt;

&lt;p&gt;一个事务在主库上执行时，Binlog Events 会先写入 Binlog Cache（一个内存 Buffer + 临时文件的结构）。MySQL 的实现里，当 Binlog Cache 用满内存 Buffer 后，会写入临时文件里。&lt;/p&gt;

&lt;p&gt;BRR 在这里插了一个钩子：每次向 Binlog Cache 写完一批 event 后，检查临时文件的大小。如果超过了一定的大小，就创建一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx&lt;/code&gt;，记录临时文件名和当前的可读位置，然后把它注册到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx_manager&lt;/code&gt; 里。之后每次向 Binlog Cache 追加 Binlog Events，都会更新 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;end_position&lt;/code&gt;，并唤醒 Dump 线程发送这些 Binlog Events 到从库。&lt;/p&gt;

&lt;h4 id=&quot;binlog-events-传输&quot;&gt;Binlog Events 传输&lt;/h4&gt;

&lt;p&gt;Dump 线程每次发送一批 Binlog Events 之前都要产生一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_gtid_executed_log_event&lt;/code&gt;, 作为当前这批 Binlog Events 的依赖事务的快照发送到从库，然后再发送这批 Binlog Events.&lt;/p&gt;

&lt;h4 id=&quot;事务提交&quot;&gt;事务提交&lt;/h4&gt;

&lt;p&gt;对大事务来说，Brr Worker 读到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_log_event&lt;/code&gt; 之前，Binlog Events 都写在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt; 的临时文件里，还不算 Relay Log。当主库最终提交时，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_log_event&lt;/code&gt; 通过 BRR 通道发到从库，IO 线程做两件事：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt; 的临时文件 rename 成一个 Relay Log 文件，主上的 Dump 线程会根据 Gtid 跳过这个这个事务的发送。这样这些 Binlog Events 就不会当作普通事务再传一遍。&lt;/li&gt;
  &lt;li&gt;通知 Brr Worker 读取 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_log_event&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Xid_log_event&lt;/code&gt;，完成事务的提交。&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;事务的回滚&quot;&gt;事务的回滚&lt;/h4&gt;

&lt;p&gt;回滚路径很简单，主库回滚时通过 BRR 通道发一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BRR_ROLLBACK_EVENT&lt;/code&gt;；从库 IO 线程收到后，直接给对应的 Brr Worker 发 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KILL_QUERY&lt;/code&gt; 信号。Brr Worker 检测到 KILL_QUERY 后，回滚当前事务，做清理工作，然后继续处理下一个 Brr_cache。&lt;/p&gt;

&lt;p&gt;注意被 Kill 了之后，Brr Worker 不会退出，也不会传播错误到 SQL 线程。这一点和普通 Worker 完全不一样，普通 Worker 遇到错误必须让整个复制停下来。因为主库那边事务已经提交，从库如果放弃执行，就会导致主从数据不一致；而 Brr Worker 执行的事务，是和主库同时执行的，主库回滚是正常路径，从库也必须回滚。&lt;/p&gt;

&lt;h3 id=&quot;ddl-的实时应用&quot;&gt;DDL 的实时应用&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-repl-8.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;brr_trx-的创建&quot;&gt;Brr_trx 的创建&lt;/h4&gt;

&lt;p&gt;在大事务里，我们通过 Binlog Cache 里的 Binlog Events 的总大小来判定是不是一个大事务。DDL 则复杂一些，有些 DDL 只操作元数据，这些 DDL 执行很快。操作了数据的 DDL 的执行时间和操作的数据量有关，要评估的精确会比较复杂。 因此对于 DDL 是否要进行实时复制，我们不是通过提前预估 DDL 的执行时间来决定， 而是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;通过 DDL 执行时间超时的方法来决定是否要执行实时复制&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;所有的 DDL 都会创建一个 Brr_trx， 但是这个 Brr_trx 不会立刻发送给从库。DDL 的 Brr_trx 还有一个门限，默认 1000 毫秒：只有当 DDL 的执行时长超过这个门限，Dump 线程才会开始发送这个 DDL 的 Brr_trx。如果一个 DDL 执行很快，在1秒内就完成了，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx&lt;/code&gt; 会被静默地清理掉，DDL 走普通的 Binlog 通道传到从库回放，和不开启 BRR 完全一样。&lt;/p&gt;

&lt;p&gt;DDL Brr_trx 是在 DDL 的 Prepare 阶段创建的，也就是在 DDL &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;获取到 MDL X 锁之后&lt;/code&gt;。因为只有获取了 X 锁才意味着 DDL 对这个表有了操作权限。其他有冲突的操作要么已经提交，要么要等待 DDL 释放了 X锁 或者到 DDL 结束才能开始。&lt;/p&gt;

&lt;h3 id=&quot;两次-gtid_executed-快照&quot;&gt;两次 gtid_executed 快照&lt;/h3&gt;

&lt;p&gt;对于 Online DDL 来说，会将执行分为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Prepare&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Execute&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Commit&lt;/code&gt; 三个阶段。在 Prepare 之后，会将 MDL 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X 锁&lt;/code&gt;降级为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;S 锁&lt;/code&gt;，因此在 Execute 阶段 DML 和 DDL 可以并行执行。在 Commit 阶段，又将 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;S 锁&lt;/code&gt; 升级 为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X 锁&lt;/code&gt;。升级为 X 锁 则意味着那些并行执行的 DML 都已经提交了。所以 DDL 在从库执行时也要遵循这个原则，要让这些已经提交的 DML 先回放完成，才能进入 Commit 阶段。&lt;/p&gt;

&lt;p&gt;因此对于 Online DDL 的实时复制，从库执行DDL时有两个必须要同步的时间点，一个是在进入 Prepare 阶段之前，一个是在进入 Commit 阶段之前。对应地，主库上需要抓取两次 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed&lt;/code&gt; 快照，一次是在 DDL 进入 Prepare 阶段之后，一次是在 DDL 进入 Execute 阶段之后。&lt;/p&gt;

&lt;h3 id=&quot;传两份-binlog-events&quot;&gt;传两份 Binlog Events&lt;/h3&gt;

&lt;p&gt;在大事务章节，我们提到大事务是通过 BRR 传输到从库上的，Binlog 文件中的大事务不会再次传送到从库。DDL 则会传输两次，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BRR 会传输一次，Binlog 文件中的 Binlog Events 还会传一次。&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;DDL 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Query_log_event&lt;/code&gt; 尺寸很小，重复传的成本可以忽略。但如果只传一份，就要走大事务那套 rename 逻辑（把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt; 的临时文件 rename 成 Relay Log），涉及一堆边界处理。对 DDL 来说，直接传两份&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt; 用完删掉是最简单的方案。&lt;/p&gt;

&lt;p&gt;顺序上，Dump 线程保证先传 BRR 事件，再传普通事件。这样 Brr Worker 一定能先拿到 DDL 开始执行；等普通事件到达 Relay Log，Brr Worker 已经在应用 DDL 了。&lt;/p&gt;

&lt;p&gt;普通 Worker 读到 Relay Log 里的 DDL 时，会检查这个 gtid 是不是在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;owned_gtids&lt;/code&gt; 里。如果是（说明 Brr Worker 正在执行），就等待；等 Brr Worker 提交后把 gtid 加到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed&lt;/code&gt;、释放 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;owned_gtids&lt;/code&gt;，普通 Worker 醒来发现 gtid 已经在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed&lt;/code&gt; 里，就会跳过整个 DDL。&lt;/p&gt;

&lt;p&gt;如果 Brr Worker 回滚了该DDL，那 Gtid 会从 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;owned_gtids&lt;/code&gt; 移除，也不会加到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed&lt;/code&gt;。普通 Worker 醒来后，发现该事务没有被执行，就会正常执行这个DDL —— &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;这是兜底路径，等价于关闭 BRR 的场景。&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&quot;结论&quot;&gt;结论&lt;/h2&gt;

&lt;p&gt;AliSQL 的 Binlog 实时复制通过主、从并行执行的机制解决了 MySQL Binlog 复制中最为棘手的复制延迟问题：大事务和DDL的复制延迟问题。此外，我们还针对 Writeset 机制、海量并发场景、批处理产生的中等事务场景也做了许多的优化。通过这些优化，消除了线上95%的复制延迟问题。&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title type="html">DDL &quot;Duplicate Key&quot; 错误详解</title>
    <link href="https://songlibing.github.io/posts/mysql-ddl-duplicate-key-problem/" rel="alternate" type="text/html" title="DDL &quot;Duplicate Key&quot; 错误详解"/>
    <published>2026-07-17T16:30:00+08:00</published>
    <updated>2026-07-17T18:04:15+08:00</updated>
    <id>https://songlibing.github.io/posts/mysql-ddl-duplicate-key-problem/</id>
    <author>
      <name>宋利兵 (Libing Song)</name>
    </author>
    <category term="MySQL"/>
    <category term="DDL"/>
    <category term="Online DDL"/>
    <category term="InnoDB"/>
    <summary type="html">MySQL 在执行 Online DDL 重建表的时候，可能会碰到 Duplicate Entry 错误，从而导致 DDL 中途失败。失败信息如下所示： 1 2 mysql&amp;gt; alter table tt add c3 int, algorithm=inplace; ERROR 1062 (23000): Duplicate entry &apos;1&apos; for key &apos;tt.uk_c2&apos; 这是一个非常知名的问题，自从 MySQL-5.6 引入了 Online DDL 之后就一直存在。相关的Bug包括: BUG#76895[1] Adding new column OR Drop column causes duplicate PK error BUG#77572[2] The bogus duplicate key error in online ddl with incorrect key name BUG#98600[3] Optimize table fails with duplicate entry on UNIQUE KEY BUG#104626[4] Remove failure of Online ALTER because concurrent...</summary>
    <content type="html" xml:base="https://songlibing.github.io/posts/mysql-ddl-duplicate-key-problem/">&lt;p&gt;MySQL 在执行 Online DDL 重建表的时候，可能会碰到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Duplicate Entry&lt;/code&gt; 错误，从而导致 DDL 中途失败。失败信息如下所示：&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;n&quot;&gt;mysql&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;alter&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tt&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c3&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;algorithm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;inplace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;  
&lt;span class=&quot;n&quot;&gt;ERROR&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1062&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;23000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Duplicate&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;1&apos;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;tt.uk_c2&apos;&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这是一个非常知名的问题，自从 MySQL-5.6 引入了 Online DDL 之后就一直存在。相关的Bug包括:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;BUG#76895[1] Adding new column OR Drop column causes duplicate PK error&lt;/li&gt;
  &lt;li&gt;BUG#77572[2] The bogus duplicate key error in online ddl with incorrect key name&lt;/li&gt;
  &lt;li&gt;BUG#98600[3] Optimize table fails with duplicate entry on UNIQUE KEY&lt;/li&gt;
  &lt;li&gt;BUG#104626[4] Remove failure of Online ALTER because concurrent Duplicate entry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;BUG#76895[5]最早报告了这个问题。BUG#76895 上提到的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Duplicate PRIMARY KEY&lt;/code&gt; 问题，实际上是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Duplicate UNIQUE KEY&lt;/code&gt;。因为 MySQL-5.6 上有另一个 BUG#77572[6]在返回的失败消息中，错误的写了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PRIMARY KEY&lt;/code&gt;。BUG#77572 在 MySQL-5.6.28 和 MySQL-5.7.10 做了修复。后来的版本中人们看到的就是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Duplicate UNIQUE KEY&lt;/code&gt; 错误，所以又有人报告了 BUG#98600.&lt;/p&gt;

&lt;p&gt;BUG#76895中，MySQL官方回复&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Not a Bug&lt;/code&gt;，认为这是 Online DDL 带来的副作用，所以一直没有修复这个问题。然而这个问题会导致 DDL 操作失败，对 DBA 运维来说是非常痛苦的一个问题。一旦 DDL 失败，原本的运维、变更计划就会被打乱甚至推迟。所以社区又有人提报了 BUG#104626，将其作为功能需求提了出来。&lt;/p&gt;

&lt;p&gt;这个 Bug 的根因是什么？为什么官方认为这不是一个Bug？ 有没有什么办法规避？要回答以上的三个问题，我们要先了解 Online DDL 的原理。&lt;/p&gt;

&lt;h2 id=&quot;online-ddl-的基本原理&quot;&gt;Online DDL 的基本原理&lt;/h2&gt;

&lt;p&gt;为了实现在执行 DDL 期间可以同时执行 DML 操作，Online DDL 设计了一套&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;全量拷贝&lt;/code&gt;加&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;增量回放&lt;/code&gt;的机制。Online DDL 先将表中已经存在的全量数据应用到新表中(如果是添加索引则不需要创建新表，而是直接应用到新的索引上)。全量应用的过程比较长，期间允许执行DML，并记录增量。全量回放完成后，将增量应用到新表中。这个机制有两个关键点：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;要有一个方法可以确切的区分一个特定时间点前后产生的数据，全量拷贝时只拷贝这个时间点前产生的数据。&lt;/li&gt;
  &lt;li&gt;这个时间点后对数据的更新都要记录到一个增量日志中，全量拷贝完成后通过回放增量日志来补齐所有数据。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;如何区分全量和增量数据&quot;&gt;如何区分全量和增量数据&lt;/h3&gt;

&lt;p&gt;Online DDL 中&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;全量数据&lt;/code&gt;是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Clustered B+Tree&lt;/code&gt;中的数据。Online DDL 通过扫描&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Clustered B+Tree&lt;/code&gt;来获取全量数据。在扫描的过程中允许 DML 操作，因此 B+Tree 上实际上包含了增量的修改。Online DDL 利用了 InnoDB 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MVCC （多版本并发控制）&lt;/code&gt;机制来区分全量和增量数据。&lt;/p&gt;

&lt;p&gt;InnoDB 的 MVCC 机制允许一个事务看到一个一致的数据快照，而不受其他并发事务的影响。其核心是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;read view&lt;/code&gt;：read view 创建时会记录当前所有活跃事务的 ID 列表，之后通过这个列表判断每条记录的哪个版本对自己可见。对于 read view 创建之后提交的数据修改，即使已经写入了聚簇索引，read view 也能通过 undo log 找到旧版本来读取。&lt;/p&gt;

&lt;p&gt;Online DDL 正是利用这个能力来划定全量拷贝的边界：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;在一个确定的时间点创建 read view，全量拷贝只读取这个 read view 可见的数据。之后并发 DML 产生的变更，无论何时提交，都不会影响全量拷贝读到的内容。&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&quot;online-ddl-的三个阶段&quot;&gt;Online DDL 的三个阶段&lt;/h2&gt;

&lt;p&gt;InnoDB 将 Online DDL 的过程分为三个阶段。三个阶段各自持有不同级别的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;元数据锁(Metadata Lock, MDL)&lt;/code&gt;，精确控制并发 DML 的行为。&lt;/p&gt;

&lt;h3 id=&quot;prepare-阶段&quot;&gt;Prepare 阶段&lt;/h3&gt;

&lt;p&gt;Prepare 阶段在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_EXCLUSIVE&lt;/code&gt; 保护下执行，此时这个表的所有读写都被阻塞。在这个”停写”窗口就是区分全量和增量数据的关键时间点。在这个阶段&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;trx_assign_read_view()&lt;/code&gt; 创建一致性读视图&lt;/li&gt;
  &lt;li&gt;在相应的索引上初始化 row log（增量日志）， 并为索引设置 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ONLINE_INDEX_CREATION&lt;/code&gt; 标记。后续索引 B+tree 上的增删改操作都会根据 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ONLINE_INDEX_CREATION&lt;/code&gt; 来决定是否需要记录到 row log 中。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Row log 的安装和 read view 的创建都在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_EXCLUSIVE&lt;/code&gt; 下完成。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_EXCLUSIVE&lt;/code&gt; 保证了两件事：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;确保 row log 和 read view 作为一个整体，能够无缝覆盖存量数据和数据变更。&lt;/li&gt;
  &lt;li&gt;2. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;排除正在执行中的事务&lt;/code&gt;。如果没有 MDL_EXCLUSIVE，可能存在一个事务已经修改了此表的数据但尚未提交。此时 row log 尚未安装，变更未被记录到 row log。尚未提交的事务在 read view 中标记为活跃事务，全量拷贝看不到。这条 DML 的修改既不在全量拷贝中也不在 row log 中，造成数据丢失。MDL_EXCLUSIVE 确保此刻没有任何修改了此表的事务在执行，从而让 row log 和 read view 建立一个精确的分界点：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;分界点之前已提交的数据由全量拷贝负责，分界点之后的所有 DML 都被 row log 捕获。&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Prepare 阶段完成后，MDL 从 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_EXCLUSIVE&lt;/code&gt; 降级为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_SHARED_UPGRADABLE&lt;/code&gt;，这是一个共享锁。&lt;/p&gt;

&lt;h3 id=&quot;execute-阶段&quot;&gt;Execute 阶段&lt;/h3&gt;

&lt;p&gt;Execute 阶段 Online DDL 持有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_SHARED_UPGRADABLE&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;允许并发 DML 正常执行&lt;/code&gt;。全量拷贝和第一次增量回放都在这个阶段完成。这是 Online DDL 中最耗时的阶段，也是 DML 不受阻塞的阶段。该表的 DML 操作都会记录到 row log 中。&lt;/p&gt;

&lt;h3 id=&quot;commit-阶段&quot;&gt;Commit 阶段&lt;/h3&gt;

&lt;p&gt;Commit 阶段 DDL 将 MDL 从 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_SHARED_UPGRADABLE&lt;/code&gt; 升级回 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_EXCLUSIVE&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;阻塞所有读写操作&lt;/code&gt;。在此保护下完成 row log 的回放，并提交 DDL 事务。由于 Execute 阶段已经回放了大部分 row log，Commit 阶段需要处理的数据很少，持有排他锁的时间很短。&lt;/p&gt;

&lt;p&gt;这个阶段的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_EXCLUSIVE&lt;/code&gt; 也保证了两件事：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;保证 Execute 阶段所有修改了该表的 DML 事务都提交了。&lt;/li&gt;
  &lt;li&gt;阻塞新的 DML 执行，确保不会产生新的 Row Log。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;从上面的内容可以看到 Online DDL并&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;不是全程 Online&lt;/code&gt;，在 Prepare 和 Commit 阶段会阻塞 DML 操作。此外由于 Metadata Lock 是事务级别的锁，如果在 DDL 获取 MDL_EXCLUSIVE 锁时有一个长事务在执行，DDL 就会被阻塞很长的时间。这个 DDL 在等待获取 MDL_EXCLUSIVE 锁时也会阻塞其他 DML 因此导致此表不可读写。&lt;/p&gt;

&lt;h2 id=&quot;全量拷贝&quot;&gt;全量拷贝&lt;/h2&gt;

&lt;p&gt;全量拷贝在 Execute 阶段执行，此时 MDL 为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_SHARED_UPGRADABLE&lt;/code&gt;，并发 DML 可以执行。&lt;/p&gt;

&lt;p&gt;全量拷贝调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;row_merge_read_clustered_index()&lt;/code&gt; 按 PRIMARY KEY 顺序从小到大扫描旧表的聚簇索引。扫描过程中对每一条记录通过 MVCC 判断可见性。只有 Read View 可见的数据才会被拷贝到新表中，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete-marked&lt;/code&gt; 的记录会被跳过。&lt;/p&gt;

&lt;p&gt;由于 MVCC 的特性，即使并发 DML 在扫描过程中修改了旧表的数据，全量拷贝通过 undo log 仍然能读到 read view 时刻的旧版本。全量拷贝看到的始终是一个一致性快照，不受并发 DML 的影响。&lt;/p&gt;

&lt;h2 id=&quot;增量日志记录&quot;&gt;增量日志记录&lt;/h2&gt;

&lt;p&gt;Row log 是对一个 B+tree 上数据页变化的记录，分两种情形：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;重建表：只记录 clustered B+tree 上的变化，包括&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INSERT&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&lt;/code&gt;。在回放 row log 时，这条记录会被&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;回放到所有的索引上&lt;/code&gt;。&lt;/li&gt;
  &lt;li&gt;创建索引：只记录正在创建的索引的 B+tree 的变化，包括 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INSERT&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&lt;/code&gt;，没有&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE&lt;/code&gt;。因为这是正在创建的索引，所以 B+Tree 上的操作只会记录到 row log 中，而&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;不会真正的修改 B+tree&lt;/code&gt;。这和重建表是不一样的，重建表的时候老表的所有索引都要实时更新。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这里要特别注意 row log 记录的时机。Row log 记录的是 B+tree 上的变化，所以是在一个 B+tree 的操作成功后记录的。如下图所示:&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;重建表场景下，当 clustered B+tree 插入成功后会记录 row log。在添加索引场景下，则是在 secondary index B+tree 插入时记录的。&lt;/code&gt;添加索引的场景下不会真正的往 B+tree 上插入记录，只是记录一下 row log。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/ddl-dupkey-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;dml-回滚场景&quot;&gt;DML 回滚场景&lt;/h3&gt;

&lt;p&gt;当 DML 执行时，要先操作 clustered B+tree 然后操作 secondary index B+tree. 这里就有一个疑问，如果二级索引的操作失败了，会发生什么？&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;首先，这条 row log 仍然存在，不会被清理掉。row log 记录的是 B+tree 上的一次操作，这一次操作是确实发生了的。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;• 其次， DML 语句会回滚。DML 回滚时，会根据 undo log 的内容，再一次操作 B+tree, 这一次的操作同样也会记录到 row log 中。因此一个失败的 INSERT 会记录两条 row log，一条是 ROW_T_INSERT, 一条是 ROW_T_DELETE，如下图所示。 两条 row log 按顺序执行后就相当于没有产生这条记录，符合回滚预期。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/ddl-dupkey-2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;失败导致回滚只是其中一个特例，实际上所有的回滚操作都是这样的逻辑，包括用户手动执行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ROLLBACK&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&quot;增量日志回放&quot;&gt;增量日志回放&lt;/h2&gt;

&lt;p&gt;Row log 的回放一共执行两次：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;第一次回放&lt;/code&gt;在 Execute 阶段，全量拷贝完成后立即执行。此时 MDL 为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_SHARED_UPGRADABLE&lt;/code&gt;，并发 DML 仍在继续，还会产生新的 row log。这次回放的目的是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;尽量消化已有的 row log，减少 Commit 阶段持有排他锁的时间&lt;/code&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;第二次回放&lt;/code&gt;在 Commit 阶段，MDL 已升级为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_EXCLUSIVE&lt;/code&gt; ，不会有新的 row log 产生。这次回放处理剩余的少量增量数据，完成后新表的数据与旧表完全一致。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;两次回放都是按写入 row log 的顺序逐条读取回放。对于重建表的场景，这条 row log 会被应用到新表的所有 Index 的 B+tree 上；对于创建索引的场景，则只会将 row log 应用到新创建的索引上。&lt;/p&gt;

&lt;h2 id=&quot;duplicate-entry-的根因&quot;&gt;Duplicate Entry 的根因&lt;/h2&gt;

&lt;p&gt;在前面的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DML 回滚场景&lt;/code&gt;中介绍了 INSERT 时即使 Unique Index 上发生了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Duplicate Entry&lt;/code&gt;错误，也会记录 row log，而且记录的是两条。如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/ddl-dupkey-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在回放第一条 row log&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ROW_T_INSERT&lt;/code&gt;时，和 INSERT 语句的执行逻辑是一样。要先插入一行记录到 clustered B+tree，然后到每个索引上插入一条记录。因此当向唯一索引插入这条记录时，同样也会报&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Duplicate Entry&lt;/code&gt;的错误。正是这个 duplicate entry 导致了 DDL 语句的失败。&lt;/p&gt;

&lt;h3 id=&quot;测试用例&quot;&gt;测试用例&lt;/h3&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;  
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c1&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;INT&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AUTO_INCREMENT&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;  
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c2&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;INT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;  
&lt;span class=&quot;err&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;UNIQUE&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;KEY&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;  
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ENGINE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;InnoDB&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;开启 3 个会话分别执行如下的语句&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;o&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;Session&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;  
&lt;span class=&quot;k&quot;&gt;BEGIN&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;；&lt;/span&gt;  
&lt;span class=&quot;k&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;INTO&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;VALUES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;  
  
&lt;span class=&quot;o&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;Session&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
&lt;span class=&quot;k&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t1&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ENGINE&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;InnoDB&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;  
  
&lt;span class=&quot;o&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;Session&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;  
&lt;span class=&quot;k&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;INTO&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;VALUES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;执行完上述的语句后，将 Session 1 的事务提交。然后你会发现 Session 2 的 ALTER 和 Session 3 的 INSERT 都报了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Duplicate Entry&lt;/code&gt;&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;o&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;Session&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
&lt;span class=&quot;n&quot;&gt;mysql&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t1&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ENGINE&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;InnoDB&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;  
&lt;span class=&quot;n&quot;&gt;ERROR&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1062&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;23000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Duplicate&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;1&apos;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;t1.c2&apos;&lt;/span&gt;  
  
&lt;span class=&quot;o&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;Session&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;  
&lt;span class=&quot;n&quot;&gt;mysql&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;INTO&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;VALUES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;  
&lt;span class=&quot;n&quot;&gt;ERROR&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1062&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;23000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Duplicate&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;1&apos;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;t1.c2&apos;&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个例子利用了前面提到的 Online DDL 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Metadata Lock&lt;/code&gt; 的机制来构造。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Session 1 的 INSERT 首先持有了 t1 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_SHARED_WRITE&lt;/code&gt; lock，这个 lock 在事务提交时才会释放。&lt;/li&gt;
  &lt;li&gt;Session 2 的 ALTER 在 prepare 阶段时 需要申请 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_EXCLUSIVE&lt;/code&gt; lock, 被 Session 1 阻塞。&lt;/li&gt;
  &lt;li&gt;Session 3 的 INSERT 同样要申请 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_SHARED_WRITE&lt;/code&gt; lock，但是被 Session 2 阻塞。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;通过 performance_schema 的 metadata_locks 表可以看到这些 session 上的metadata locks。如下图所示：其中第一行是 Session 1 持有的锁。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/ddl-dupkey-4.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;当 Session 1 的事务提交后， Session 2 获得&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_EXCLUSIVE&lt;/code&gt;lock，执行完 prepare 阶段后，降级到&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_SHARED_UPGRADABLE&lt;/code&gt;lock。这个锁和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_SHARED_WRITE&lt;/code&gt;不冲突，所以 Session 3 获得了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MDL_SHARED_WRITE&lt;/code&gt;开始执行。执行的过程中因为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c2 = 1&lt;/code&gt;已经存在，就报了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Duplicate key&lt;/code&gt;的错误。这个过程中记录了 row log，因此也导致了 Session 2 ALTER 语句的错误。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/ddl-dupkey-5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;如何避免这个问题&quot;&gt;如何避免这个问题&lt;/h3&gt;

&lt;p&gt;在理解了这个问题产生的原因后，我认为这就是一个 Bug，于是我们在 AliSQL 上修复了这个问题。如果你使用的是社区版 MySQL, 要想避免这个问题，那就是尽量去避免 DML 执行时出现 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Duplicate Entry&lt;/code&gt; 的情况。&lt;/p&gt;

&lt;p&gt;一个常见的场景是给自增主键的表添加了唯一索引，业务在 INSERT 一条记录时，自增键由 MySQL 产生。业务上有重试的逻辑，一旦前一个 INSERT 慢了，可能就会在另一个会话里重试这个SQL。从业务上来说逻辑是合理的，因为有唯一索引，只有一个 INSERT 能成功。但是恰恰就是这个逻辑，导致了 DDL 的失败。因此在 DDL 期间可以尝试加大重试的超时时间来避免这个问题。&lt;/p&gt;

&lt;h2 id=&quot;alisql-上的优化&quot;&gt;AliSQL 上的优化&lt;/h2&gt;

&lt;p&gt;AliSQL 上选择了一个比较直观，也比较简单的方案: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;当碰到 Duplicate Entry 错误时，忽略掉这个错误。&lt;/code&gt;&lt;/p&gt;

&lt;h3 id=&quot;真重复假重复&quot;&gt;真重复、假重复&lt;/h3&gt;

&lt;p&gt;这个忽略的策略是有限制条件的，并不是所有的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Duplicate Entry&lt;/code&gt; 都能忽略掉。&lt;/p&gt;

&lt;p&gt;在 Online DDL 期间还存在一种&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;真重复&lt;/code&gt;的场景：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DDL 引入了新的唯一性约束(新增/更改主键,或新增 UNIQUE 索引)，原本的数据中存在重复记录&lt;/code&gt;。新增的唯一性约束在 Online DDL 期间是不生效的，因此这期间的 DML 操作仍然能够引入重复的记录。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;当碰到这样的情况时，DDL 的执行必须要失败&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;如果 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Duplicate Entry&lt;/code&gt; 发生在原表已经存在的唯一索引上，一定是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;假重复&lt;/code&gt;，可以被跳过。这是最常见的场景，AliSQL 的优化的就是这个场景。&lt;/p&gt;

&lt;h3 id=&quot;后续-undo-row-log-的处理&quot;&gt;后续 Undo Row Log 的处理&lt;/h3&gt;

&lt;p&gt;前面说了 DML 执行失败时，实际上记录了两条 row log。以 INSERT 为例：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&amp;lt;ROW_T_INSERT, pk1, ...&amp;gt;  
&amp;lt;ROW_T_DELETE, pk1&amp;gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Row Log 回放时，往唯一索引 B+tree 上插入数据失败，被忽略掉了。但是主键 B+tree 上是插入成功了的。当回放第二条日志时，主键 B+tree 上的记录会被删掉，这个没有问题。但是唯一索引上本来就没有插入成功，就找不到这条记录，这会导致回放失败。&lt;/p&gt;

&lt;p&gt;因此当遇到已经存在的唯一索引上的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Duplicate Entry&lt;/code&gt;错误时，我们需要将这个错误记录下来。当回放回滚产生的 row log 时，同样也跳过这个索引上的操作。如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/ddl-dupkey-6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;UPDATE 的情况会更加复杂，如果一个 UPDATE 修改了一个二级索引列，要在二级索引上执行两个操作：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;删除老的记录&lt;/li&gt;
  &lt;li&gt;插入新的记录&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;回放 row log 时的失败发生在第 2 步，因此执行后续的回滚操作时就不能简单的跳过这个索引上的所有操作，而是要跳过第1步，第2步还要执行, 如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/ddl-dupkey-7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;通过以上的设计，AliSQL 避免了 Online DDL 时不必须要的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Duplicate Entry&lt;/code&gt;错误。&lt;/p&gt;

&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;

&lt;p&gt;Online DDL 有时会报 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Duplicate Entry&lt;/code&gt; 的错误，导致 DDL 执行失败。这个报错具有不确定性，很难完全避免。一旦 DDL 失败，原本的运维、变更计划就会被打乱甚至推迟。这个问题产生的原因是 Online DDL 期间有并发的 DML 出现了 ‘Duplicate Entry’ 错误导致的。 DML 的这个错误通过 Online DDL 的 row log 传到到了 DDL 上，导致了 DDL 的错误发生。 AliSQL 上对 Online DDL 做了优化，针对已经存在的唯一索引上发生的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Duplicate Entry&lt;/code&gt; 错误做了忽略处理，让 Online DDL 不再因这个错误而中断。&lt;/p&gt;

&lt;h4 id=&quot;引用链接&quot;&gt;引用链接&lt;/h4&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[1]&lt;/code&gt; BUG#76895:&lt;em&gt;https://bugs.mysql.com/bug.php?id=76895&lt;/em&gt;&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[2]&lt;/code&gt;BUG#77572:&lt;em&gt;https://bugs.mysql.com/bug.php?id=77572&lt;/em&gt;&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[3]&lt;/code&gt;BUG#98600:&lt;em&gt;https://bugs.mysql.com/bug.php?id=98600&lt;/em&gt;&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[4]&lt;/code&gt;BUG#104626:&lt;em&gt;https://bugs.mysql.com/bug.php?id=104626&lt;/em&gt;&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[5]&lt;/code&gt;BUG#76895:&lt;em&gt;https://bugs.mysql.com/bug.php?id=76895&lt;/em&gt;&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[6]&lt;/code&gt;BUG#77572:&lt;em&gt;https://bugs.mysql.com/bug.php?id=77572&lt;/em&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title type="html">MySQL 大事务的 Binlog 传输优化</title>
    <link href="https://songlibing.github.io/posts/mysql-large-transaction-binlog-transmission/" rel="alternate" type="text/html" title="MySQL 大事务的 Binlog 传输优化"/>
    <published>2026-07-16T10:00:00+08:00</published>
    <updated>2026-07-21T12:42:57+08:00</updated>
    <id>https://songlibing.github.io/posts/mysql-large-transaction-binlog-transmission/</id>
    <author>
      <name>宋利兵 (Libing Song)</name>
    </author>
    <category term="MySQL"/>
    <category term="复制"/>
    <category term="Binlog"/>
    <category term="大事务"/>
    <category term="半同步"/>
    <summary type="html">大事务是MySQL中一个痛点问题， 不仅会带来复制延迟，也会带来大量的稳定性问题。上一篇文章《MySQL大事务提交优化》介绍了大事务在提交时带来的问题，以及AliSQL中做的技术优化。这篇文章会介绍大事务在半同步复制时的问题，以及AliSQL中如何解决这个问题。 在《MySQL大事务提交优化》中，我们提到大事务提交时写Binlog会导致系统出现如下奇怪的慢SQL。 平时执行很快的INSERT语句，竟然执行了1.3s，并且慢SQL记录里也没有看到长时间的锁等待。 多语句事务的所有语句都已经执行完了，但是COMMIT语句竟然执行了1.3s。 除了大事务写Binlog会导致这种表现外，半同步复制时大事务的Binlog传输也会导致这种表现。以下是一个模拟测试的结果，我们用sysbench oltp_write_only来模拟正常的写业务，然后在后台提交了一个产生了2GB Binlog Events的事务(已经做了大事务提交优化)。可以看到在大事务提交时，写操作跌0，直到半同步复制超时后才恢复。 根因分析 上图是半同步复制的事务提交过程图： 当事务提交时，需要执行两阶段提交，首先进行Prepare。 然后将Binlog Events写入到Binlog文件。 写完Binlog后，开始等待自己的Binlog Events被发送到备库(after_sync模式)。 此后Binlog dump线程将该事务的Binlog Events发送到备节点。 备节点的IO线程负责接收这些Binlog Events，并且将收到的Binlog Events写入到Relay Log文件中。 当接收到完整的事务后，IO线程会给主库一个应答，告诉主库收到了这个事务的所有Binlog Events。这个应答是用Binlog文件名和位点来表示的。上图中Trx_n的binlog结束位点是530，因此备节点的IO线程会发送master-bin.000001:530给主库，这表示master-bin.000001:530之前的所有事务都已经收到了。 主库的Semisync Ack Receiver线程收到应答后，会根据位点唤醒相应的事务。 事务被唤醒后，就继续进行提交操作，提交完成后返回用户OK。 主节点和备节点之间只有一个Dump线程，Dump线程按照Binlog Events写入Binlog的顺序进行传输。备节点的IO线程也是按照这个顺序将收到的Binlog Events写入Relay log中，然后应答主库。因此只有前面的事务发送完成，才能发送后面的事务。如果当前的事务的Binlog Events非常多，那么这个事务的Binlog Events发送的时间就会非常的长。后面的事务尽管很小，也必须要等待。这个等待不仅包含自己事务的传输时间，也包含前面大事务的传输时间。因此就会出现上面慢日志的情况，一个小的事务突然变的很慢。 为了应对这种情况，MySQL提供了rpl_semi_sync_master_timeout参数。该参数定义了事务等待ACK的时长，当等待的时长超过rpl_semi_sync_master_timeout后，自动退化成异步复制。我们可以将该参数设置一个较小的值，来避免大事务导致整个实例不可写的严重情况。 基于半同步复制的RPO = 0方案 因为在半同步复制中，事务要等Binlog复制到备节点后才能提交，人们自然而然就会想到通过半同步复制来构建RPO=0的数据一致性方案。 这个架构中需要两个备节点，半同步复制保证事务在收到任意一个备节点的应答后才能提交。 如果主宕机，那么数据至少复制到了一个备上。 如果一个备节点不可用，也不会影响集群的可用性。 要保证RPO=0，就要保证半同步不能退化成异步。MySQL的半同步复制有两个可退化异步的点： 宕机重启后，已经写入Binlog的事务会自动提交，此时这些事务可能还没有复制到备库上。 当等待时间达到rpl_semi_sync_master_timeout设置的时间后，退化成异步。 前者是没办法控制的，需要修改MySQL的代码来解决。后者则需要给rpl_semi_sync_master_timeout设置一个非常大的值，让半同步不能退化成异步。大事务无疑是RPO=0方案中非常棘手的问题。因为一旦有了大事务，就会导致整个集群不可写。因此在RPO=0的技术方案中，我们必须要采取一些措施。如果DBA对业务有比较高的约束力，则会采取措施在业务中规避大事务的出现。然而对于大公司而言，业务纷繁复杂很难做到全面消除大事务。RDS服务提供商，则对使用者完全没有约束力。在实际的环境中，可用性往往比数据一致性重要的多。因此许多方案中都会采用临时退化的策略，当有大事务时临时退化成异步的形式。 大事务实时传输 在AliSQL中，我们设计了一套实时传输的机制来解决大事务传输导致的问题，有了这套传输机制就不需要将半同步退化为异步了。 大事务实时传输机制在事务做DML期间，就将事务产生的Binlog Events从Binlog Cache的临时文件中读出，传送到备节点。关键的步骤包括： 在DML执行期间，当事务产生的Binlog Events超过一定的量时，该事务会被注册到大事务列表中，当作大事务进行处理。 Binlog Dump线程会根据大事务列表中的信息，读取大事务的Binlog临时文件，将内容发送到备节点。大事务的Binlog Events和Binlog文件中的Binlog Events交替发送，并且对大事务的发送做了限流处理，优先发送Binlog文件中的内容，保证当前正在提交的事务不受影响。 大事务的Binlog Events发送时有特殊的标记和额外信息。当备节点的IO线程收到大事务的Events，将其存储到一个临时文件中，这里称作Relay...</summary>
    <content type="html" xml:base="https://songlibing.github.io/posts/mysql-large-transaction-binlog-transmission/">&lt;p&gt;大事务是MySQL中一个痛点问题， 不仅会带来复制延迟，也会带来大量的稳定性问题。上一篇文章《&lt;a href=&quot;https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization&quot;&gt;MySQL大事务提交优化&lt;/a&gt;》介绍了大事务在提交时带来的问题，以及AliSQL中做的技术优化。这篇文章会介绍大事务在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;半同步&lt;/code&gt;复制时的问题，以及AliSQL中如何解决这个问题。&lt;/p&gt;

&lt;p&gt;在《&lt;a href=&quot;https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization&quot;&gt;MySQL大事务提交优化&lt;/a&gt;》中，我们提到大事务提交时写Binlog会导致系统出现如下奇怪的慢SQL。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-binlog-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;平时执行很快的INSERT语句，竟然执行了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.3s&lt;/code&gt;，并且慢SQL记录里也没有看到长时间的锁等待。&lt;/li&gt;
  &lt;li&gt;多语句事务的所有语句都已经执行完了，但是COMMIT语句竟然执行了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.3s&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;除了大事务写Binlog会导致这种表现外，半同步复制时大事务的Binlog传输也会导致这种表现。以下是一个模拟测试的结果，我们用sysbench oltp_write_only来模拟正常的写业务，然后在后台提交了一个产生了2GB Binlog Events的事务(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;已经做了大事务提交优化&lt;/code&gt;)。可以看到在大事务提交时，写操作跌0，直到半同步复制超时后才恢复。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-binlog-2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;根因分析&quot;&gt;根因分析&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-binlog-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;上图是半同步复制的事务提交过程图：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;当事务提交时，需要执行两阶段提交，首先进行Prepare。&lt;/li&gt;
  &lt;li&gt;然后将Binlog Events写入到Binlog文件。&lt;/li&gt;
  &lt;li&gt;写完Binlog后，开始等待自己的Binlog Events被发送到备库(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after_sync&lt;/code&gt;模式)。&lt;/li&gt;
  &lt;li&gt;此后Binlog dump线程将该事务的Binlog Events发送到备节点。&lt;/li&gt;
  &lt;li&gt;备节点的IO线程负责接收这些Binlog Events，并且将收到的Binlog Events写入到Relay Log文件中。&lt;/li&gt;
  &lt;li&gt;当接收到完整的事务后，IO线程会给主库一个应答，告诉主库收到了这个事务的所有Binlog Events。这个应答是用Binlog文件名和位点来表示的。上图中Trx_n的binlog结束位点是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;530&lt;/code&gt;，因此备节点的IO线程会发送&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;master-bin.000001:530&lt;/code&gt;给主库，这表示&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;master-bin.000001:530&lt;/code&gt;之前的所有事务都已经收到了。&lt;/li&gt;
  &lt;li&gt;主库的Semisync Ack Receiver线程收到应答后，会根据位点唤醒相应的事务。&lt;/li&gt;
  &lt;li&gt;事务被唤醒后，就继续进行提交操作，提交完成后返回用户OK。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;主节点和备节点之间只有一个Dump线程，Dump线程按照Binlog Events写入Binlog的顺序进行传输。备节点的IO线程也是按照这个顺序将收到的Binlog Events写入Relay log中，然后应答主库。因此只有前面的事务发送完成，才能发送后面的事务。如果当前的事务的Binlog Events非常多，那么这个事务的Binlog Events发送的时间就会非常的长。后面的事务尽管很小，也必须要等待。这个等待不仅包含自己事务的传输时间，也包含前面大事务的传输时间。因此就会出现上面慢日志的情况，一个小的事务突然变的很慢。&lt;/p&gt;

&lt;p&gt;为了应对这种情况，MySQL提供了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rpl_semi_sync_master_timeout&lt;/code&gt;参数。该参数定义了事务等待ACK的时长，当等待的时长超过&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rpl_semi_sync_master_timeout&lt;/code&gt;后，自动退化成异步复制。我们可以将该参数设置一个较小的值，来避免大事务导致整个实例不可写的严重情况。&lt;/p&gt;

&lt;h2 id=&quot;基于半同步复制的rpo--0方案&quot;&gt;基于半同步复制的RPO = 0方案&lt;/h2&gt;

&lt;p&gt;因为在半同步复制中，事务要等Binlog复制到备节点后才能提交，人们自然而然就会想到通过半同步复制来构建RPO=0的数据一致性方案。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-binlog-4.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这个架构中需要两个备节点，半同步复制保证事务在收到任意一个备节点的应答后才能提交。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;如果主宕机，那么数据至少复制到了一个备上。&lt;/li&gt;
  &lt;li&gt;如果一个备节点不可用，也不会影响集群的可用性。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;要保证RPO=0，就要保证半同步不能退化成异步。MySQL的半同步复制有两个可退化异步的点：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;宕机重启后，已经写入Binlog的事务会自动提交，此时这些事务可能还没有复制到备库上。&lt;/li&gt;
  &lt;li&gt;当等待时间达到&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rpl_semi_sync_master_timeout&lt;/code&gt;设置的时间后，退化成异步。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;前者是没办法控制的，需要修改MySQL的代码来解决。后者则需要给&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rpl_semi_sync_master_timeout&lt;/code&gt;设置一个非常大的值，让半同步不能退化成异步。大事务无疑是RPO=0方案中非常棘手的问题。因为一旦有了大事务，就会导致整个集群不可写。因此在RPO=0的技术方案中，我们必须要采取一些措施。如果DBA对业务有比较高的约束力，则会采取措施在业务中规避大事务的出现。然而对于大公司而言，业务纷繁复杂很难做到全面消除大事务。RDS服务提供商，则对使用者完全没有约束力。在实际的环境中，可用性往往比数据一致性重要的多。因此许多方案中都会采用临时退化的策略，当有大事务时临时退化成异步的形式。&lt;/p&gt;

&lt;h2 id=&quot;大事务实时传输&quot;&gt;大事务实时传输&lt;/h2&gt;

&lt;p&gt;在AliSQL中，我们设计了一套实时传输的机制来解决大事务传输导致的问题，有了这套传输机制就&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;不需要&lt;/code&gt;将半同步退化为异步了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-binlog-5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;大事务实时传输机制&lt;/code&gt;在事务做DML期间，就将事务产生的Binlog Events从Binlog Cache的临时文件中读出，传送到备节点。关键的步骤包括：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;在DML执行期间，当事务产生的Binlog Events超过一定的量时，该事务会被注册到大事务列表中，当作大事务进行处理。&lt;/li&gt;
  &lt;li&gt;Binlog Dump线程会根据大事务列表中的信息，读取大事务的Binlog临时文件，将内容发送到备节点。大事务的Binlog Events和Binlog文件中的Binlog Events交替发送，并且对大事务的发送做了限流处理，优先发送Binlog文件中的内容，保证当前正在提交的事务不受影响。&lt;/li&gt;
  &lt;li&gt;大事务的Binlog Events发送时有特殊的标记和额外信息。当备节点的IO线程收到大事务的Events，将其存储到一个临时文件中，这里称作&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Relay Log Cache&lt;/code&gt;。&lt;/li&gt;
  &lt;li&gt;事务提交时，Dump线程发送完所有Binlog Events，然后将&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_even&lt;/code&gt;发送给备库。&lt;/li&gt;
  &lt;li&gt;备库收到&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_event&lt;/code&gt;后，知道已经接收到了事务的所有Binlog Events。然后将&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Relay Log Cache&lt;/code&gt;转成一个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Relay Log&lt;/code&gt;文件。&lt;/li&gt;
  &lt;li&gt;当多个大事务同时执行时，这套机制支持多个大事务同时进行实时传输。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;根据以上的步骤我们可以知道：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;大事务的Binlog Events是在产生时就一点一点的发送到了备库，当事务提交时只需要发送Gtid_event&lt;/code&gt;。因此提交阶段发送的数据量非常的小，就不会阻塞其他事务的Binlog Events传输。此外，也消除了突发了大量网络传输，减少了网络的拥塞。&lt;/p&gt;

&lt;h3 id=&quot;relay-log-cache&quot;&gt;Relay Log Cache&lt;/h3&gt;

&lt;p&gt;实时传输的机制和大事务的提交优化一脉相承，并且复用了大事务优化中的一些实现。事务的Binlog Events是在DML执行的过程中产生，逐渐累积。当事务产生的Binlog Events超过&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;binlog_cache_size&lt;/code&gt;时，这些Binlog Events会被写入一个临时文件中。在事务提交时这些Binlog Events一次性的写入到Binlog文件中。《MySQL大事务提交优化》中，大事务的临时文件会自动转成一个新的Binlog文件，因此大事务的提交导致的问题被消除。&lt;/p&gt;

&lt;p&gt;大事务实时传输复用了这个逻辑，在Relay Log Cache的头部保留一定的空间。当Relay Log Cache被转成Relay Log文件时，需要在头部填充Relay Log需要的一些特殊Binlog Events，比如Format_description_event等。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-binlog-6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;异常处理&quot;&gt;异常处理&lt;/h3&gt;

&lt;p&gt;大事务执行时间比较长，如果中间出了异常则需要做相应的处理。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;如果大事务在主库回滚了，Binlog dump线程会发送&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rollback&lt;/code&gt;给备库，IO线程收到&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rollback&lt;/code&gt;后，会将相应的Relay Log Cache销毁。&lt;/li&gt;
  &lt;li&gt;如果IO线程到主库的连接断开了，或者执行了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;STOP SLAVE&lt;/code&gt;的操作，IO线程则会销毁所有的Relay Log Cache. 当重新连接后，则会重新进行大事务的实时复制。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;优化效果&quot;&gt;优化效果&lt;/h2&gt;

&lt;p&gt;我们用sysbench oltp_write_only来模拟正常的写业务，然后在后台提交了一个产生了2GB Binlog Events的事务。测试结果如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-binlog-7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;采用了实时复制后，业务写入运行平稳，不再有业务跌0的情况出现。&lt;/p&gt;

&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;

&lt;p&gt;在MySQL半同步复制架构中，大事务是一个比较典型的问题。为了避免大事务造成实例的不稳定，人们不得不努力的去消除业务中的大事务，或者干脆让其退化为异步复制。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;大事务实时传输&lt;/code&gt;则将大事务的binlog Events的传输从提交阶段提前到了执行阶段，Binlog Events产生后立刻传输到备库。因此避免了在提交阶段长时间阻塞其他事务的Binlog Events传输，也避免了网络的拥塞。当遇到大事务时半同步复制不再需要退化为异步复制，为实现基于半同步的RPO=0方案解决了一个棘手问题。&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title type="html">MySQL 大事务提交优化</title>
    <link href="https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization/" rel="alternate" type="text/html" title="MySQL 大事务提交优化"/>
    <published>2026-07-15T10:00:00+08:00</published>
    <updated>2026-07-15T10:00:00+08:00</updated>
    <id>https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization/</id>
    <author>
      <name>宋利兵 (Libing Song)</name>
    </author>
    <category term="MySQL"/>
    <category term="Binlog"/>
    <category term="大事务"/>
    <category term="提交优化"/>
    <summary type="html">本文也有英文版：English version。 在使用和运维MySQL的过程中你一定碰到过下面这种奇怪的慢SQL。 平时执行很快的INSERT语句，竟然执行了1.3s，并且慢SQL记录里也没有看到长时间的锁等待。 多语句事务的所有语句都已经执行完了，但是COMMIT语句竟然执行了1.3s。 当这种情况出现时，最有可能的就是有大事务在提交。以下是一个模拟测试的结果，我们用Sysbench来模拟正常的业务，然后在后台每5秒执行一个大的UPDATE，可以看到大的UPDATE会严重的影响业务的性能。 根因分析 上图展示了两个事务的执行过程： 事务执行分为两个阶段，执行阶段和提交阶段。 事务执行阶段，当语句更新数据时，会生成Binlog Events。这些Binlog events会被存储到Binlog Cache中，Binlog Cache有两部分，一部分是内存的Buffer，一部分是一个临时文件，当Buffer 写满后，就会将Binlog Events 写入到临时文件中。 当事务提交时，会将Binlog Cache 中的Binlog Events 全部拷贝到Binlog文件中。 将Binlog Events写入Binlog文件的过程必须要串行执行，只有一个事务写完了，另外一个事务才能执行。因此当Trx_n在写Binlog文件时，Trx_m就必须等待。 图中Trx_n是一个大事务，产生了大量的Binlog Events。拷贝Binlog Events到Binlog 文件所需要的时间是和事务产生的Binlog Events大小线性相关的，Binlog Events越大，拷贝的时间就越长。 Trx_m是一个小事务，虽然执行阶段很快就完成了，但是在提交时，遇到了大事务Trx_n在提交，因此必须要等待Trx_n拷贝完Binlog Events才能继续。Trx_m在提交阶段花了大量的时间在等待Trx_n写Binlog文件，这就是小事务变慢的原因。 问题的严重性 从前面我们的模拟测试我们可以看到，大事务的提交对业务的稳定性是有非常大的影响的。实际的使用场景中可能要严重的多，并且也很普遍。 GB量级的事务会造成实例长时间的不可写。由于存储的IO带宽是一定的，大事务写binlog的时间就取决于事务的大小。在运维的过程中，我们见到的最大的事务产生了104GB的Binlog Events。 GB量级的事务会造成IO吞吐上升变慢,甚至IO打满，这也会导致查询变慢。 几百MB的事务虽然不会造成长时间的影响，但仍有会让业务DML慢上几百毫秒。这种程度的慢，对于延迟敏感型的业务来说，可能也是不能接受的。 除此之外，以上的几种情况都可能引起活跃连接数的上升。如果活跃连接不能及时消化，导致CPU打高，可能造成恶性循环，最终雪崩，产生更大的问题。 大事务写Binlog优化 我们在AliSQL上对大事务写Binlog的过程做了优化，彻底消除了大事务提交对稳定性的影响。RDS-5.7，RDS-8.0都已经默认的开启了这个优化。去年我们将AliSQL的这个优化捐赠给了MariaDB，这个功能已经在MariaDB-11.7[1]上发布。 优化方案 下面我来介绍一下MariaDB-11.7上的实现方案，MySQL和MariaDB代码上虽然差别已经比较大了，但是根本的逻辑还是一样的，所以实现方案也是一样的。 这个优化方案说起来是比较简单和清晰的：既然Binlog Cache已经将Binlog Events写到了文件里，那我们就直接把这个文件直接转成(rename)一个Binlog文件。这样就不需要拷贝Binlog Events，没有额外的IO产生。并且Rename操作的时间是恒定的和Binlog Cache的大小无关，可以彻底解决大事务造成的问题。下面我们来看一下实现逻辑。 #binlog_cache_files 目录 Binlog Cache里的文件是一个系统临时文件，不能直接转成一个普通文件。因此我们在binlog所在的目录创建了一个目录#binlog_cache_files，Binlog Cache创建的文件从系统临时文件变成了普通的文件，放在这个目录里。 1 2 $ls var/mysqld.1/data/#binlog_cache_files      ...</summary>
    <content type="html" xml:base="https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization/">&lt;blockquote class=&quot;prompt-tip&quot;&gt;
  &lt;p&gt;本文也有英文版：&lt;a href=&quot;https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization-en/&quot;&gt;English version&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;在使用和运维MySQL的过程中你一定碰到过下面这种奇怪的慢SQL。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-commit-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;平时执行很快的INSERT语句，竟然执行了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.3s&lt;/code&gt;，并且慢SQL记录里也没有看到长时间的锁等待。&lt;/li&gt;
  &lt;li&gt;多语句事务的所有语句都已经执行完了，但是COMMIT语句竟然执行了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.3s&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;当这种情况出现时，最有可能的就是有大事务在提交。以下是一个模拟测试的结果，我们用Sysbench来模拟正常的业务，然后在后台每5秒执行一个大的UPDATE，可以看到大的UPDATE会严重的影响业务的性能。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-commit-2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;根因分析&quot;&gt;根因分析&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-commit-3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;上图展示了两个事务的执行过程：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;事务执行分为两个阶段，执行阶段和提交阶段。&lt;/li&gt;
  &lt;li&gt;事务执行阶段，当语句更新数据时，会生成Binlog Events。这些Binlog events会被存储到Binlog Cache中，Binlog Cache有两部分，一部分是内存的Buffer，一部分是一个临时文件，当Buffer 写满后，就会将Binlog Events 写入到临时文件中。&lt;/li&gt;
  &lt;li&gt;当事务提交时，会将Binlog Cache 中的Binlog Events 全部拷贝到Binlog文件中。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;将Binlog Events写入Binlog文件的过程必须要串行执行，只有一个事务写完了，另外一个事务才能执行。&lt;/code&gt;因此当&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_n&lt;/code&gt;在写Binlog文件时，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_m&lt;/code&gt;就必须等待。&lt;/li&gt;
  &lt;li&gt;图中&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_n&lt;/code&gt;是一个大事务，产生了大量的Binlog Events。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;拷贝Binlog Events到Binlog 文件所需要的时间是和事务产生的Binlog Events大小线性相关的，Binlog Events越大，拷贝的时间就越长。&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_m&lt;/code&gt;是一个小事务，虽然执行阶段很快就完成了，但是在提交时，遇到了大事务&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_n&lt;/code&gt;在提交，因此必须要等待&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_n&lt;/code&gt;拷贝完Binlog Events才能继续。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_m&lt;/code&gt;在提交阶段花了大量的时间在等待&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_n&lt;/code&gt;写Binlog文件，这就是小事务变慢的原因。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;问题的严重性&quot;&gt;问题的严重性&lt;/h2&gt;

&lt;p&gt;从前面我们的模拟测试我们可以看到，大事务的提交对业务的稳定性是有非常大的影响的。实际的使用场景中可能要严重的多，并且也很普遍。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;GB量级的事务会造成实例长时间的不可写。由于存储的IO带宽是一定的，大事务写binlog的时间就取决于事务的大小。在运维的过程中，我们见到的最大的事务产生了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;104GB&lt;/code&gt;的Binlog Events。&lt;/li&gt;
  &lt;li&gt;GB量级的事务会造成IO吞吐上升变慢,甚至IO打满，这也会导致查询变慢。&lt;/li&gt;
  &lt;li&gt;几百MB的事务虽然不会造成长时间的影响，但仍有会让业务DML慢上几百毫秒。这种程度的慢，对于延迟敏感型的业务来说，可能也是不能接受的。&lt;/li&gt;
  &lt;li&gt;除此之外，以上的几种情况都可能引起活跃连接数的上升。如果活跃连接不能及时消化，导致CPU打高，可能造成恶性循环，最终雪崩，产生更大的问题。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;大事务写binlog优化&quot;&gt;大事务写Binlog优化&lt;/h2&gt;

&lt;p&gt;我们在AliSQL上对大事务写Binlog的过程做了优化，彻底消除了大事务提交对稳定性的影响。RDS-5.7，RDS-8.0都已经默认的开启了这个优化。去年我们将AliSQL的这个优化捐赠给了MariaDB，这个功能已经在MariaDB-11.7[1]上发布。&lt;/p&gt;

&lt;h3 id=&quot;优化方案&quot;&gt;优化方案&lt;/h3&gt;

&lt;p&gt;下面我来介绍一下MariaDB-11.7上的实现方案，MySQL和MariaDB代码上虽然差别已经比较大了，但是根本的逻辑还是一样的，所以实现方案也是一样的。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-commit-4.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这个优化方案说起来是比较简单和清晰的：既然Binlog Cache已经将Binlog Events写到了文件里，那我们就直接把这个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;文件直接转成(rename)一个Binlog文件&lt;/code&gt;。这样就不需要拷贝Binlog Events，没有额外的IO产生。并且Rename操作的时间是恒定的和Binlog Cache的大小无关，可以彻底解决大事务造成的问题。下面我们来看一下实现逻辑。&lt;/p&gt;

&lt;h3 id=&quot;binlog_cache_files-目录&quot;&gt;#binlog_cache_files 目录&lt;/h3&gt;

&lt;p&gt;Binlog Cache里的文件是一个系统临时文件，不能直接转成一个普通文件。因此我们在binlog所在的目录创建了一个目录&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#binlog_cache_files&lt;/code&gt;，Binlog Cache创建的文件从系统临时文件变成了普通的文件，放在这个目录里。&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nv&quot;&gt;$ls&lt;/span&gt; var/mysqld.1/data/#binlog_cache_files                                
ML_140413554102520
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;头部保留空间&quot;&gt;头部保留空间&lt;/h3&gt;

&lt;p&gt;Binlog cache的文件里只包含事务的Binlog Events，如果要转成Binlog文件，则需要保留一定的空间用来写Binlog文件头的Binlog Events，比如Format_description_event等。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-commit-5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;预留的空间是按4KB对齐的，因此至少会预留4KB。对于大多数情况来说，4KB的空间已经足够。但是在一些场景下Gtid_list_log_event(类似于MySQL的Previous_gtid_event用来记录这个Binlog之前生成的Gtid集合)可能会非常大。为了避免在这种情况下这个功能无法使用，在生成新的Binlog文件时会根据Binlog文件头部Events实际占用的空间大小来调整保留的空间。当下一个事务开始时，会调整Binlog Cache文件的保留空间。Binlog头部的Binlog Events通常占用不到4KB的空间，因此在写完头部的Binlog Events后，可能会剩余一些空间。如何处理这些剩余空间呢？得益于MariaDB Gtid_log_event可以在末尾填充0的机制，这些剩余空间会被填充到对应的Gtid_log_event中。在将Binlog Cache文件转成Binlog文件后，其结构如下所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-commit-6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;rename-过程&quot;&gt;Rename 过程&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-commit-7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Rename 的主要过程如下：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;持久化Binlog Cache文件，此时还没有进入Rename过程，不会阻塞其他事务提交。&lt;/li&gt;
  &lt;li&gt;执行Rotate的过程，关闭原来的Binlog文件，产生一个新的Binlog文件。&lt;/li&gt;
  &lt;li&gt;将新文件的Header的内容拷贝到Binlog Cache头部。&lt;/li&gt;
  &lt;li&gt;生成Gtid_log_event.&lt;/li&gt;
  &lt;li&gt;删除生成的Binlog文件，将Binlog Cache文件Rename成新的Binlog文件。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;优化效果&quot;&gt;优化效果&lt;/h2&gt;

&lt;p&gt;我们仍然用sysbench模拟业务执行，然后在后台每5秒执行一个大的UPDATE，这个UPDATE会产生512MB的Binlog Events。测试结果如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-commit-8.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在有大事务提交优化的情况下, sysbench的TPS已经比较平稳，不会出现剧烈的抖动。看起来每5秒仍然会有一个小幅的抖动，这个抖动是因为执行大的UPDATE本身要占用一定的CPU，而不是事务提交导致的。&lt;/p&gt;

&lt;p&gt;此外我们也模拟了不同大小的事务，对业务SQL造成的延迟情况。结果如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://songlibing.github.io/assets/img/bigtxn-commit-9.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;在没有大事务提交优化的情况下，当大事务超过64MB后sysbench的最大延迟开始明显增加，并且随着事务变大，迅速增加。&lt;/li&gt;
  &lt;li&gt;当开启了大事务提交优化后, 无论事务多大sysbench的最大延迟始终保持稳定，保持正常业务延迟水平。当事务达到1024GB时，因为多了一次Binlog Rotate，所以我们看到延迟略有增加。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;

&lt;p&gt;在MySQL Binlog复制架构中，大事务是一个比较典型的问题导火索，会导致稳定性、复制延迟等问题。通过将Binlog Cache的临时文件直接转成Binlog文件的方法，可以避免对于Binlog Events的拷贝，消除额外的IO，让大事务的提交始终保持高效和稳定。因此彻底解决了大事务提交导致的各种稳定性问题。&lt;/p&gt;

&lt;h4 id=&quot;引用链接&quot;&gt;引用链接&lt;/h4&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[1]&lt;/code&gt; MariaDB-11.7: &lt;em&gt;https://mariadb.com/resources/blog/binlog-commit-optimization-for-large-transaction/&lt;/em&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title type="html">MySQL DROP TABLE 的性能问题与优化</title>
    <link href="https://songlibing.github.io/posts/mysql-drop-table-optimization/" rel="alternate" type="text/html" title="MySQL DROP TABLE 的性能问题与优化"/>
    <published>2026-04-10T14:56:21+08:00</published>
    <updated>2026-07-17T17:28:50+08:00</updated>
    <id>https://songlibing.github.io/posts/mysql-drop-table-optimization/</id>
    <author>
      <name>宋利兵 (Libing Song)</name>
    </author>
    <category term="MySQL"/>
    <category term="InnoDB"/>
    <category term="DROP TABLE"/>
    <category term="性能优化"/>
    <summary type="html">以下信息是在测试一个 SQL 时，AliSQL 的 Performance Agent 功能采集的 buffer pool 相关的监控信息。你能从这些监控信息看出是什么样的 SQL 在执行吗？ 图中：bp = buffer pool, pg = page。 bp_pg_total 是 buffer pool 的总 page 数，一般不会变化的，除非修改了 buffer pool 的大小。 bp_pg_data 指的是 buffer pool 中数据文件的 page 的数量，包括 undo page。 bp_pg_dirty 指的是数据页中的脏页(被修改了，但还没有写入数据文件的页)的数量。 bp_pg_free 是 buffer pool 中空闲的 page 数量，这部分是没有被使用的部分。 bp_wait_free 当从存储上 读一个 page 到 buffer pool 时，如果没有空闲的 page 也没有净页(没有被修改的) 可释放，则需要等待一个脏页被刷盘后释放。bp_wait_free 记录的是每秒等待的次数。 bp_reads...</summary>
    <content type="html" xml:base="https://songlibing.github.io/posts/mysql-drop-table-optimization/">&lt;p&gt;以下信息是在测试一个 SQL 时，AliSQL 的 Performance Agent 功能采集的 buffer pool 相关的监控信息。你能从这些监控信息看出是什么样的 SQL 在执行吗？&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://mmbiz.qpic.cn/sz_mmbiz_png/ibf4J5w9SFz6sT26TSXzbgQTWbziaOmDGpQxiaEotib6cEUKicUvOUZ0w1HvicBuSzH8totUzoQf6icwc6VPLaR5rA5ZQ/640?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;图中：bp = buffer pool, pg = page。&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;bp_pg_total 是 buffer pool 的总 page 数，一般不会变化的，除非修改了 buffer pool 的大小。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;bp_pg_data 指的是 buffer pool 中数据文件的 page 的数量，包括 undo page。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;bp_pg_dirty 指的是数据页中的脏页(被修改了，但还没有写入数据文件的页)的数量。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;bp_pg_free 是 buffer pool 中空闲的 page 数量，这部分是没有被使用的部分。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;bp_wait_free 当从存储上 读一个 page 到 buffer pool 时，如果没有空闲的 page 也没有净页(没有被修改的) 可释放，则需要等待一个脏页被刷盘后释放。bp_wait_free 记录的是每秒等待的次数。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;bp_reads 是每秒从存储上读取的 page 的数量。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;bp_read_req 是每秒读取的 page 总数量，包括从 buffer pool 和磁盘读取的 page 数。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;以上的信息取自于 InnoDB 提供的 buffer pool 状态变量（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Status&lt;/code&gt;），Performance Agent 每 1 秒采集一次，将这些信息存储到了文件中。&lt;img src=&quot;https://mmbiz.qpic.cn/sz_mmbiz_png/ibf4J5w9SFz4QXmTF6QtNyqiamvDMyesOibEbDU6Pvh3rxHZX51PPWVIOuic4TUVzBEeoKUc7cLicjyHpDYiaPIfcXvg/0?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;现象分析&quot;&gt;现象分析&lt;/h2&gt;
&lt;h3 id=&quot;bp_reads-和-bp_read_req&quot;&gt;bp_reads 和 bp_read_req&lt;/h3&gt;
&lt;p&gt;上面的监控信息可以看到 bp_reads 非常的少，这说明被访问的数据页几乎都在 buffer pool 中，因此不需要从存储上读取。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;当需要判断问题是否是由 IO 引起的时候，这两个指标非常重要&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&quot;bp_pg_free&quot;&gt;bp_pg_free&lt;/h3&gt;

&lt;p&gt;刚开始的时候 bp_pg_free 的数量是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1024&lt;/code&gt;，这个值是由参数 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;innodb_lru_scan_depth&lt;/code&gt; 控制的，默认是 1024。当 free page 保持在这个值附近时，说明 buffer pool 的空间已经用完了。为了避免因没有 free page 而导致的等待，后台的 page cleaner 线程会扫描 LRU 链表最后的 1024 个 page，如果能释放则释放掉这些 page。&lt;/p&gt;

&lt;p&gt;当一个正在执行的 SQL 需要从磁盘读一个 page 时，首先会尝试从 free list 获取一个空闲页。如果此时没有空闲页，当前线程则会扫描 LRU 链表去释放一个净页，如果没有找到合适的净页，则会尝试释放一个脏页。释放脏页就要刷脏，时间会比较长，会导致 SQL 变慢，活跃连接增加，严重时实例可用性受损。下图是这种情形下的一个函数调用栈，可以看到实例超过&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;59%&lt;/code&gt;的cpu时间都是在获取一个free page(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;buf_LRU_get_free_block&lt;/code&gt;)，其中&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;45%&lt;/code&gt;都是因为没有free page或者clean page而不得通过刷脏来获取一个free page(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;buf_flush_single_page_from_LRU&lt;/code&gt;)。&lt;img src=&quot;https://mmbiz.qpic.cn/sz_mmbiz_png/ibf4J5w9SFz4QXmTF6QtNyqiamvDMyesOibznO60CaxtZZWyk4iaFia5T44MJZDtXF1F6IynsiaWsK3ffmSj62vlticEA/0?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;对于规格比较大的实例，可以将 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;innodb_lru_scan_depth&lt;/code&gt; 调大来提高实例的稳定性。注意这个参数控制的是单个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;buffer pool instance&lt;/code&gt; 的空闲页数量，如果实例设置了 8 个或者更多的 buffer pool instance，则空闲页的总数量是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8 * innodb_lru_scan_depth&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&quot;bp_pg_free--bp_pg_data--bp_pg_total&quot;&gt;bp_pg_free + bp_pg_data &amp;lt; bp_pg_total&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;00:18:46&lt;/code&gt;时 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;空闲页&lt;/code&gt; 与 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;数据页&lt;/code&gt; 之和比 buffer pool 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;总页数&lt;/code&gt; 少了大约 9 万个页。&lt;/li&gt;
  &lt;li&gt;从&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;00:18:46&lt;/code&gt;到&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;00:21:15&lt;/code&gt;这段时间，空闲页的数量持续的增加，但是数据页却没有减少。&lt;/li&gt;
  &lt;li&gt;直到&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;00:21:15&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;空闲页&lt;/code&gt; 与 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;数据页&lt;/code&gt; 之和才接近了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;总页数&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;那9万多个页那里去了呢？除了数据页之外，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;行锁和自适应哈希索引（AHI）也会占用 buffer pool 的内存&lt;/code&gt;。AHI 占据的内存比例通常不会特别大，因为只有已经加载到 buffer pool 中的页上的数据可能会产生 AHI 记录。当一个页被淘汰时，它的 AHI 记录也会被清理掉。而行锁是在事务提交时才会释放和页的淘汰无关。比如全表更新这种操作，很可能产生大量的行锁记录。InnoDB 限制这些额外占用的 buffer pool 内存不能超过 buffer pool 总大小的 75%（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;buf_LRU_buf_pool_running_out()&lt;/code&gt;）。如果超过了，在申请行锁时则会报 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ERR_LOCK_TABLE_FULL&lt;/code&gt; 错误。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;The total number of locks exceeds the lock table size
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;从上面描述的现象看，很像是在持续的释放 AHI 记录或者是行锁占用的 page。&lt;/p&gt;

&lt;h3 id=&quot;数据页突然大量减少&quot;&gt;数据页突然大量减少&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;00:21:16&lt;/code&gt; 这一秒减少了大约 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;62 万&lt;/code&gt;个数据页，这些数据页都变成了空闲页。短时间内释放这么多的数据页，通常和 tablespace 的删除、重建相关，因为这些操作会把当前 tablespace 的所有页从 buffer pool 中清除掉。因此可能是以下几种操作：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;删除表、表分区的操作&lt;/li&gt;
  &lt;li&gt;清空表、表分区的操作&lt;/li&gt;
  &lt;li&gt;重建表的操作，包括OPTIMIZE TABLE和许多Inplace/Copy DDL&lt;/li&gt;
  &lt;li&gt;Undo空间的缩容和删除&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Undo空间的缩容和删除不会有 AHI 或者行锁的释放行为，重建表前期往往有大量的IO操作，所以&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;最可能的还是删除或者清空表或者表分区&lt;/code&gt;。如果是这两个操作之一的话，那么这个实例应该是个 MySQL 5.7 或者 MySQL 8.0.23 以前的实例。&lt;/p&gt;

&lt;p&gt;MySQL 8.0.23 实现了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;快速清空、删除表空间&lt;/code&gt;的功能。简单地说就是给 tablespace 添加了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;版本信息&lt;/code&gt;，每个 page 都记录有当前的 tablespace 版本。Drop/truncate table 时，只是将 tablespace 的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;版本加 1&lt;/code&gt;，不会清除这个 tablespace 的数据页。这些遗留下来的页称为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stale page&lt;/code&gt;，当从 buffer pool 获取一个页时，就要检测当前页是否是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stale page&lt;/code&gt;。如果是就直接放到free list中。详细信息可以参考 &lt;a href=&quot;https://dev.mysql.com/worklog/task/?id=14100&quot;&gt;WL#14100: InnoDB: Faster truncate/drop table space&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&quot;drop-table-带来的问题&quot;&gt;DROP TABLE 带来的问题&lt;/h2&gt;
&lt;p&gt;以上的监控信息是一个 RDS MySQL 5.7 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DROP TABLE&lt;/code&gt; 语句产生的，这个语句执行了大约 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;150&lt;/code&gt; 秒。它导致了主实例长时间的不可写，因此发生了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;主备切换&lt;/code&gt;。这里最大疑问是&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;为什么&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;单个表&lt;/code&gt;的 DROP TABLE 语句会导致&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;所有的表&lt;/code&gt;都不可写？&lt;/li&gt;
  &lt;li&gt;为什么会持续这么长的时间？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;导致 HA 切换的原因如下：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;这个表上有大量的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AHI 记录&lt;/code&gt;，DROP TABLE 时需要清理这些 AHI 的记录，花费了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;149&lt;/code&gt; 秒的时间。&lt;/li&gt;
  &lt;li&gt;DROP TABLE 清理 AHI 记录时持有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict_sys::mutex&lt;/code&gt;。dict_sys::mutex 是内部字典系统的全局锁，因此所有访问字典系统的行为都会被阻塞。&lt;/li&gt;
  &lt;li&gt;DML 操作时，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before_dml hook&lt;/code&gt; 中需要获取当前表的外键信息，这个操作需要访问字典系统，因此被 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DROP TABLE&lt;/code&gt; 阻塞。HA 写操作探测失败，触发了主备切换。DML当时的函数栈如下图所示&lt;img src=&quot;https://mmbiz.qpic.cn/sz_mmbiz_png/ibf4J5w9SFz4AGQ1X31Waz40pkkbH5CDuWvKgicUFyErmKEkNjRchibnFRiaGetx1JwSFmJnCQGyU4LeLd3mxOKY9g/0?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;before_dml-hook-优化&quot;&gt;before_dml hook 优化&lt;/h2&gt;
&lt;p&gt;事务的钩子系统是binlog复制为了实现对事务的控制设计的，一共有5个钩子：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;before_dml&lt;/li&gt;
  &lt;li&gt;before_commit&lt;/li&gt;
  &lt;li&gt;before_rollback&lt;/li&gt;
  &lt;li&gt;after_commit&lt;/li&gt;
  &lt;li&gt;after_rollback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plugin 如果需要在相应的阶段做一些事情，需要实现一个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;事务观察者(Trans_observer)&lt;/code&gt;，将其注册到钩子系统里。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;semi-sync&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;group replication&lt;/code&gt; 都会注册一个事务观察者，当安装了 semi-sync plugin 或者 group replication plugin 后，before_dml hook 就会被执行，plugin 内部会根据开启的状态进行不同的处理。before_dml hook 在调用plugin相应函数前，需要初始化一些参数的信息，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;外键的信息&lt;/code&gt;也是在调用 plugin 相应函数前获取的。所以哪怕semi-sync 或者 group replication &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;没有开启&lt;/code&gt;，也会触发上面例子里的问题。如果要避免这个问题，就必须要&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;卸载(UNINSTALL PLUGIN)&lt;/code&gt;了 semi-sync plugin.&lt;/p&gt;

&lt;p&gt;before_dml hook 目前仅在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Group Replication&lt;/code&gt; 中有用，Group Replication 不支持 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CASCADE&lt;/code&gt; 操作，会在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before_dml hook&lt;/code&gt; 中做外键级联操作的检查。这个操作的效率是非常低，没有开启 Group Replication 的情况下，是没有必要做检查的，因此&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AliSQL-5.7上去掉了这个hook&lt;/code&gt;。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MySQL-8.0&lt;/code&gt; 上由于采用了新的 Data Dictionary 机制，这个 hook 里已经不需要持有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict_sys::mutex&lt;/code&gt; ，所以不会被阻塞。&lt;/p&gt;

&lt;h2 id=&quot;dict_sysmutex&quot;&gt;dict_sys::mutex&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict_sys::mutex&lt;/code&gt; 是用来保护字典系统的。字典是InnoDB的内部表和内存中的缓存结构，用来存储表的元信息。比如表结构信息、索引信息、外键约束信息等等。读取或者修改字典表和缓存结构的内容需要持有dict_sys::mutex，因此字典是完全串行的访问模式，访问效率非常低。&lt;/p&gt;

&lt;p&gt;字典的访问还有以下几种情况：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;访问的表不在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Table Cache&lt;/code&gt; 中，需要创建新的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TABLE&lt;/code&gt; 对象。&lt;/li&gt;
  &lt;li&gt;DML操作的表有外键时，做外键检查时需要持有 dict_sys::mutex;&lt;/li&gt;
  &lt;li&gt;DDL 修改表的元数据信息。&lt;/li&gt;
  &lt;li&gt;通过 information_schema 获取表的元数据信息。&lt;/li&gt;
  &lt;li&gt;无主键表上会有一个 InnoDB 内建的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;row_id&lt;/code&gt; 字段，获取全局 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;row_id&lt;/code&gt; 时也需要持有 dict_sys::mutex。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以需要持有 dict_sys::mutex 的情形是非常多的，对于 DROP TABLE 过程还是要做更多的优化，尽量减少持有 dict_sys::mutex 带来的全局影响。MySQL-5.7上 DROP TABLE 在持有 dict_sys::mutex 时主要做的事情包括:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;获取 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict_sys::mutex&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;删除 Dictionary 里的元数据信息。&lt;/li&gt;
  &lt;li&gt;删除所有索引（包括聚簇索引）。&lt;/li&gt;
  &lt;li&gt;删除 ibd 文件&lt;/li&gt;
  &lt;li&gt;释放 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict_sys::mutex&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;adaptive-hash-index-清理优化&quot;&gt;Adaptive Hash Index 清理优化&lt;/h2&gt;
&lt;p&gt;从上面的原因来看，关闭 InnoDB 的 Adaptive Hash Index 就应该能解决这个问题了吧？确实对于上面的案例来说，关闭 AHI 后就能够解决问题。InnoDB 中的 AHI 功能本身的实现是不够好的，关于 AHI 导致的稳定性的 bug 有一大堆，因此&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;关闭 AHI 也是强烈推荐的做法&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;但是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;关闭 AHI 就万事大吉了吗？&lt;/code&gt; 上面测试中表的大小是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10GB&lt;/code&gt;，如果你拿一个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1TB&lt;/code&gt;的表去做&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DROP TABLE&lt;/code&gt;操作，你会发现即使关闭了AHI，它仍然会阻塞所有的 DML 很长的时间。通过下面的 CPU 热点图，我们可以看到大量的时间仍然耗费在了清理 AHI 相关的函数上(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;btr_search_drop_page_hash_when_freed()&lt;/code&gt;)。&lt;img src=&quot;https://mmbiz.qpic.cn/sz_mmbiz_png/ibf4J5w9SFz7FZjEndCKDMKHgDUQt2dVB9fc16bW8mWY26JLCicYcYQmian5RzQBCTRfkulOu3UFrbR3DrzhIzGeQ/0?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;清理 AHI 记录是融合在删除索引的过程中的。删除索引时会读取 Index B+Tree 上的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;File Segment&lt;/code&gt; 中记录的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Extent&lt;/code&gt; 信息，逐个Extent 释放。为了释放 AHI 的记录，会检查 Extent 中的每一个 Page 是否有 AHI 的记录， 如果有就释放。具体执行分为三步：&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;检查 page 是否在 buffer pool 中。&lt;/li&gt;
  &lt;li&gt;如果 page 在 buffer pool 中则检查 page block 的 index 是否为 NULL。为 NULL 则表示这个 page 上没有 AHI 记录。&lt;/li&gt;
  &lt;li&gt;如果 page 上有 AHI 记录，则读取每一行记录计算 hash 检查是否有对应的 AHI 记录，如果有则删除 AHI 上的记录。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;第3步非常耗时，上面例子中 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10GB&lt;/code&gt; 的表清空时，绝大部分的时间消耗在第3步。虽然关闭了 AHI 后不会有第3步的执行，但是第1步、第2步的检查还在。当表非常大时由于检测的数据页数量非常的庞大，在第1步上花费的时间就会变长。上面的CPU热点图里我们可以看到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;buf_page_get_gen&lt;/code&gt;  这个函数及下层函数总共花费了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;46.24%&lt;/code&gt;的 CPU 时间，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;buf_page_get_gen&lt;/code&gt; 本身则占了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;41.24%&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;AliSQL上对这个问题做了优化，逻辑很简单：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;将清理 AHI 的工作放到删除表之前，这一步不需要&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict_sys::mutex&lt;/code&gt;的保护。&lt;/li&gt;
  &lt;li&gt;在删除索引时，会判断这个索引上是否有 AHI 的记录，如果没有则跳过每个 page 的检查。由于第一步已经提前清理了表上 AHI 记录(或者 AHI 是关闭的)，这一步会跳过每个 page 的检查。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;删除-ibd-文件优化&quot;&gt;删除 IBD 文件优化&lt;/h2&gt;
&lt;p&gt;在 DROP TABLE 的过程，实际删除 IBD 文件的过程被&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict_sys::mutex&lt;/code&gt;保护。删除一个文件花费的时间和文件的大小成正比，比如删除一个100GB的文件，我的测试环境里大约需要&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;20&lt;/code&gt;秒。&lt;/p&gt;

&lt;p&gt;在 AliSQL 中我们实现一套异步删除大文件的机制：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;删除一个超过一定大小的 IBD 文件时，这个文件不会立刻删除，而是被 rename 成一个临时文件。&lt;/li&gt;
  &lt;li&gt;一个后台线程以 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;512MB&lt;/code&gt; 为单位，逐步的 truncate 文件，直到文件长度为 0，才删除文件。这样可以避免瞬时大量的 os 内核 inode 操作，让删除更平滑。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;清理-page-优化&quot;&gt;清理 Page 优化&lt;/h2&gt;
&lt;p&gt;在真正的删除 table space 文件之前，MySQL-5.7 需要将这个 table space 的所有 page 从 buffer pool 中清理掉。由于 MySQL buffer pool 中没有单个表的 page 链表，所以 MySQL-5.7 采用了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LRU Scan&lt;/code&gt; 的方法，就是扫描 buffer pool 中的所有数据页，将扫到的属于当前 table space 的 page 全部释放掉。在 LRU scan 的过程中需要持有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;buf_pool::mutex&lt;/code&gt;, 其他的 buffer pool 的读写都会被阻塞。&lt;/p&gt;

&lt;p&gt;这个设计最坑的地方在于，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;删除任意大小的表都需要执行 LRU scan&lt;/code&gt;。这对&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;大内存&lt;/code&gt;的实例非常的不友好，对一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;128GB&lt;/code&gt; buffer pool 的实例来说，大约有&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;800万&lt;/code&gt;个 page, 即便分为 8 个 buffer pool instance，一次的 LRU scan 也要扫描 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;100万&lt;/code&gt; 个 page。在运维中大家对大表的处理一般都会比较谨慎。对于非常小的表可能会忽视了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LRU scan&lt;/code&gt; 产生的影响。如果在繁忙的系统上做了小表的删除、清理操作，就会导致实例不稳定。&lt;img src=&quot;https://mmbiz.qpic.cn/sz_mmbiz_png/ibf4J5w9SFz6lkVcFM5Z3v2jDQGKY2zCSicywuWriaotOdwv29lIyxmYAZlldu7jj7Q9QiaCgFia5HxnQBQ63aKkG4w/0?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;除了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LRU scan&lt;/code&gt;， DDL 也常常需要做 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Flush List Scan&lt;/code&gt;，扫描脏页链表，把DDL产生的脏页都刷盘。这个扫描也需要 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;buf_pool::mutex&lt;/code&gt; 的保护，其他的 buffer pool 的读写都会被阻塞。对于一个写繁忙的实例来说，flush list 会更长，从而 flush list scan 持锁的时间就会变长。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;更要命的当碰到一个需要刷盘的 page 时，flush list scan 就会被中断。当刷完这个 page 后，需要从头再来一遍。直到最后一遍，一个需要刷盘的 page 都没有发现才结束&lt;/code&gt;。详情可以参考&lt;a href=&quot;https://bugs.mysql.com/bug.php?id=102289&quot;&gt;BUG#102289&lt;/a&gt; 。MySQL-8.0 对 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;flush list scan&lt;/code&gt; 做了个优化，就是提前 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pin&lt;/code&gt; 住下一个要检查的 page，然后才去刷当前页。当刷完这个页后，可以接着从被 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pin&lt;/code&gt;住的页继续处理。这样只需要执行一次完整的 flush scan 就可以了。如果下一个页正在做 IO 操作, pin 就会失败，这时还是要从头再来一遍。&lt;/p&gt;

&lt;p&gt;AliSQL 中设计了基于 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;space_id&lt;/code&gt; 分区的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page list&lt;/code&gt; 和 ` flush list &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;，总共分了 &lt;/code&gt;2048&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt; 个分区。当要清除数据页或刷脏时，只需要扫描一个分区就可以了，不再需要进行 &lt;/code&gt;LRU scan&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt; 或 &lt;/code&gt;flush list scan`。&lt;/p&gt;

&lt;p&gt;以上的几个优化同样也适用于&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;重建表&lt;/code&gt;的场景。OPTIMIZE TABLE, 以及许多的 Online DDL, Copy DDL 内部其实都是重建了一个新表，把原来的表删除掉。&lt;/p&gt;

&lt;h2 id=&quot;truncate-table-的问题&quot;&gt;TRUNCATE TABLE 的问题&lt;/h2&gt;
&lt;p&gt;MySQL-5.7 上 TRUNCATE TABLE 主要的执行过程如下:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;获取 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict_sys::mutex&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;删除所有索引（包括聚簇索引）。&lt;/li&gt;
  &lt;li&gt;重建所有索引&lt;/li&gt;
  &lt;li&gt;释放 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict_sys::mutext&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;获取 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fil_sys::mutex&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Truncate ibd 文件&lt;/li&gt;
  &lt;li&gt;释放 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fil_sys::mutex&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TRUNCATE TABLE 也会删除所有的索引，因此删除 AHI 记录的优化对 TRUNCATE TABLE 同样有效。但是 TRUNCATE TABLE 不会删除 IBD 文件，只是将 IBD 多余的空闲空间给 truncate 掉，因此无法进行异步的删除。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DROP TABLE&lt;/code&gt; 时 table space 会被删除，因此只有释放 table space 内存结构时持有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fil_sys::mutex&lt;/code&gt;，删除文件时不需要持有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fil_sys::mutex&lt;/code&gt;，所以持锁时间很短。但是清空一个大表时 truncate 文件过程中会持有&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fil_sys::mutex&lt;/code&gt;。Truncate 大文件时花费的时间很长，这段时间内&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;所有的 DML 会被阻塞&lt;/code&gt;。DML被阻塞时的堆栈如下图所示：&lt;img src=&quot;https://mmbiz.qpic.cn/sz_mmbiz_png/ibf4J5w9SFz7FZjEndCKDMKHgDUQt2dVB9wZazbiaNk3qBT1jsW6yh6tyhcHtWS6wibC7sAT60lk2llSt3AWnCN1Q/0?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;
 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set_named_space&lt;/code&gt; 函数在获取正在修改的 table space 时需持有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fil_sys::mutex&lt;/code&gt;，进而被 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUNCATE TABLE&lt;/code&gt; 阻塞。&lt;/p&gt;

&lt;p&gt;在MySQL-5.7 中，所有数据页的操作都需要调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set_named_space&lt;/code&gt; 函数。 这个函数是 InnoDB redo 重放机制中的一部分。Redo 中的所有数据页上的操作只记录了 space id。在重放这些记录前需要将 space id 和 文件名一一对应起来，这样才知道去操作哪个文件。&lt;/p&gt;

&lt;p&gt;MySQL-5.7 的实现是把这个对应关系写到 redo 中去，因此在做数据页修改时就需要获取当前数据页所在的table space。MySQL-8.0 去掉了这部分逻辑，取而代之从 Data Dictionary 中获取 space id 和 文件名的对应关系。因此在 MySQL-8.0 中, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fil_sys::mutex&lt;/code&gt; 使用的频率就非常低了，一般的数据页操作都不会涉及到 table space。实际上 MySQL-8.0 并不存在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fil_sys::mutex&lt;/code&gt; , MySQL-8.0对&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fil_sys&lt;/code&gt; 做了分区，总共分了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;64&lt;/code&gt; 个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fil_shard&lt;/code&gt; 。一般情况下只需要获取一个分区的 mutex 就可以了。&lt;/p&gt;

&lt;h2 id=&quot;truncate-table-实操建议&quot;&gt;TRUNCATE TABLE 实操建议&lt;/h2&gt;
&lt;p&gt;鉴于上述的问题，我们建议使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RENAME + DROP&lt;/code&gt; 的方式来替代 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUNCATE TABLE&lt;/code&gt;。&lt;/p&gt;
&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;o&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;重建一张相同的表&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t1_new&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;LIKE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;检查重建表的表定义是否符合预期&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SHOW&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t1_new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;新表和老表交换&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RENAME&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t1&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t1_bak&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t1_new&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;删除原表&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;DROP&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t1_bak&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如果你使用的是社区的 MySQL-5.7，实例的稳定性又要求特别高。那么建议通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DISCARD TABLESPACE&lt;/code&gt; 的方式来完成表的删除。&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;o&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;创建一个&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bak&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ibd&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;的硬连接&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ln&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bak&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ibd&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hard_link_t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bak&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ibd&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t1_bak&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DISCARD&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TABLESPACE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;DROP&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t1_bak&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;分批&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;truncate&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hard_link_t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bak&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ibd&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;最后&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hard_link_t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bak&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ibd&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;用 Discard 的好处是discard table space期间不需要删除索引，可以避免释放 AHI 带来的问题。这种方法执行删除，唯一不能避免的就是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LRU Scan&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&quot;mysql-80&quot;&gt;MySQL-8.0&lt;/h2&gt;
&lt;p&gt;MySQL-8.0 针对需要删除表的场景做了很大的优化：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;删除表时不再需要从table space 中删除B+tree了。&lt;/li&gt;
  &lt;li&gt;AHI 清理变成了 LRU scan 的方式，如果 index 上没有任何 AHI 记录则不会做 LRU scan，在关闭AHI的情况下，没有任何效率问题。&lt;/li&gt;
  &lt;li&gt;采用多版本的 table space 机制，在删除表时不再需要清理数据页，避免了 LRU scan。&lt;/li&gt;
  &lt;li&gt;实现了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Atomic DDL&lt;/code&gt; 机制，table space 的物理删除过程放到  post_ddl 阶段，避免了长时间持有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict_sys::mutex&lt;/code&gt;。想了解 Atomic DDL 的，可以参考《Atomic DDL 揭秘》&lt;/li&gt;
  &lt;li&gt;TRUNCATE TABLE 内部通过 DROP + CREATE 的方式实现，避免了长时间持有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fil_sys::mutext&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;目前遗留的问题主要有两个：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;大文件删除可能造成的实例抖动&lt;/li&gt;
  &lt;li&gt;个别地方仍然需要 LRU scan 和 flush list scan 可能造成实例的抖动。
AliSQL-8.0 则集成了 AliSQL-5.7 的优化，通过大文件异步删除和基于 space id 分区的设计避免了这两个问题。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;通过以上的优化，MySQL-8.0 上删除表、清空表和需要重建表的 Online/Copy DDL要比 MySQL-5.7 触发问题的概率要低很多。之所以大家体感 MySQL-8.0 非常不稳定，那是因为前几年 MySQL-8.0 由于采用了 Rolling Release 的策略，每个小版本都有新功能引入，所以导致版本极不稳定。但是从 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MySQL-8.0.34&lt;/code&gt; 之后就不再有新功能的合并只做Bugfix，之后就越来越稳定了。&lt;/p&gt;

</content>
  </entry>
  <entry>
    <title type="html">深入理解 MySQL caching_sha2_password 认证机制</title>
    <link href="https://songlibing.github.io/posts/mysql-caching-sha2-password/" rel="alternate" type="text/html" title="深入理解 MySQL caching_sha2_password 认证机制"/>
    <published>2026-04-09T10:00:00+08:00</published>
    <updated>2026-04-09T10:00:00+08:00</updated>
    <id>https://songlibing.github.io/posts/mysql-caching-sha2-password/</id>
    <author>
      <name>宋利兵 (Libing Song)</name>
    </author>
    <category term="MySQL"/>
    <category term="认证"/>
    <category term="安全"/>
    <category term="caching_sha2_password"/>
    <summary type="html">密码认证的基本要素 基于密码的身份认证包括了两个部分： 服务器端认证信息的存储 密码的认证过程 基于密码的身份认证有一个原则：仅使用者知道密码。密码不能被存储在认证服务器中，在认证过程中也不能通过网络明文传输。因为存储的信息可能被窃取或者滥用，网络可能被监听。这些安全隐患都可能导致密码的泄露。 MySQL中常用的 mysql_native_password 和 caching_sha2_password 都遵循上述的要求。mysql_natvie_password 是MySQL-8.0之前最主要的认证方式。从MySQL-5.7.23开始，MySQL引入了新的认证方式 caching_sha2_password，并在MySQL-8.0将其设置为默认的认证方式。mysql_native_password则在MySQL-8.4被默认禁用，并且从MySQL-9.0.0之后被移除了。关于 mysql_native_password 的实现逻辑，可以参考《MySQL原生密码认证》一文。 为什么引入 caching_sha2_password mysql_native_password 诞生于 2000 年代初，使用 SHA1(SHA1(密码)) 做存储和认证。在当时的计算能力和安全威胁模型下，这个方案是可靠的。随着时间推移，情况已经发生改变。GPU 和专用硬件使得暴力破解 SHA1 的成本大幅降低；预计算彩虹表在无盐哈希面前效率极高；2017 年 Google 和 CWI Amsterdam 完成了对 SHA1 的首次实际碰撞攻击（SHAttered）。此后 NIST 正式建议弃用 SHA1（NIST SP 800-131A Rev.2）。在这样的背景下，MySQL-5.7 引入了 caching_sha2_password，来应对新的安全挑战，满足企业的合规需求。 认证信息的存储 mysql_native_password 的存储格式 在《MySQL原生密码认证》中我们介绍过，mysql_native_password 在 mysql.user 表中存储的是两次 SHA1 的哈希值： 1 *0D3CED9BEC10A777AEC23CCC353A8C08A633045E 其计算方式为： 1 stage2hash = SHA1(SHA1(密码)) 这个哈希值只有 20 字节（十六进制...</summary>
    <content type="html" xml:base="https://songlibing.github.io/posts/mysql-caching-sha2-password/">&lt;h2 id=&quot;密码认证的基本要素&quot;&gt;密码认证的基本要素&lt;/h2&gt;
&lt;p&gt;基于密码的身份认证包括了两个部分：&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;服务器端认证信息的存储&lt;/li&gt;
  &lt;li&gt;密码的认证过程&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;基于密码的身份认证有一个原则：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;仅使用者知道密码&lt;/code&gt;。密码不能被存储在认证服务器中，在认证过程中也不能通过网络明文传输。因为存储的信息可能被窃取或者滥用，网络可能被监听。这些安全隐患都可能导致密码的泄露。&lt;/p&gt;

&lt;p&gt;MySQL中常用的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caching_sha2_password&lt;/code&gt; 都遵循上述的要求。mysql_natvie_password 是MySQL-8.0之前最主要的认证方式。从&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MySQL-5.7.23&lt;/code&gt;开始，MySQL引入了新的认证方式 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caching_sha2_password&lt;/code&gt;，并在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MySQL-8.0&lt;/code&gt;将其设置为默认的认证方式。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt;则在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MySQL-8.4&lt;/code&gt;被默认禁用，并且从&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MySQL-9.0.0&lt;/code&gt;之后被移除了。关于 mysql_native_password 的实现逻辑，可以参考《MySQL原生密码认证》一文。&lt;/p&gt;

&lt;h2 id=&quot;为什么引入-caching_sha2_password&quot;&gt;为什么引入 caching_sha2_password&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt; 诞生于 2000 年代初，使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SHA1(SHA1(密码))&lt;/code&gt; 做存储和认证。在当时的计算能力和安全威胁模型下，这个方案是可靠的。随着时间推移，情况已经发生改变。GPU 和专用硬件使得暴力破解 SHA1 的成本大幅降低；预计算彩虹表在无盐哈希面前效率极高；2017 年 Google 和 CWI Amsterdam 完成了对 SHA1 的首次实际碰撞攻击（&lt;a href=&quot;https://shattered.io/&quot;&gt;SHAttered&lt;/a&gt;）。此后 NIST 正式建议弃用 SHA1（&lt;a href=&quot;https://csrc.nist.gov/pubs/sp/800/131/a/r2/final&quot;&gt;NIST SP 800-131A Rev.2&lt;/a&gt;）。在这样的背景下，MySQL-5.7 引入了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caching_sha2_password&lt;/code&gt;，来应对新的安全挑战，满足企业的合规需求。&lt;/p&gt;

&lt;h2 id=&quot;认证信息的存储&quot;&gt;认证信息的存储&lt;/h2&gt;

&lt;h3 id=&quot;mysql_native_password-的存储格式&quot;&gt;mysql_native_password 的存储格式&lt;/h3&gt;

&lt;p&gt;在《MySQL原生密码认证》中我们介绍过，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt; 在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql.user&lt;/code&gt; 表中存储的是两次 SHA1 的哈希值：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;*0D3CED9BEC10A777AEC23CCC353A8C08A633045E
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;其计算方式为：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;stage2hash = SHA1(SHA1(密码))
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个哈希值只有 20 字节（十六进制 40 字节），而且没有加盐，也就是说&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;相同的密码产生的哈希值总是一样的&lt;/code&gt;。这就给暴力破解和彩虹表破解创造了便利条件。&lt;/p&gt;

&lt;h3 id=&quot;caching_sha2_password-的存储格式&quot;&gt;caching_sha2_password 的存储格式&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caching_sha2_password&lt;/code&gt; 在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql.user&lt;/code&gt; 表的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;authentication_string&lt;/code&gt; 字段中存储的格式如下：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;$A$005$&amp;lt;20字节的salt&amp;gt;&amp;lt;43字节的digest&amp;gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我们来看看每个部分的含义：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$&lt;/code&gt;：分隔符，每一部分都是以&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$&lt;/code&gt;开始&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$A&lt;/code&gt;：摘要类型，A 表示 SHA256&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$005&lt;/code&gt;：哈希计算轮次的十六进制表示，实际次数 = 0x005 × 1000 = 5000 轮&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$&amp;lt;salt&amp;gt;&amp;lt;digest&amp;gt;&lt;/code&gt;: 20 字节随机生成的salt(随机数)和43 字节Base64 编码的 SHA256 摘要&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;哈希计算轮次通过系统变量 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caching_sha2_password_digest_rounds&lt;/code&gt; 配置，默认 5000 轮，最大可配置到 0xFFF × 1000 = 4,095,000 轮。这个参数是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;只读的&lt;/code&gt;，启动后不能动态修改。不同用户可以有不同的轮数，因为轮数编码在各自的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;authentication_string&lt;/code&gt;字段中。修改该参数后，只有&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;新设置密码的用户&lt;/code&gt;才会使用新的轮数，已有用户不受影响。密码最大长度为 256 字节。&lt;/p&gt;

&lt;p&gt;相比 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt;，主要增强了两个方面：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;加盐：每个用户使用 20 字节随机盐值，相同密码产生不同哈希值，攻击者必须针对每个用户独立计算&lt;/li&gt;
  &lt;li&gt;多轮哈希：默认 5000 轮 SHA256，单次破解的计算量远高于 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt; 的 2 轮 SHA1&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;两段式认证&quot;&gt;两段式认证&lt;/h2&gt;
&lt;p&gt;多轮加盐哈希增加了破解的难度，但同时也带来了认证效率的下降。5000 轮哈希的计算太慢了，如果每次认证都需要计算哈希，会严重的降低认证的效率。&lt;/p&gt;

&lt;p&gt;于是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caching_sha2_password&lt;/code&gt; 做了一个两阶段认证的设计：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;快速认证(fast_auth)&lt;/li&gt;
  &lt;li&gt;完整认证(full_auth)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;首次认证&lt;/code&gt;必须走完整认证（传输密码 → 多轮哈希验证 → 缓存快速摘要）。首次认证后服务器会缓存 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SHA256(SHA256(密码))&lt;/code&gt; 到内存。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;后续认证&lt;/code&gt;走快速认证（scramble 验证，类似 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt;）&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;这也是 &quot;caching&quot; 这个名字的由来——通过缓存快速哈希，将昂贵的多轮哈希验证转换为轻量级的 scramble 验证。&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;之所以称作两阶段认证，是因为认证的过程默认使用的是快速认证。客户端会先执行快速认证的过程，如果认证失败，则会进行完整认证。&lt;/p&gt;

&lt;h2 id=&quot;快速认证&quot;&gt;快速认证&lt;/h2&gt;

&lt;h3 id=&quot;1-服务器发送-scramble-到客户端&quot;&gt;1. 服务器发送 scramble 到客户端&lt;/h3&gt;

&lt;p&gt;服务器生成一个 20 字节的随机字符串（scramble），发送给客户端。这个随机字符串和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt; 中的作用一样，用于生成一次性的加密秘钥。&lt;/p&gt;

&lt;h3 id=&quot;2-客户端生成并发送密文&quot;&gt;2. 客户端生成并发送密文&lt;/h3&gt;

&lt;p&gt;客户端收到 scramble 后，使用 SHA256 算法生成 32 字节的密文发送给服务器：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;digest_stage1 = SHA256(密码)
digest_stage2 = SHA256(digest_stage1)
key = SHA256(digest_stage2 + scramble)
密文 = XOR(digest_stage1, key)
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个过程和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt; 的逻辑完全一致，区别只是将 SHA1 替换成了 SHA256，密文长度从 20 字节变成了 32 字节。&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;对密码进行两轮 SHA256 哈希 (digest_stage2) 后混合 scramble 产生一个加密密钥。&lt;/li&gt;
  &lt;li&gt;通过 XOR 的方式对密码的 SHA256 哈希(digest_stage1) 进行加密后发送给服务端。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;3-服务器验证密文&quot;&gt;3. 服务器验证密文&lt;/h3&gt;

&lt;p&gt;快速认证的前提是服务器的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;内存缓存中已经存在该用户的快速认证哈希&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;服务器从内存缓存中取出该用户的快速摘要（即 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;digest_stage2 SHA256(SHA256(密码))&lt;/code&gt;），然后验证：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;key = SHA256(digest_stage2 + scramble)
digest_stage1 = XOR(密文, key)
验证: SHA256(digest_stage1) == 快速摘要 ?
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;ul&gt;
  &lt;li&gt;服务端通过缓存的 digest_stage2 哈希，生成和客户端相同的key&lt;/li&gt;
  &lt;li&gt;然后用这个 key 解密出 digest_stage1&lt;/li&gt;
  &lt;li&gt;最后用 SHA256(digest_stage1) 和自己缓存的 digest_stage2 哈希比对是否相同。&lt;/li&gt;
  &lt;li&gt;如果成功则发送&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;快速认证成功&lt;/code&gt;的消息给客户端，否则发送完整认证的请求给客户端。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;协议消息定义&quot;&gt;协议消息定义&lt;/h3&gt;
&lt;p&gt;除了快速认证成功消息，后面还有两个特殊的消息。他们都是只有&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1&lt;/code&gt;个字节的消息定义如下。
&lt;img src=&quot;https://mmbiz.qpic.cn/mmbiz_png/Jg6JM4XTDce7gD4RcDd12wicqgkhBV52Eibh90KU7MdEvR0qokWIdKibG37nbWj9qiaJW9eqLTT0kjSfPAiaP8ZnFH0MMxtWcJ4fn4rICpZ3zgoA/640?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;完整认证&quot;&gt;完整认证&lt;/h2&gt;

&lt;p&gt;当缓存中没有该用户的条目或快速认证验证失败时，服务器发送&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;要求客户端进行完整认证&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&quot;客户端发送密码&quot;&gt;客户端发送密码&lt;/h3&gt;

&lt;p&gt;完整认证需要客户端将用户的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;原始密码&lt;/code&gt;发送给服务端，因此需要有一个安全的传输通道。根据连接类型有两种处理方式：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;安全连接（TLS / Unix Socket）：通道本身已加密或者是本地socket连接，直接发送明文密码。&lt;/li&gt;
  &lt;li&gt;非安全连接：需要使用服务端的 RSA 公钥对用户密码进行加密发送。在加密前，要先将用户密码和 scramble 做 XOR 运算。由于 scramble 是一次性的随机数，可以防止重放攻击。
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;xor_password = XOR(用户密码, scramble)
密文 = RSA_encrypt(xor_password)
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果客户端没有预配置 RSA 公钥，可向服务器发送公钥请求。服务器收到请求后，则返回 PEM 格式的公钥。客户端相关的连接参数：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--server-public-key-path&lt;/code&gt;：预先指定服务器 RSA 公钥文件路径（推荐，避免中间人攻击）&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--get-server-public-key&lt;/code&gt;：允许客户端从服务器获取公钥（方便但存在安全风险）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果非 TLS 连接且以上两个参数都未设置，客户端会报错&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authentication requires secure connection&lt;/code&gt;。这是 DBA 从 MySQL-5.7 升级到 MySQL-8.0 后最常遇到的连接问题。&lt;/p&gt;

&lt;h3 id=&quot;服务器验证密码&quot;&gt;服务器验证密码&lt;/h3&gt;

&lt;p&gt;服务器收到密码后（RSA 加密的先解密还原），根据 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;authentication_string&lt;/code&gt;字段中的盐值(salt)和迭代次数，对明文密码计算 SHA256-crypt 多轮哈希，然后与user表中记录的摘要比对。&lt;/p&gt;

&lt;p&gt;认证成功后，服务器生成快速摘要 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SHA256(SHA256(密码))&lt;/code&gt; 并&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;写入内存缓存&lt;/code&gt;，后续该用户的连接就可以走快速认证了。&lt;/p&gt;

&lt;p&gt;完整的认证流程图如下所示:
&lt;img src=&quot;https://mmbiz.qpic.cn/sz_mmbiz_png/Jg6JM4XTDcdzWmKuFp3jv6DE8YKDu0JlicY8YIBZEOBAoeI0hq1xyNf93Qw4gU9vSdapQ9wWE8KkAZFRYQrKHZseia487ibKFJZCBvibpTItLvw/640?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;参考代码(MySQL-8.0)：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sql/auth/sha2_password.cc&lt;/code&gt; 中的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Caching_sha2_password::authenticate()&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&quot;缓存管理&quot;&gt;缓存管理&lt;/h2&gt;

&lt;h3 id=&quot;缓存结构&quot;&gt;缓存结构&lt;/h3&gt;

&lt;p&gt;缓存使用哈希表实现，key 是”用户名+主机名”，value 是该用户的快速摘要。读写锁保护并发访问：快速认证查找缓存时加读锁，完整认证成功写入缓存时加写锁。写锁只在写入缓存的瞬间持有，5000 轮哈希计算在锁外完成，不会阻塞其他连接的快速认证。&lt;/p&gt;

&lt;h3 id=&quot;缓存失效&quot;&gt;缓存失效&lt;/h3&gt;

&lt;p&gt;caching_sha2_password 实现了一个审计 plugin（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sha2_cache_cleaner&lt;/code&gt;），通过监听认证相关的审计事件来管理认证缓存。之所以使用 audit plugin 而不是直接在 ACL 子系统中调用缓存清理接口，是为了保持松耦合——认证插件不需要侵入 ACL 模块的代码，只需监听事件即可响应用户变更。具体的失效规则：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FLUSH PRIVILEGES&lt;/code&gt;：清空所有用户的缓存，之后所有用户需重新走完整认证&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DROP USER&lt;/code&gt;：删除该用户的缓存条目&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RENAME USER&lt;/code&gt;：删除旧用户名的缓存条目&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ALTER USER&lt;/code&gt;（修改密码）：删除该用户的缓存条目&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;参考代码：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sql/auth/sha2_password.cc&lt;/code&gt; 中的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sha2_cache_cleaner_notify()&lt;/code&gt;&lt;/p&gt;

&lt;h3 id=&quot;双密码支持&quot;&gt;双密码支持&lt;/h3&gt;

&lt;p&gt;MySQL-8.0 支持 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ALTER USER ... RETAIN CURRENT PASSWORD&lt;/code&gt;，允许一个用户同时拥有主密码和备用密码（旧密码）。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caching_sha2_password&lt;/code&gt; 对双密码的支持体现在：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;存储层：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;authentication_string&lt;/code&gt;（主密码）和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;additional_auth_string&lt;/code&gt;（备用密码）各自独立存储&lt;/li&gt;
  &lt;li&gt;内存缓存：每个用户的缓存条目包含两个槽位，分别存储主密码和备用密码的快速摘要&lt;/li&gt;
  &lt;li&gt;认证顺序：先验证主密码，失败后再验证备用密码。快速认证和完整认证都遵循这个顺序&lt;/li&gt;
  &lt;li&gt;审计标记：如果用户通过备用密码认证成功，服务器会记录一条日志提示管理员&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;与-mysql_native_password-的兼容&quot;&gt;与 mysql_native_password 的兼容&lt;/h2&gt;

&lt;p&gt;MySQL-5.7 升级到 MySQL-8.0 后，已有的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt; 用户仍然使用原来的认证插件不受影响。如果需要将用户切换到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caching_sha2_password&lt;/code&gt;，需要通过&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ALTER USER&lt;/code&gt; 进行调整。创建新用户时，默认使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caching_sha2_password&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&quot;安全性分析&quot;&gt;安全性分析&lt;/h2&gt;

&lt;h3 id=&quot;与-mysql_native_password-的对比&quot;&gt;与 mysql_native_password 的对比&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://mmbiz.qpic.cn/mmbiz_png/Jg6JM4XTDcedp7kxckDDsFoQGJzicbPoSiap6Mdia6lLFes8ibj9RcXkDv51PPUePl2pH6fXRa1kR9bjDmQgeIt1xcKjibretDrC78r57ibkOo3A4/640?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;服务器上的哈希值被盗&quot;&gt;服务器上的哈希值被盗&lt;/h3&gt;

&lt;p&gt;由于使用了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;加盐 + 多轮哈希&lt;/code&gt;，即使 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;authentication_string&lt;/code&gt; 泄露，攻击者也需要针对每个用户独立计算 5000 轮哈希才能尝试破解。而 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt; 未使用盐值，相同密码的哈希值相同，攻击者可以用一次计算结果匹配所有使用相同密码的用户。&lt;/p&gt;

&lt;h3 id=&quot;内存缓存被盗&quot;&gt;内存缓存被盗&lt;/h3&gt;

&lt;p&gt;如果攻击者能读取服务器进程内存，获取到缓存的快速摘要 SHA256(SHA256(密码))，攻击者&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;不能&lt;/code&gt;直接用它通过快速认证。因为认证需要的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SHA256(密码)&lt;/code&gt;，而从 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SHA256(SHA256(密码))&lt;/code&gt; 无法反推出 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SHA256(密码)&lt;/code&gt;。但攻击者可以用它做离线暴力破解或者彩虹表破解。相比从磁盘上的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;authentication_string&lt;/code&gt; 破解（每次尝试 5000 轮）代价低得多。这和偷到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt; 中 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql.user&lt;/code&gt; 存储的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SHA1(SHA1(密码))&lt;/code&gt; 的情况是一样的。&lt;/p&gt;

&lt;h3 id=&quot;网络被监听&quot;&gt;网络被监听&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;快速认证：和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt; 安全性相同。每次 scramble 不同，无法重放或反推。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;完整认证：需要传输密码，因此必须通过 TLS 或 RSA 加密保护。这里有一个需要注意的风险：如果客户端从服务器获取 RSA 公钥（而非预先配置），在中间人攻击场景下，攻击者可以替换为自己的公钥。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;建议在不安全的网络环境中预先配置 RSA 公钥，或直接使用 TLS 连接&lt;/code&gt;。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;

&lt;p&gt;从 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt; 到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caching_sha2_password&lt;/code&gt;，是 MySQL 密码认证体系随安全威胁演进而自然升级的结果。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt; 在它的时代是可靠的设计；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caching_sha2_password&lt;/code&gt; 则在继承其 scramble 验证思想的基础上，针对现代安全威胁引入了加盐和多轮哈希，并通过缓存机制平衡了安全性和性能：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;存储层：加盐 + 5000 轮 SHA256-crypt，大幅提升单次破解的计算量&lt;/li&gt;
  &lt;li&gt;认证层：缓存 SHA256(SHA256(密码))，继承 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql_native_password&lt;/code&gt; 的 scramble 验证思想&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;首次连接&lt;/code&gt;走完整认证（慢但安全），&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;后续连接&lt;/code&gt;走快速认证（快且安全）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;使用建议：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;优先使用 TLS 加密连接&lt;/li&gt;
  &lt;li&gt;非 TLS 环境下，客户端预先配置 RSA 公钥（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--server-public-key-path&lt;/code&gt;）&lt;/li&gt;
  &lt;li&gt;了解 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FLUSH PRIVILEGES&lt;/code&gt; 和服务器重启会清空缓存，可能导致短暂 CPU 尖刺&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;caching_sha2_password.digest_rounds&lt;/code&gt; 可调整存储哈希轮数，轮数越高越安全但完整认证越慢&lt;/li&gt;
&lt;/ul&gt;

</content>
  </entry>
  <entry>
    <title type="html">MySQL 内存问题分析利器 Jemalloc</title>
    <link href="https://songlibing.github.io/posts/mysql-jemalloc/" rel="alternate" type="text/html" title="MySQL 内存问题分析利器 Jemalloc"/>
    <published>2026-04-08T10:00:00+08:00</published>
    <updated>2026-07-17T17:02:35+08:00</updated>
    <id>https://songlibing.github.io/posts/mysql-jemalloc/</id>
    <author>
      <name>宋利兵 (Libing Song)</name>
    </author>
    <category term="MySQL"/>
    <category term="内存"/>
    <category term="Jemalloc"/>
    <category term="性能优化"/>
    <summary type="html">内存泄漏、内存占用高是MySQL中较常见的问题，这些问题的排查非常依赖完善的内存监控信息。MySQL的Performance Schema中提供了内存的监控信息，PFS的内存统计信息粒度比较粗，很难精准的定位到问题代码。 上图是PFS中监控到的内存信息，虽然从内存监控中我们看到String::value和thd::main_mem_root上分配了很多的内存，但是这两个对象是MySQL中的基本对象，在非常多的地方使用。我们无法从这些信息推断出到底是什么操作导致了内存的占用。此外，由于PFS本身的内存占用比较高并且对性能有一定的影响，因此很多使用者不会在实例上开启PFS。 Jemalloc是一个高效的内存分配器，通过进程和线程级的内存缓存机制，提升内存的分配、回收性能，并减少内存的碎片。此外Jemalloc提供了一套内存监控和分析机制，有以下几个特点： 通过采样的方式记录内存的分配和释放，来减少对进程运行的影响。 采样时记录了内存分配时的函数调用栈，通过函数栈可以准确的定位内存分配行为。 可以将采样信息dump到文件中。 通过jeprof工具可以将采样的信息用图形直观的展示。 可以将两个时间点的采样信息Diff后进行展示，这样可以聚焦到一段时间内分配的内存，让问题的分析更容易。 Jemalloc非常适合用来诊断MySQL中的内存使用问题，以及研究MySQL中的内存使用情况。下面我们通过两个案例来体验一下Jemalloc的强大内存分析能力。 案例 1 - Clone_persist_gtid内存泄漏 Bug#107991是AliSQL的同学利用Jemalloc定位到的一个内存泄漏问题。Clone_persist_gtid是MySQL-8.0引入的一个线程用来将事务的Gtid持久化到mysql.gtid_executed表中。如果实例的写事务非常频繁，就会遇到这个内存泄漏的问题。每次泄漏的内存不多，但是经过几天、十几天后泄漏的内存就会非常多。下图是该bug的函数调用栈的一部分，完整的快照文件在Bug#107991的页面里，需要的可以自取。 由于泄漏的内存占总内存的很小比例，这里的图是用两个时间点的采样信息Diff后产生的。通过这个函数调用栈，我们可以清楚的看到内存是Clone_persist_gtid线程打开系统表时分配的。通过这个函数调用栈很快就可以定位到，是因为该线程THD::mem_root一直没有清理导致的内存泄漏（详情请参考Bug#107991)。修复也很简单，只有一行代码。我们很快在AliSQL中做了修复，并且提交了Patch给官方。官方在最新的版本MySQL-8.0.42修复了该问题。 案例 2 - AHI内存浪费 除了分析内存泄漏，在研究MySQL的内存使用上也非常有用。比如拉起一个实例后，立即生成一个内存分配的快照，来分析实例启动时的内存分配情况。下图是一个64GB Buffer Pool实例拉起后的内存快照的一部分。 我们可以看到Buffer Pool初始化的过程中，分配的内存包括以下两部分: btr_search_sys_create 创建自适应哈希索引结构分配了1280MB内存。 buf_block_init 为buffer pool中的每个page的rw lock和mutex创建的os_event结构，总共占用了1918.8MB内存。 由于AHI存在着严重的稳定性问题，我们默认都是关闭AHI的，官方也从MySQL-8.4开始将默认值改成了OFF。然而在AHI关闭的情况下，AHI仍然占用了1GB的内存。查阅代码可知这部分内存是buffer pool大小的1/64（详细分析见Bug#112223）。浪费的内存有点多, 所以AliSQL修复了这个问题，在AHI关闭的情况下不会分配这部分的内存。 为什么图中没有Buffer Pool占用的64G内存呢？因为Buffer Pool的内存分配是用mmap不是malloc，所以不会被jemalloc监控到。 MySQL使用jemalloc 系统中安装libjemalloc后，做如下配置： 1 2 3 # 设置jemalloc库到LD_PRELOAD export LD_PRELOAD=&amp;lt;/路径/jemalloc.so.2&amp;gt; # 启动 mysqld 如果使用mysqld_safe启动MySQL, 则可以在MySQL的配置文件中配置，如下： 1 2 [mysqld_safe] malloc-lib = &amp;lt;/路径/jemalloc.so.2&amp;gt; 可以通过如下方法检查mysqld是否使用了jemalloc 1 lsof...</summary>
    <content type="html" xml:base="https://songlibing.github.io/posts/mysql-jemalloc/">&lt;p&gt;内存泄漏、内存占用高是MySQL中较常见的问题，这些问题的排查非常依赖完善的内存监控信息。MySQL的Performance Schema中提供了内存的监控信息，PFS的内存统计信息粒度比较粗，很难精准的定位到问题代码。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://mmbiz.qpic.cn/sz_mmbiz_png/ibf4J5w9SFz4qibCyCxtwSASicXk2rlzpar3tmHauB4moaicvcSw0DuonIgtfPWXqStITXNcBVfw9fPefnPVkRBarg/640?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;上图是PFS中监控到的内存信息，虽然从内存监控中我们看到&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;String::value&lt;/code&gt;和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;thd::main_mem_root&lt;/code&gt;上分配了很多的内存，但是这两个对象是MySQL中的基本对象，在非常多的地方使用。我们无法从这些信息推断出到底是什么操作导致了内存的占用。此外，由于PFS本身的内存占用比较高并且对性能有一定的影响，因此很多使用者不会在实例上开启PFS。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/jemalloc/jemalloc&quot;&gt;Jemalloc&lt;/a&gt;是一个高效的内存分配器，通过进程和线程级的内存缓存机制，提升内存的分配、回收性能，并减少内存的碎片。此外Jemalloc提供了一套内存监控和分析机制，有以下几个特点：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;通过采样的方式记录内存的分配和释放，来减少对进程运行的影响。&lt;/li&gt;
  &lt;li&gt;采样时记录了内存分配时的函数调用栈，通过函数栈可以准确的定位内存分配行为。&lt;/li&gt;
  &lt;li&gt;可以将采样信息dump到文件中。&lt;/li&gt;
  &lt;li&gt;通过jeprof工具可以将采样的信息用图形直观的展示。&lt;/li&gt;
  &lt;li&gt;可以将两个时间点的采样信息Diff后进行展示，这样可以聚焦到一段时间内分配的内存，让问题的分析更容易。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Jemalloc非常适合用来诊断MySQL中的内存使用问题，以及研究MySQL中的内存使用情况。下面我们通过两个案例来体验一下Jemalloc的强大内存分析能力。&lt;/p&gt;

&lt;h2 id=&quot;案例-1---clone_persist_gtid内存泄漏&quot;&gt;案例 1 - Clone_persist_gtid内存泄漏&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.mysql.com/bug.php?id=107991&quot;&gt;Bug#107991&lt;/a&gt;是AliSQL的同学利用Jemalloc定位到的一个内存泄漏问题。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Clone_persist_gtid&lt;/code&gt;是MySQL-8.0引入的一个线程用来将事务的Gtid持久化到&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mysql.gtid_executed&lt;/code&gt;表中。如果实例的写事务非常频繁，就会遇到这个内存泄漏的问题。每次泄漏的内存不多，但是经过几天、十几天后泄漏的内存就会非常多。下图是该bug的函数调用栈的一部分，完整的快照文件在Bug#107991的页面里，需要的可以自取。
&lt;img src=&quot;https://mmbiz.qpic.cn/sz_mmbiz_png/ibf4J5w9SFz4qibCyCxtwSASicXk2rlzparWibCDhQqeLp8UZj6loNYmABgB6KFTfibrgsVo3ibTvJhUBr3AqQ4VXBlQ/640?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;
由于泄漏的内存占总内存的很小比例，这里的图是用两个时间点的采样信息&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Diff&lt;/code&gt;后产生的。通过这个函数调用栈，我们可以清楚的看到内存是Clone_persist_gtid线程打开系统表时分配的。通过这个函数调用栈很快就可以定位到，是因为该线程&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;THD::mem_root&lt;/code&gt;一直没有清理导致的内存泄漏（详情请参考Bug#107991)。修复也很简单，只有一行代码。我们很快在AliSQL中做了修复，并且提交了Patch给官方。官方在最新的版本MySQL-8.0.42修复了该问题。&lt;/p&gt;

&lt;h2 id=&quot;案例-2---ahi内存浪费&quot;&gt;案例 2 - AHI内存浪费&lt;/h2&gt;
&lt;p&gt;除了分析内存泄漏，在研究MySQL的内存使用上也非常有用。比如拉起一个实例后，立即生成一个内存分配的快照，来分析实例启动时的内存分配情况。下图是一个64GB Buffer Pool实例拉起后的内存快照的一部分。
&lt;img src=&quot;https://mmbiz.qpic.cn/sz_mmbiz_png/ibf4J5w9SFz4z3HlTS49TWXOKL1jv2zMqWbba8icP1sPibEf2aOmWTPldvOlZp23rSKuNgqGicesZrSDpBpVibgXaQA/640?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;
我们可以看到Buffer Pool初始化的过程中，分配的内存包括以下两部分:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;btr_search_sys_create&lt;/code&gt; 创建自适应哈希索引结构分配了1280MB内存。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;buf_block_init&lt;/code&gt; 为buffer pool中的每个page的rw lock和mutex创建的os_event结构，总共占用了1918.8MB内存。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;由于AHI存在着严重的稳定性问题，我们默认都是关闭AHI的，官方也从MySQL-8.4开始将默认值改成了OFF。然而在AHI关闭的情况下，AHI仍然占用了1GB的内存。查阅代码可知这部分内存是buffer pool大小的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1/64&lt;/code&gt;（详细分析见&lt;a href=&quot;https://bugs.mysql.com/bug.php?id=112223&quot;&gt;Bug#112223&lt;/a&gt;）。浪费的内存有点多, 所以AliSQL修复了这个问题，在AHI关闭的情况下不会分配这部分的内存。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;为什么图中没有Buffer Pool占用的64G内存呢？因为Buffer Pool的内存分配是用mmap不是malloc，所以不会被jemalloc监控到。&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&quot;mysql使用jemalloc&quot;&gt;MySQL使用jemalloc&lt;/h2&gt;

&lt;p&gt;系统中安装libjemalloc后，做如下配置：&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# 设置jemalloc库到LD_PRELOAD&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;LD_PRELOAD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&amp;lt;/路径/jemalloc.so.2&amp;gt;  
&lt;span class=&quot;c&quot;&gt;# 启动 mysqld&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;如果使用mysqld_safe启动MySQL, 则可以在MySQL的配置文件中配置，如下：&lt;/p&gt;
&lt;div class=&quot;language-ini highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nn&quot;&gt;[mysqld_safe]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;malloc-lib&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;lt;/路径/jemalloc.so.2&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;可以通过如下方法检查mysqld是否使用了jemalloc&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;lsof &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &amp;lt;pid&amp;gt; |grep jemalloc
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;开启jemallocprofiling&quot;&gt;开启jemalloc profiling&lt;/h2&gt;
&lt;p&gt;在启动mysqld前，需要提前设置以下环境变量开启jemalloc的profiling&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;MALLOC_CONF&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;prof:true,prof_active:true,prof_prefix:/tmp/mysqld.jedump&quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;prof：设置开启profiling，只能在启动mysqld前设置。&lt;/li&gt;
  &lt;li&gt;prof_active:设置profiling 的状态为active，如果设置为false，则不会记录内存分配的信息。&lt;/li&gt;
  &lt;li&gt;prof_prefix: 设置profiling 快照的存储位置和文件前缀。
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Profiling功能需要在编译时开启&lt;/code&gt;, 如果jemalloc库不支持profiling，则会报如下的错误:
&lt;img src=&quot;https://mmbiz.qpic.cn/sz_mmbiz_png/ibf4J5w9SFz7NRR7TKZ1xDE4uMGDm1rDvw68SkGn25xAD6zm5XDx7MZemPFYrZYlSibFeHqYKuMbEWZdDlBnd7Vw/640?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;
这时需要重新编译一个支持profiling的版本，编译时需要加上参数&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--enable-prof&lt;/code&gt;。
    &lt;h3 id=&quot;对性能的影响&quot;&gt;对性能的影响&lt;/h3&gt;
    &lt;p&gt;通过sysbench压测，大概可以得出以下的结论：&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;Jemalloc-5.2 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prof:true,prof_active:false&lt;/code&gt;时,  对性能基本没有影响，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prof:true, prof_active:true&lt;/code&gt;时，高并发下大约有4%的性能下降。&lt;/li&gt;
  &lt;li&gt;Jemalloc-5.3 在两种情况下对性能都没有明显的影响。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;自动生成内存快照&quot;&gt;自动生成内存快照&lt;/h2&gt;
&lt;p&gt;jemalloc提供了两种自动生成快照的方式：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;lg_prof_interval 每分配多少内存后，dump一次。 值为2的N次方，比如设置为30，则为2的30次方即1GB，意味着每1GBdump一次。&lt;/li&gt;
  &lt;li&gt;prof_gdump 每次当总内存数创新高时dump 一次。&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;MALLOC_CONF&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;prof:true,prof_active:true,lg_prof_interval:30,prof_prefix:/tmp/mysqld.jedump.&quot;&lt;/span&gt; 
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;MALLOC_CONF&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;prof:true,prof_active:true,prof_gdump:true,prof_prefix:/tmp/mysqld.jedump.&quot;&lt;/span&gt; 
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;手动生成内存快照&quot;&gt;手动生成内存快照&lt;/h2&gt;
&lt;p&gt;自动生成快照虽然简单，但是对于MySQL这种会频繁分配释放内存的系统，我们更倾向于自己控制生成快照。手动产生快照需要在进程内调用相关的函数来生成快照，比如&lt;a href=&quot;https://docs.percona.com/percona-server/8.0/jemalloc-profiling.html#use-percona-server-for-mysql-with-jemalloc-with-profiling-enabled&quot;&gt;Percona&lt;/a&gt;就在内核中集成了相关的命令来产生快照.
如果使用的是社区版的MySQL，该如何生成快照呢？这里提供了两种方式来手动生成快照，一种是通过gdb来生成，适合于开发环境，或者临时使用场景。另一种是通过UDF的方式来生成快照，这种方法更适用于在生产环境使用。&lt;/p&gt;

&lt;h2 id=&quot;gdb-产生内存快照&quot;&gt;gdb 产生内存快照&lt;/h2&gt;

&lt;p&gt;这种方法是通过gdb调用mallctl函数来产生快照。这时就不需要设置log_prof_interval.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;define jeprof_dump
  p mallctl&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;prof.dump&quot;&lt;/span&gt;, 0, 0, 0, 0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
end

jeprof_dump
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;将以上内容拷贝一个文件中(jemalloc.gdb)，然后调用gdb执行以上文件中的脚本。&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;gdb &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &amp;lt;pid&amp;gt; &lt;span class=&quot;nt&quot;&gt;-x&lt;/span&gt; jemalloc.gdb &lt;span class=&quot;nt&quot;&gt;-batch&lt;/span&gt; 
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;此外，可以动态控制prof_active状态，查看active的状态，脚本如下：&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;define jeprof_status
  &lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$backup_opt_help&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; opt_help
  &lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$backup_opt_tc_log_size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; opt_tc_log_size
  &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;opt_tc_log_size &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; sizeof&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;opt_help&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;

  call mallctl&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;opt.prof&quot;&lt;/span&gt;, &amp;amp;opt_help, &amp;amp;opt_tc_log_size, 0, 0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;printf&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;opt.prof is %d&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;, opt_help

  call mallctl&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;prof.active&quot;&lt;/span&gt;, &amp;amp;opt_help, &amp;amp;opt_tc_log_size, 0, 0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;printf&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;prof.active is %d&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;, opt_help

  &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;opt_help &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$backup_opt_help&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;opt_tc_log_size &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$backup_opt_tc_log_size&lt;/span&gt;
end
jeprof_status
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;define jeprof_off
  p mallctl&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;prof.active&quot;&lt;/span&gt;, 0, 0, &amp;amp;opt_help, sizeof&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;bool&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
end
jeprof_off
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;define jeprof_on
  &lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$backup_opt_help&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; opt_help
  &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;opt_help &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 1

  p mallctl&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;prof.active&quot;&lt;/span&gt;, 0, 0, &amp;amp;opt_help, sizeof&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;bool&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;

  &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;opt_help &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$backup_opt_help&lt;/span&gt;
end
jeprof_on
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;udf产生内存快照&quot;&gt;UDF 产生内存快照&lt;/h2&gt;

&lt;p&gt;gdb的方式比较hack, 不适合自动化。此外gdb会中断进程的运行，会导致实例的抖动。MySQL提供了一套可加载函数的机制，这套机制通常也成为&lt;a href=&quot;https://dev.mysql.com/doc/extending-mysql/8.4/en/adding-loadable-function.html&quot;&gt;UDF&lt;/a&gt;。通过这个机制，我们可以用C语言，将上述功能实现到一个动态库中，然后动态的加载到正在运行的实例中。代码如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-C++&quot;&gt;#include &amp;lt;jemalloc/jemalloc.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;stdbool.h&amp;gt;
/* The following is for user defined functions */

#define PLUGIN_EXPORT
#define longlong long
#define my_bool bool

enum Item_result {STRING_RESULT=0, REAL_RESULT, INT_RESULT, ROW_RESULT,
                  DECIMAL_RESULT};

typedef struct st_udf_args
{
  unsigned int arg_count;		/* Number of arguments */
  enum Item_result *arg_type;		/* Pointer to item_results */
  char **args;				/* Pointer to argument */
  unsigned long *lengths;		/* Length of string arguments */
  char *maybe_null;			/* Set to 1 for all maybe_null args */
  char **attributes;                    /* Pointer to attribute name */
  unsigned long *attribute_lengths;     /* Length of attribute arguments */
  void *extension;
} UDF_ARGS;

/* This holds information about the result */

typedef struct st_udf_init
{
  my_bool maybe_null;          /* 1 if function can return NULL */
  unsigned int decimals;       /* for real functions */
  unsigned long max_length;    /* For string functions */
  char *ptr;                   /* free pointer for function data */
  my_bool const_item;          /* 1 if function always returns the same value */
  void *extension;
} UDF_INIT;

// 查看prof是否开启 (opt.prof)，成功return 0
PLUGIN_EXPORT my_bool
jeprof_prof_status_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
    if (args-&amp;gt;arg_count != 0) {
        strcpy(message, &quot;Usage: prof_opt_status()&quot;);
        return 1;
    }
    return 0;
}

PLUGIN_EXPORT longlong
jeprof_prof_status(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
{
    bool enabled;
    size_t len = sizeof(enabled);

    if (mallctl(&quot;opt.prof&quot;, &amp;amp;enabled, &amp;amp;len, NULL, 0)) {
        *error = 1;
        return -1;
    }
    return enabled ? 1 : 0;
}

// 查看prof.active状态 (prof.active)，成功return 0
PLUGIN_EXPORT my_bool
jeprof_active_status_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
    if (args-&amp;gt;arg_count != 0) {
        strcpy(message, &quot;Usage: prof_active_status()&quot;);
        return 1;
    }
    return 0;
}

PLUGIN_EXPORT longlong
jeprof_active_status(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
{
    bool active;
    size_t len = sizeof(active);

    if (mallctl(&quot;prof.active&quot;, &amp;amp;active, &amp;amp;len, NULL, 0)) {
        *error = 1;
        return -1;
    }
    return active ? 1 : 0;
}

// 开启 profiling, 设置prof.active为true，成功return 0
PLUGIN_EXPORT my_bool
jeprof_enable_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
    if (args-&amp;gt;arg_count != 0) {
        strcpy(message, &quot;Usage: prof_enable()&quot;);
        return 1;
    }
    return 0;
}

PLUGIN_EXPORT longlong
jeprof_enable(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
{
    bool enable = true;

    if (mallctl(&quot;prof.active&quot;, NULL, NULL, &amp;amp;enable, sizeof(enable))) {
        *error = 1;
        return -1;
    }
    return 0;
}

// 关闭 profiling, 设置prof.active为false，成功 return 0
PLUGIN_EXPORT my_bool
jeprof_disable_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
    if (args-&amp;gt;arg_count != 0) {
        strcpy(message, &quot;Usage: prof_disable()&quot;);
        return 1;
    }
    return 0;
}

PLUGIN_EXPORT longlong
jeprof_disable(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
{
    bool enable = false;
    if (mallctl(&quot;prof.active&quot;, NULL, NULL, &amp;amp;enable, sizeof(enable))) {
        *error = 1;
        return -1;
    }
    return 0;
}

// Dump 内存堆栈信息, 成功return 0
PLUGIN_EXPORT my_bool
jeprof_dump_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
    if (args-&amp;gt;arg_count != 0) {
        strcpy(message, &quot;Usage: prof_dump()&quot;);
        return 1;
    }
    return 0;
}

PLUGIN_EXPORT longlong
jeprof_dump(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
{
    int ret = mallctl(&quot;prof.dump&quot;, NULL, NULL, NULL, 0);
    if(ret) {
        *error = 1;
        return ret;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译以上代码依赖jemalloc的头文件，因此需要先安装jemalloc的头文件. yum下安装方法如下：&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;yum &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;jemalloc-devel
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;然后通过以下命令进行编译, 编译后将jemalloc_udf.so拷贝到实例的的&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/server-system-variables.html#sysvar_plugin_dir&quot;&gt;plugin_dir&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;gcc &lt;span class=&quot;nt&quot;&gt;-shared&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-fPIC&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; jemalloc_udf.so jeprof_udf.c
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在使用这些udf前，需要先加载这些UDF，SQL如下：&lt;/p&gt;
&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FUNCTION&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jeprof_dump&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;RETURNS&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;INTEGER&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SONAME&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;jemalloc_udf.so&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FUNCTION&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jeprof_enable&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;RETURNS&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;INTEGER&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SONAME&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;jemalloc_udf.so&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FUNCTION&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jeprof_disable&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;RETURNS&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;INTEGER&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SONAME&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;jemalloc_udf.so&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FUNCTION&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jeprof_prof_status&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;RETURNS&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;INTEGER&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SONAME&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;jemalloc_udf.so&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FUNCTION&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jeprof_active_status&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;RETURNS&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;INTEGER&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SONAME&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;jemalloc_udf.so&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;然后就可以通过SELECT jeprof_xxx()来调用。如图所示：
&lt;img src=&quot;https://mmbiz.qpic.cn/sz_mmbiz_png/ibf4J5w9SFz7NRR7TKZ1xDE4uMGDm1rDvd4NonVULtibYficzsnxUNDUY9sN7GUQpFlHZ9xUQpovFucJw1p2n8yiaA/640?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jeprof_prof_status&lt;/code&gt;显示&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prof&lt;/code&gt;参数的状态，返回1代表开启，0代表关闭。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jeprof_active_status&lt;/code&gt;显示&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prof.active&lt;/code&gt;的状态，返回1代表开启，0代表关闭。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jeprof_enable&lt;/code&gt; 设置&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prof.active&lt;/code&gt;为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt;，成功返回0，否则返回1.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jeprof_disable&lt;/code&gt;设置&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prof.active&lt;/code&gt;为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;false&lt;/code&gt;, 成功返回0，否则返回1.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jeprof_dump&lt;/code&gt; 生成一个内存快照文件, 成功返回0，否则返回1.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;生成profiling调用关系图&quot;&gt;生成Profiling 调用关系图&lt;/h2&gt;
&lt;p&gt;Jemalloc中有一个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jeprof&lt;/code&gt;命令行工具，用来直观的显示内存快照中的信息，一个常用的就是生成调用关系图。快照信息可以用svg,pdf等方式呈现，如图所示
&lt;img src=&quot;https://mmbiz.qpic.cn/sz_mmbiz_png/ibf4J5w9SFz7NRR7TKZ1xDE4uMGDm1rDvf7IPGcffqqyQ2zPyW47rMfsc0ibicZcRKM9Aut8zicRSrDDsG4H7bKMZw/640?wx_fmt=png&amp;amp;from=appmsg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;生图命令如下：&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;jeprof  ./sql/mysqld mysqld.jedump.2 &lt;span class=&quot;nt&quot;&gt;-svg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; jedump.svg
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;产生两个快照的diff命令如下:&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;jeprof ./sql/mysqld &lt;span class=&quot;nt&quot;&gt;--base&lt;/span&gt; mysqld.jedump.1 mysqld.jedump.2 &lt;span class=&quot;nt&quot;&gt;-svg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; jedump_diff.svg
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;
&lt;p&gt;Jemalloc提供了强大内存分析功能，在记录分配的内存信息时采集了函数栈信息，这些信息可以帮助我们快速准确的定位内存问题。通过MySQL的UDF机制，可以很方便的将jemalloc的内存快照能力集成到MySQL中，并且在线的开启和使用。Jemalloc通过采样的来统计内存信息，在默认配置下开销很小，可以放心开启。&lt;/p&gt;

</content>
  </entry>
</feed>
