<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <generator uri="https://jekyllrb.com/">Jekyll</generator>
  <title type="html">Libing Song&apos;s Blog (English)</title>
  <subtitle>English articles from 宋利兵 (Libing Song).</subtitle>
  <link href="https://songlibing.github.io/feed-en.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-en.xml</id>
  <author>
    <name>宋利兵 (Libing Song)</name>
  </author>
  <entry>
    <title type="html">Optimizing Replication Lag for Large Transactions and DDL in MySQL</title>
    <link href="https://songlibing.github.io/posts/mysql-large-transaction-ddl-replication-en/" rel="alternate" type="text/html" title="Optimizing Replication Lag for Large Transactions and DDL in MySQL"/>
    <published>2026-07-17T17:30:00+08:00</published>
    <updated>2026-07-20T20:43:51+08:00</updated>
    <id>https://songlibing.github.io/posts/mysql-large-transaction-ddl-replication-en/</id>
    <author>
      <name>宋利兵 (Libing Song)</name>
    </author>
    <category term="MySQL"/>
    <category term="Replication"/>
    <category term="DDL"/>
    <category term="Large Transaction"/>
    <category term="Replication Lag"/>
    <summary type="html">This article is also available in Chinese: 中文版. Browse all English articles. Since MySQL 5.6, the MySQL replication team has been working to reduce replication lag. The first step was schema-level parallel application of the binlog, but schema-level parallelism only helps when writes are spread across many databases; in the common case, where most write traffic hits a single database,...</summary>
    <content type="html" xml:base="https://songlibing.github.io/posts/mysql-large-transaction-ddl-replication-en/">&lt;blockquote class=&quot;prompt-tip&quot;&gt;
  &lt;p&gt;This article is also available in Chinese: &lt;a href=&quot;https://songlibing.github.io/posts/mysql-large-transaction-ddl-replication/&quot;&gt;中文版&lt;/a&gt;. Browse &lt;a href=&quot;https://songlibing.github.io/english/&quot;&gt;all English articles&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Since MySQL 5.6, the MySQL replication team has been working to reduce replication lag. The first step was schema-level parallel application of the binlog, but schema-level parallelism only helps when writes are spread across many databases; in the common case, where most write traffic hits a single database, it provides almost no parallelism. MySQL 5.7 then introduced the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Commit-Order&lt;/code&gt; parallel-replay strategy, which depends on how many transactions run concurrently on the primary: the replica can replay quickly only when the primary is highly concurrent. When concurrency on the primary is low, the replica still replays slowly and lag builds up. To fix that, MySQL 5.7 also introduced the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Writeset&lt;/code&gt; (row-level) strategy, which lets the replica replay in parallel quickly no matter how concurrent the primary is.&lt;/p&gt;

&lt;p&gt;We rolled out the writeset-based strategy across our fleet long ago, and it eliminated roughly 60% of our replication-lag problems. Another more than 30% comes from large transactions and DDL — the hardest replication-lag problem to solve in MySQL. Last year we built a mechanism in AliSQL called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Binlog Realtime Replication (BRR)&lt;/code&gt; that solves it completely.&lt;/p&gt;

&lt;h2 id=&quot;how-binlog-realtime-replication-works&quot;&gt;How Binlog Realtime Replication Works&lt;/h2&gt;

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

&lt;p&gt;The figure above shows why large transactions and DDL cause replication lag. Binlog replication works at the granularity of a transaction: a transaction’s events are written to the binlog file only after it commits, then shipped to the replica and executed there (a DDL can be treated as a single transaction). The change becomes visible to applications only once the replica finishes executing. If a transaction takes a long time on the primary, it takes just as long on the replica, and the lag equals the replica’s execution time. In practice the lag is often worse. First, a large transaction produces very large binlog events, which adds transmission delay. Second, while a large transaction — especially a DDL — is running, it can block the replay of other transactions, so relay log piles up; once the large transaction or DDL finishes replaying, that backlog also needs time to drain before the replica catches up.&lt;/p&gt;

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

&lt;p&gt;The idea behind the optimization is simple: have the replica start executing the large transaction or DDL at the same time as the primary, and once the primary commits, tell the replica to commit too. With this mechanism, replication lag for large transactions and DDL stays under one second. The chart below compares the lag from a large transaction before and after the optimization: with realtime replication, large transactions no longer cause lag, and neither do DDLs.&lt;/p&gt;

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

&lt;p&gt;The feature has been enabled by default in our RDS service since 2025. To date more than 3,000 instances have used it, running realtime replication about 300,000 times for large transactions and about 60,000 times for DDL.&lt;/p&gt;

&lt;h2 id=&quot;implementing-realtime-replication&quot;&gt;Implementing Realtime Replication&lt;/h2&gt;

&lt;p&gt;The core idea of realtime replication fits in one sentence: &lt;em&gt;as soon as the primary starts executing, it ships the binlog events (or DDL) to the replica, which executes them in lockstep; when the primary finally commits or rolls back, the replica does the same.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Realtime replication has two parts: realtime transmission and realtime application. Realtime transmission streams the binlog events a large transaction produces on the primary to the replica as they are generated; that part is covered in &lt;em&gt;&lt;a href=&quot;https://songlibing.github.io/posts/mysql-large-transaction-binlog-transmission-en/&quot;&gt;Binlog Transmission Optimization for Large MySQL Transactions&lt;/a&gt;&lt;/em&gt;. Realtime application replays those events on the replica as they arrive, using a dedicated group of replay threads, as shown below:&lt;/p&gt;

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

&lt;p&gt;While a transaction runs on the primary, the binlog events it produces are first buffered in the Binlog Cache. If the transaction is large (the Binlog Cache exceeds a threshold), the primary’s Dump thread reads the Binlog Cache temporary file and sends the events straight to the replica. The replica writes them into a dedicated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr Cache&lt;/code&gt; (not the relay log file), where a new group of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr Worker&lt;/code&gt; threads applies them in real time.&lt;/p&gt;

&lt;p&gt;For DDL, binlog events are produced later than for a large transaction — a DDL writes its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Query_log_event&lt;/code&gt; into the Binlog Cache only during the commit phase. BRR therefore handles DDL specially: once the primary starts executing the DDL, it builds the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Query_log_event&lt;/code&gt; directly and puts it in an in-memory buffer, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ddl_query_buffer&lt;/code&gt;; the Dump thread reads events from this buffer and sends them to the replica, where a Brr Worker again executes the DDL in real time.&lt;/p&gt;

&lt;p&gt;As a result, replica execution of DDL and large transactions shifts from &lt;em&gt;run only after the primary finishes&lt;/em&gt; to &lt;em&gt;run on the primary and replica in parallel&lt;/em&gt;, leaving only network transmission and commit as the residual lag — typically on the order of tens of milliseconds.&lt;/p&gt;

&lt;p&gt;Below we look at how BRR is implemented, from both the primary and the replica side.&lt;/p&gt;

&lt;h3 id=&quot;overall-brr-architecture&quot;&gt;Overall BRR Architecture&lt;/h3&gt;

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

&lt;h4 id=&quot;primary-side&quot;&gt;Primary Side&lt;/h4&gt;

&lt;p&gt;When a large transaction or DDL needs realtime replication, a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx&lt;/code&gt; is created and registered with the &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; is an extension of the Dump thread; it reads events from a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx&lt;/code&gt; and pushes them to the replica. Originally the Dump thread did just one thing: read events from the binlog file and send them to the replica. BRR gives it one more job — poll each active &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx&lt;/code&gt;, read binlog events from its Binlog Cache temporary file or from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ddl_query_buffer&lt;/code&gt;, and send them to the replica.&lt;/p&gt;

&lt;p&gt;Realtime transmission reuses the existing Dump channel. To tell BRR traffic apart from ordinary traffic, BRR borrows an idea from Semisync and attaches an extra &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BRR Header&lt;/code&gt; to each event; the header identifies whether an event is BRR or ordinary replication traffic. And to keep BRR events from choking the ordinary binlog-event channel, BRR applies flow control.&lt;/p&gt;

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

&lt;h4 id=&quot;replica-side&quot;&gt;Replica Side&lt;/h4&gt;

&lt;p&gt;Using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BRR Header&lt;/code&gt;, the replica’s IO thread splits events into two kinds: BRR events go into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt;, while normal events take the original path into the relay log.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt; is the replica-side storage for a BRR transaction; each BRR transaction has one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt;. When the IO thread receives a BRR event, it uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;brr_index&lt;/code&gt; in the header to locate the matching &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt; (if it’s the first event, it creates a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt; and wakes a Brr Worker), writes the event into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt; temporary file, and updates the readable position.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_rpl_info&lt;/code&gt; manages these BRR transactions.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BRR Worker&lt;/code&gt; threads are dedicated to applying BRR transactions. When idle, a Brr Worker picks a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt; that hasn’t started being applied and makes itself its owner. From then on it is bound to that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt;, looping to read and replay binlog events until it sees a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_log_event&lt;/code&gt; (the primary has committed) or receives a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BRR_ROLLBACK_EVENT&lt;/code&gt; (the primary rolled back).&lt;/p&gt;

&lt;h3 id=&quot;the-gtid_executed-snapshot&quot;&gt;The gtid_executed Snapshot&lt;/h3&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uncommitted&lt;/code&gt; BRR transactions from the primary run in parallel on the replica alongside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;already-committed&lt;/code&gt; transactions. If a BRR transaction depends on an already-committed one, its binlog events must not start until that dependency has finished replaying on the replica; Otherwise you get escalating failures: a deadlock, then a broken replication channel, and in the worst case data inconsistency between primary and replica. Take this example:&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;k&quot;&gt;VALUES&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;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;c1&quot;&gt;-- large transaction&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;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE&lt;/code&gt; is the large transaction, and it must not begin until the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INSERT&lt;/code&gt; has finished replaying. If the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE&lt;/code&gt; runs first, it fails when updating the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pk1&lt;/code&gt; row because that row doesn’t exist yet.&lt;/p&gt;

&lt;p&gt;BRR uses a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed snapshot&lt;/code&gt; to enforce these ordering dependencies. When a DDL or large transaction starts on the primary, the primary’s current &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed&lt;/code&gt; captures every preceding transaction it saw. Once the replica’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed&lt;/code&gt; has caught up to that value (that is, is a superset of it), all the transactions this one depends on have been replayed on the replica, and it is safe to start applying it.&lt;/p&gt;

&lt;p&gt;To do this, BRR adds a new event type, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_gtid_executed_log_event&lt;/code&gt;, whose body holds a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed&lt;/code&gt; set. At specific moments the primary takes a gtid_executed snapshot and writes it to the BRR channel; when a replica Brr Worker reads the snapshot, it waits for all the GTIDs in it to finish before continuing.&lt;/p&gt;

&lt;h3 id=&quot;realtime-replication-of-large-transactions&quot;&gt;Realtime Replication of Large Transactions&lt;/h3&gt;

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

&lt;h4 id=&quot;creating-and-updating-a-brr_trx&quot;&gt;Creating and Updating a Brr_trx&lt;/h4&gt;

&lt;p&gt;When a transaction runs on the primary, its binlog events go first into the Binlog Cache (an in-memory buffer backed by a temporary file). In MySQL, once the Binlog Cache fills its in-memory buffer, it spills to the temporary file.&lt;/p&gt;

&lt;p&gt;BRR hooks in here: after each batch of events is written to the Binlog Cache, it checks the temporary file’s size. Once the file exceeds a certain size, BRR creates a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx&lt;/code&gt;, records the temporary file name and the current readable position, and registers it with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx_manager&lt;/code&gt;. From then on, every append to the Binlog Cache updates the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx&lt;/code&gt;’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;end_position&lt;/code&gt; and wakes the Dump thread to send those events to the replica.&lt;/p&gt;

&lt;h4 id=&quot;transmitting-binlog-events&quot;&gt;Transmitting Binlog Events&lt;/h4&gt;

&lt;p&gt;Before sending each batch of binlog events, the Dump thread emits a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_gtid_executed_log_event&lt;/code&gt; as that batch’s dependency snapshot, then sends the batch itself.&lt;/p&gt;

&lt;h4 id=&quot;committing-the-transaction&quot;&gt;Committing the Transaction&lt;/h4&gt;

&lt;p&gt;For a large transaction, the binlog events sit in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt; temporary file — not yet relay log — until the Brr Worker reads the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_log_event&lt;/code&gt;. When the primary finally commits, it sends the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_log_event&lt;/code&gt; over the BRR channel, and the IO thread does two things:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Renames the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt; temporary file into a relay log file. Based on the GTID, the primary’s Dump thread then skips sending this transaction, so its events aren’t shipped again as an ordinary transaction.&lt;/li&gt;
  &lt;li&gt;Notifies the Brr Worker to read the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_log_event&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Xid_log_event&lt;/code&gt; and complete the commit.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;rolling-back-the-transaction&quot;&gt;Rolling Back the Transaction&lt;/h4&gt;

&lt;p&gt;The rollback path is straightforward: when the primary rolls back, it sends a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BRR_ROLLBACK_EVENT&lt;/code&gt; over the BRR channel; on receiving it, the replica’s IO thread sends a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KILL_QUERY&lt;/code&gt; signal to the corresponding Brr Worker. The Brr Worker detects &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KILL_QUERY&lt;/code&gt;, rolls back the current transaction, cleans up, and moves on to the next &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note that after being killed, a Brr Worker neither exits nor propagates the error to the SQL thread — unlike an ordinary Worker, which must halt all replication on an error. The reason: for an ordinary Worker the transaction has already committed on the primary, so if the replica gives up, the two diverge. A Brr Worker’s transaction, by contrast, runs concurrently with the primary, so a primary rollback is the normal path and the replica must roll back as well.&lt;/p&gt;

&lt;h3 id=&quot;realtime-application-of-ddl&quot;&gt;Realtime Application of DDL&lt;/h3&gt;

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

&lt;h4 id=&quot;creating-a-brr_trx&quot;&gt;Creating a Brr_trx&lt;/h4&gt;

&lt;p&gt;For large transactions, we decide whether a transaction is “large” by the total size of its binlog events in the Binlog Cache. DDL is trickier: some DDLs only touch metadata and finish almost instantly, while for DDLs that touch data the run time depends on how much data is involved and is hard to estimate accurately. So instead of predicting a DDL’s run time up front, we decide whether to realtime-replicate it &lt;em&gt;by whether its execution exceeds a timeout.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every DDL creates a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx&lt;/code&gt;, but that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx&lt;/code&gt; isn’t sent to the replica right away. A DDL’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx&lt;/code&gt; has a threshold — 1000 ms by default — and only when the DDL’s run time exceeds it does the Dump thread start sending the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx&lt;/code&gt;. If a DDL finishes quickly, within one second, its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx&lt;/code&gt; is silently discarded and the DDL ships to the replica over the ordinary binlog channel, exactly as if BRR were off.&lt;/p&gt;

&lt;p&gt;A DDL’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_trx&lt;/code&gt; is created during the DDL’s Prepare phase — that is, &lt;em&gt;after the DDL has acquired the MDL X lock&lt;/em&gt; — because only with the X lock does the DDL have permission to operate on the table. Any conflicting operations have either already committed or must wait until the DDL releases the X lock or finishes.&lt;/p&gt;

&lt;h3 id=&quot;two-gtid_executed-snapshots&quot;&gt;Two gtid_executed Snapshots&lt;/h3&gt;

&lt;p&gt;An Online DDL runs in three phases: &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;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Commit&lt;/code&gt;. After Prepare, the MDL &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X lock&lt;/code&gt; is downgraded to an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;S lock&lt;/code&gt;, so during Execut, DML and DDL can run in parallel. During Commit, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;S lock&lt;/code&gt; is upgraded back to an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X lock&lt;/code&gt;; regaining the X lock means all those parallel DMLs have already committed. The replica must honor the same rule: those committed DMLs have to finish replaying before the replica can enter the Commit phase.&lt;/p&gt;

&lt;p&gt;So realtime replication of an Online DDL has two points on the replica that must be synchronized: one before entering Prepare, and one before entering Commit. Correspondingly, the primary takes two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed&lt;/code&gt; snapshots — one after the DDL enters Prepare, and one after it enters Commit.&lt;/p&gt;

&lt;h3 id=&quot;shipping-binlog-events-twice&quot;&gt;Shipping Binlog Events Twice&lt;/h3&gt;

&lt;p&gt;In the large-transaction section we saw that a large transaction is transmitted to the replica via BRR, and the copy in the binlog file is not shipped again. DDL is different: it ships twice — &lt;em&gt;once over BRR, and again as the binlog events in the binlog file.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A DDL’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Query_log_event&lt;/code&gt; is tiny, so shipping it twice costs almost nothing. Shipping it only once would force us into the large-transaction rename logic (renaming the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt; temporary file into relay log), with all its edge cases. For DDL, simply shipping it twice and discarding the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Brr_cache&lt;/code&gt; afterward is the simplest approach.&lt;/p&gt;

&lt;p&gt;As for ordering, the Dump thread guarantees BRR events ship before ordinary events. That way the Brr Worker is sure to get the DDL first and start executing it; by the time the ordinary events reach the relay log, the Brr Worker is already applying the DDL.&lt;/p&gt;

&lt;p&gt;When an ordinary Worker reads the DDL from the relay log, it checks whether the GTID is in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;owned_gtids&lt;/code&gt;. If it is (a Brr Worker is executing it), the ordinary Worker waits; once the Brr Worker commits, the GTID is added into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed&lt;/code&gt;. The ordinary Worker wakes and finds the GTID already in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed&lt;/code&gt;, so it skips the whole DDL.&lt;/p&gt;

&lt;p&gt;If the Brr Worker rolled the DDL back, the GTID is removed from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;owned_gtids&lt;/code&gt; and never added to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtid_executed&lt;/code&gt;. The ordinary Worker then wakes and sees the transaction wasn’t executed. It runs the DDL normally — &lt;em&gt;the fallback path, equivalent to running with BRR off.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;AliSQL’s Binlog Realtime Replication tackles the thorniest lag in MySQL binlog replication — lag from large transactions and DDL — by executing on the primary and replica in parallel. On top of that, we’ve made optimizations for the writeset mechanism, for massively concurrent workloads, and for the medium-sized transactions that batch jobs produce. Together, these have eliminated 95% of the replication lag in our production environment.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title type="html">Binlog Transmission Optimization for Large MySQL Transactions</title>
    <link href="https://songlibing.github.io/posts/mysql-large-transaction-binlog-transmission-en/" rel="alternate" type="text/html" title="Binlog Transmission Optimization for Large MySQL Transactions"/>
    <published>2026-07-16T10:00:00+08:00</published>
    <updated>2026-07-21T12:42:07+08:00</updated>
    <id>https://songlibing.github.io/posts/mysql-large-transaction-binlog-transmission-en/</id>
    <author>
      <name>宋利兵 (Libing Song)</name>
    </author>
    <category term="MySQL"/>
    <category term="Replication"/>
    <category term="Binlog"/>
    <category term="Large Transaction"/>
    <category term="Semisync"/>
    <summary type="html">This article is also available in Chinese: 中文版. Browse all English articles. Large transactions are a notorious problem in MySQL: they cause not only replication lag but also stability problems. A previous article, MySQL Large Transaction Commit Optimization, covered the problems a large transaction causes at commit time and the optimizations we made in AliSQL. This article looks at the...</summary>
    <content type="html" xml:base="https://songlibing.github.io/posts/mysql-large-transaction-binlog-transmission-en/">&lt;blockquote class=&quot;prompt-tip&quot;&gt;
  &lt;p&gt;This article is also available in Chinese: &lt;a href=&quot;https://songlibing.github.io/posts/mysql-large-transaction-binlog-transmission/&quot;&gt;中文版&lt;/a&gt;. Browse &lt;a href=&quot;https://songlibing.github.io/english/&quot;&gt;all English articles&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Large transactions are a notorious problem in MySQL: they cause not only replication lag but also stability problems. A previous article, &lt;em&gt;&lt;a href=&quot;https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization-en&quot;&gt;MySQL Large Transaction Commit Optimization&lt;/a&gt;&lt;/em&gt;, covered the problems a large transaction causes at commit time and the optimizations we made in AliSQL. This article looks at the problems a large transaction causes during semi-synchronous replication, and how AliSQL solves them.&lt;/p&gt;

&lt;p&gt;In &lt;em&gt;&lt;a href=&quot;https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization-en&quot;&gt;MySQL Large Transaction Commit Optimization&lt;/a&gt;&lt;/em&gt; we noted that writing the binlog when a large transaction commits can produce strange slow queries like these:&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;An &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INSERT&lt;/code&gt; that normally runs in an instant took &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.3s&lt;/code&gt;, yet the slow-query log shows no long lock wait.&lt;/li&gt;
  &lt;li&gt;Every statement in a multi-statement transaction had already finished, yet the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COMMIT&lt;/code&gt; alone took &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.3s&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Besides writing the binlog at commit, transmitting a large transaction’s binlog during semi-synchronous replication produces the same symptom. Below is a simulated test: we used sysbench &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oltp_write_only&lt;/code&gt; to simulate a normal write workload, then in the background, a transaction that generated 2 GB of binlog events (with the large-transaction commit optimization already applied). When the large transaction commits, writes drop to zero and don’t recover until semisync times out.&lt;/p&gt;

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

&lt;h2 id=&quot;root-cause&quot;&gt;Root Cause&lt;/h2&gt;

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

&lt;p&gt;The figure above shows the commit flow of a transaction under semi-synchronous replication:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;On commit, the transaction runs two-phase commit, starting with Prepare.&lt;/li&gt;
  &lt;li&gt;It then writes its binlog events to the binlog file.&lt;/li&gt;
  &lt;li&gt;After writing the binlog, it waits for its binlog events to be sent to the replica (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after_sync&lt;/code&gt; mode).&lt;/li&gt;
  &lt;li&gt;The binlog Dump thread then sends the transaction’s binlog events to the replica.&lt;/li&gt;
  &lt;li&gt;The replica’s IO thread receives these events and writes them into the relay log file.&lt;/li&gt;
  &lt;li&gt;Once it has the complete transaction, the IO thread sends the primary an acknowledgment saying it has all of the transaction’s binlog events. The ack is expressed as a binlog file name and offset. In the figure, Trx_n’s binlog end offset is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;530&lt;/code&gt;, so the replica’s IO thread sends &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;master-bin.000001:530&lt;/code&gt; to the primary, meaning every transaction before &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;master-bin.000001:530&lt;/code&gt; has been received.&lt;/li&gt;
  &lt;li&gt;On the primary, the Semisync Ack Receiver thread receives the ack and, based on the offset, wakes the corresponding transaction.&lt;/li&gt;
  &lt;li&gt;Once woken, the transaction finishes committing and returns OK to the user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is only one Dump thread between the primary and the replica, and it transmits binlog events in the order they were written to the binlog. The replica’s IO thread likewise writes received events into the relay log in that same order before acknowledging the primary. So a later transaction can’t be sent until the earlier one has finished. If the current transaction has a huge number of binlog events, sending them takes a very long time, and a later transaction — however small — has to wait. That wait includes not just its own transmission time but the large transaction’s ahead of it. Hence the slow-log symptom: a small transaction suddenly becomes very slow.&lt;/p&gt;

&lt;p&gt;To cope with this, MySQL provides the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rpl_semi_sync_master_timeout&lt;/code&gt; parameter, which sets how long a transaction waits for an ack; once the wait exceeds &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rpl_semi_sync_master_timeout&lt;/code&gt;, replication automatically falls back to asynchronous. We can set this to a small value to avoid the severe case where a large transaction makes the whole instance unwritable.&lt;/p&gt;

&lt;h2 id=&quot;an-rpo--0-design-based-on-semi-synchronous-replication&quot;&gt;An RPO = 0 Design Based on Semi-Synchronous Replication&lt;/h2&gt;

&lt;p&gt;Because a transaction under semi-synchronous replication can’t commit until its binlog has been replicated to a replica, it’s natural to think of using semisync to build an RPO = 0 (zero data loss) consistency solution.&lt;/p&gt;

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

&lt;p&gt;This architecture needs two replicas, and semisync guarantees that a transaction commits only after it receives an ack from at least one of them.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;If the primary crashes, the data has been replicated to at least one replica.&lt;/li&gt;
  &lt;li&gt;If one replica becomes unavailable, cluster availability is unaffected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To guarantee RPO = 0, semisync must never fall back to async. MySQL semisync has two points where it can degrade to async:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;After a crash and restart, transactions already written to the binlog are committed automatically, even though they may not yet have been replicated to a replica.&lt;/li&gt;
  &lt;li&gt;Once the wait reaches &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rpl_semi_sync_master_timeout&lt;/code&gt;, it degrades to async.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The former can’t be controlled from outside — it requires changing MySQL’s code. The latter requires setting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rpl_semi_sync_master_timeout&lt;/code&gt; to a very large value so semisync never degrades. Large transactions are clearly the thorniest issue in an RPO = 0 design: the moment one appears, it makes the whole cluster unwritable, so the design must take countermeasures. A DBA with strong influence over the application can arrange for it to avoid large transactions; but at a large company, with sprawling and complex applications, eliminating them entirely is hard, and an RDS provider has no control over its users at all. In practice, availability usually matters far more than consistency, so many designs adopt a temporary-degradation strategy, falling back to async whenever a large transaction appears.&lt;/p&gt;

&lt;h2 id=&quot;realtime-transmission-of-large-transactions&quot;&gt;Realtime Transmission of Large Transactions&lt;/h2&gt;

&lt;p&gt;In AliSQL we designed a realtime-transmission mechanism to solve the problems large-transaction transmission causes; with it, there is no need to degrade semisync to async.&lt;/p&gt;

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

&lt;p&gt;The realtime large-transaction transmission mechanism reads a transaction’s binlog events out of the Binlog Cache temporary file and sends them to the replica while the transaction is still doing DML. The key steps:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;During DML execution, once the binlog events of a transaction has produced exceed a certain amount, the transaction is registered in the large-transaction list and handled as a large transaction.&lt;/li&gt;
  &lt;li&gt;Based on that list, the binlog Dump thread reads the large transaction’s binlog temporary file and sends its contents to the replica. The large transaction’s binlog events and the events from the binlog file are sent interleaved, with flow control on the large transaction: events from the binlog file take priority, so the transaction currently committing is unaffected.&lt;/li&gt;
  &lt;li&gt;The large transaction’s binlog events carry a special marker and extra information. When the replica’s IO thread receives them, it stores them in a temporary file called the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Relay Log Cache&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;At commit, once the Dump thread has sent all the binlog events, it sends a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_event&lt;/code&gt; to the replica.&lt;/li&gt;
  &lt;li&gt;On receiving the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_event&lt;/code&gt;, the replica knows it has all of the transaction’s binlog events, and it turns the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Relay Log Cache&lt;/code&gt; into a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Relay Log&lt;/code&gt; file.&lt;/li&gt;
  &lt;li&gt;When several large transactions run at once, the mechanism can transmit them all in real time simultaneously.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From these steps we can see: &lt;em&gt;a large transaction’s binlog events are sent to the replica bit by bit as they are produced, so at commit only the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_event&lt;/code&gt; needs to be sent.&lt;/em&gt; The amount of data sent at commit is therefore tiny, and it no longer blocks other transactions’ binlog-event transmission. It also removes the sudden burst of network traffic, reducing congestion.&lt;/p&gt;

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

&lt;p&gt;The realtime-transmission mechanism follows directly from the large-transaction commit optimization and reuses parts of its implementation. A transaction’s binlog events are produced and accumulate during DML execution; once they exceed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;binlog_cache_size&lt;/code&gt;, they are written to a temporary file, and at commit they are written to the binlog file all at once. In &lt;em&gt;MySQL Large Transaction Commit Optimization&lt;/em&gt;, a large transaction’s temporary file is automatically turned into a new binlog file, which eliminates the problems that large-transaction commit causes.&lt;/p&gt;

&lt;p&gt;Realtime large-transaction transmission reuses this logic, reserving some space at the head of the Relay Log Cache. When the Relay Log Cache is turned into a Relay Log file, that head space is filled with the special binlog events a relay log needs, such as the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Format_description_event&lt;/code&gt;.&lt;/p&gt;

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

&lt;h3 id=&quot;handling-failures&quot;&gt;Handling Failures&lt;/h3&gt;

&lt;p&gt;A large transaction runs for a long time, so any failure along the way has to be handled.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;If the large transaction rolls back on the primary, the binlog Dump thread sends a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rollback&lt;/code&gt; to the replica; on receiving it, the IO thread destroys the corresponding Relay Log Cache.&lt;/li&gt;
  &lt;li&gt;If the IO thread’s connection to the primary drops, or a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;STOP SLAVE&lt;/code&gt; is issued, the IO thread destroys all Relay Log Caches. After reconnecting, it restarts realtime replication of the large transaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;results&quot;&gt;Results&lt;/h2&gt;

&lt;p&gt;We used sysbench &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oltp_write_only&lt;/code&gt; to simulate a normal write workload, then committed, in the background, a transaction that generated 2 GB of binlog events. The results:&lt;/p&gt;

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

&lt;p&gt;With realtime replication, the application’s writes run smoothly, with no more drops to zero.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;In MySQL’s semi-synchronous replication architecture, large transactions are a classic problem. To keep them from destabilizing the instance, people have had to work hard to eliminate large transactions from their applications, or simply let replication degrade to async. &lt;em&gt;Realtime large-transaction transmission&lt;/em&gt; moves the transmission of a large transaction’s binlog events from the commit phase up to the execution phase, sending each event to the replica as soon as it is produced. This avoids blocking other transactions’ binlog-event transmission for a long time at commit, and avoids network congestion. When a large transaction comes along, semisync no longer needs to degrade to async — clearing away a thorny obstacle on the path to a semisync-based RPO = 0 design.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title type="html">Commit Optimization for Large MySQL Transactions</title>
    <link href="https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization-en/" rel="alternate" type="text/html" title="Commit Optimization for Large MySQL Transactions"/>
    <published>2026-07-15T10:00:00+08:00</published>
    <updated>2026-07-21T13:09:02+08:00</updated>
    <id>https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization-en/</id>
    <author>
      <name>宋利兵 (Libing Song)</name>
    </author>
    <category term="MySQL"/>
    <category term="Binlog"/>
    <category term="Large Transaction"/>
    <category term="Commit Optimization"/>
    <summary type="html">This article is also available in Chinese: 中文版. Browse all English articles. If you use and operate MySQL, you’ve surely run into a strange slow query like this: An INSERT that’s normally instant took 1.3s, and the slow-query log shows no long lock wait. Every statement in a multi-statement transaction had already finished, yet the COMMIT alone took 1.3s. When...</summary>
    <content type="html" xml:base="https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization-en/">&lt;blockquote class=&quot;prompt-tip&quot;&gt;
  &lt;p&gt;This article is also available in Chinese: &lt;a href=&quot;https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization/&quot;&gt;中文版&lt;/a&gt;. Browse &lt;a href=&quot;https://songlibing.github.io/english/&quot;&gt;all English articles&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you use and operate MySQL, you’ve surely run into a strange slow query like this:&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;An &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INSERT&lt;/code&gt; that’s normally instant took &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.3s&lt;/code&gt;, and the slow-query log shows no long lock wait.&lt;/li&gt;
  &lt;li&gt;Every statement in a multi-statement transaction had already finished, yet the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COMMIT&lt;/code&gt; alone took &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.3s&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When this happens, the most likely cause is a large transaction committing. Below is a simulated test: we used sysbench to simulate a normal workload, then ran a large UPDATE in the background every 5 seconds. You can see the large UPDATE severely hurts performance.&lt;/p&gt;

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

&lt;h2 id=&quot;root-cause&quot;&gt;Root Cause&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;The figure above shows the execution of two transactions:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A transaction runs in two phases: an execution phase and a commit phase.&lt;/li&gt;
  &lt;li&gt;During execution, when a statement updates data it generates binlog events. These are stored in the Binlog Cache, which has two parts: an in-memory buffer and a temporary file. When the buffer fills up, the events are written to the temporary file.&lt;/li&gt;
  &lt;li&gt;At commit, all the binlog events in the Binlog Cache are copied into the binlog file.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Writing binlog events to the binlog file must be serialized — one transaction can’t do it until the previous one has finished.&lt;/strong&gt; So while &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_n&lt;/code&gt; is writing to the binlog file, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_m&lt;/code&gt; has to wait.&lt;/li&gt;
  &lt;li&gt;In the figure, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_n&lt;/code&gt; is a large transaction that produced a lot of binlog events. &lt;em&gt;The time to copy binlog events into the binlog file is linear in the size of the events the transaction produced — the more events, the longer the copy takes.&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_m&lt;/code&gt; is a small transaction. Even though its execution phase finished quickly, at commit it runs into the large transaction &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_n&lt;/code&gt; committing, so it must wait for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_n&lt;/code&gt; to finish copying its binlog events before it can proceed. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_m&lt;/code&gt; spends most of its commit phase waiting for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Trx_n&lt;/code&gt; to write the binlog file — and that’s why the small transaction becomes slow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;how-serious-the-problem-is&quot;&gt;How Serious the Problem Is&lt;/h2&gt;

&lt;p&gt;As our simulated test shows, committing a large transaction has a major impact on workload stability. In real-world scenarios it can be far worse, and it’s common.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A GB-scale transaction can make the instance unwritable for a long time. Since storage IO bandwidth is fixed, the time to write a large transaction’s binlog depends on the transaction’s size. The largest transaction we’ve seen in production produced &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;104 GB&lt;/code&gt; of binlog events.&lt;/li&gt;
  &lt;li&gt;A GB-scale transaction can push IO throughput up and slow it down, or even saturate IO, which also slows queries.&lt;/li&gt;
  &lt;li&gt;A few-hundred-MB transaction won’t cause a long outage, but it can still add hundreds of milliseconds to application DML. For latency-sensitive workloads, even that may be unacceptable.&lt;/li&gt;
  &lt;li&gt;On top of this, all of the above can raise the number of active connections. If those active connections aren’t cleared in time, CPU spikes, and it can turn into a vicious cycle — eventually an avalanche and a much bigger problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;optimizing-how-large-transactions-write-the-binlog&quot;&gt;Optimizing How Large Transactions Write the Binlog&lt;/h2&gt;

&lt;p&gt;In AliSQL we optimized how a large transaction writes the binlog, completely eliminating the stability impact of large-transaction commit. RDS 5.7 and RDS 8.0 both enable this optimization by default. Last year we contributed it to MariaDB, and the feature shipped in MariaDB 11.7&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;h3 id=&quot;the-approach&quot;&gt;The Approach&lt;/h3&gt;

&lt;p&gt;Here is the implementation in MariaDB 11.7. MySQL and MariaDB have diverged quite a bit in code, but the underlying logic — and therefore the approach — is the same.&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;The idea is simple and clean: since the Binlog Cache has already written the binlog events to a file, we just &lt;em&gt;rename that file directly into a binlog file.&lt;/em&gt; This avoids copying the binlog events, so there is no extra IO. And a rename takes constant time regardless of the Binlog Cache’s size, which fully solves the large-transaction problem. Let’s look at the implementation.&lt;/p&gt;

&lt;h3 id=&quot;the-binlog_cache_files-directory&quot;&gt;The #binlog_cache_files Directory&lt;/h3&gt;

&lt;p&gt;The Binlog Cache’s file is a system temporary file, which can’t be renamed into a regular file directly. So we create a directory, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#binlog_cache_files&lt;/code&gt;, in the binlog directory; the file the Binlog Cache creates then becomes a regular file in this directory instead of a system temp file.&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;reserving-head-space&quot;&gt;Reserving Head Space&lt;/h3&gt;

&lt;p&gt;The Binlog Cache file contains only the transaction’s binlog events. To turn it into a binlog file, we need to reserve some space for the binlog header events, such as the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Format_description_event&lt;/code&gt;.&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;The reserved space is 4 KB-aligned, so at least 4 KB is reserved, which is enough in most cases. But in some situations the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_list_log_event&lt;/code&gt; (similar to MySQL’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Previous_gtids_event&lt;/code&gt;, recording the GTID set generated before this binlog) can be very large. To keep the feature usable in that case, when generating a new binlog file we adjust the reserved space based on how much the header events actually occupy; the Binlog Cache file’s reserved space is then adjusted when the next transaction begins. The binlog header events usually take less than 4 KB, so after writing them some space may be left over. How do we handle the leftover? Thanks to MariaDB’s mechanism of padding a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_log_event&lt;/code&gt; with trailing zeros, the leftover space is absorbed into the corresponding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_log_event&lt;/code&gt;. After the Binlog Cache file is turned into a binlog file, its structure looks like this:&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;the-rename-process&quot;&gt;The Rename Process&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;The rename works roughly as follows:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Persist the Binlog Cache file. At this point the rename hasn’t started, so it doesn’t block other transactions from committing.&lt;/li&gt;
  &lt;li&gt;Perform a rotate: close the current binlog file and create a new one.&lt;/li&gt;
  &lt;li&gt;Copy the new file’s header into the head of the Binlog Cache.&lt;/li&gt;
  &lt;li&gt;Generate the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gtid_log_event&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Delete the newly generated binlog file, and rename the Binlog Cache file into the new binlog file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;results&quot;&gt;Results&lt;/h2&gt;

&lt;p&gt;Again we used sysbench to simulate a workload, then ran a large UPDATE in the background every 5 seconds, each producing 512 MB of binlog events. The results:&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;With the large-transaction commit optimization, sysbench’s TPS is fairly steady, with no violent swings. There’s still a small dip every 5 seconds, but that comes from the large UPDATE itself using some CPU, not from transaction commit.&lt;/p&gt;

&lt;p&gt;We also simulated the DML latency caused by transactions of different sizes. The results:&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;Without the optimization, once a large transaction exceeds 64 MB, sysbench’s max latency starts to climb noticeably, and rises rapidly as the transaction grows.&lt;/li&gt;
  &lt;li&gt;With the optimization on, sysbench’s max latency stays stable no matter how large the transaction, holding at normal workload levels. At 1024 GB, one extra binlog rotate adds a slight bump in latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;In MySQL’s binlog replication architecture, large transactions are a classic trigger for trouble, causing stability and replication-lag problems. By renaming the Binlog Cache’s temporary file directly into a binlog file, we avoid copying binlog events and eliminate the extra IO, keeping large-transaction commit fast and stable — and fully resolving the various stability problems that large-transaction commit causes.&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
      &lt;p&gt;MariaDB 11.7 — &lt;a href=&quot;https://mariadb.com/resources/blog/binlog-commit-optimization-for-large-transaction/&quot;&gt;Binlog Commit Optimization for Large Transactions&lt;/a&gt; &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content>
  </entry>
</feed>
