前面介紹的MySQL 的主從復制流程如下所示:

主備延遲的主要原因在于,master A 上產(chǎn)生 binlog 的速度大于slave B 處理 binlog 的速度。數(shù)據(jù)的積壓就在于 sql_thread 處理的速度。在 MySQL 5.6 版本之前,只支持單線程復制。單線程的 binlog 復制,在高并發(fā)的場景下會出現(xiàn)嚴重的主從不一致。要解決這個問題,就需要將上面的 sql_thread 拆解成為多個線程處理。

上圖中的 coordinate 就是本文開始的 sql_thread,具體執(zhí)行 binlog 復制的是 Worker。生產(chǎn)上,Worker 的個數(shù)設置為 8-16 個比較合適(32核CPU),因為從庫還需要可能還需要處理讀的請求。
對于 coordinate 的分發(fā)策略,并不能是隨機的,因為這樣對于SQL 執(zhí)行不同的順序,可能會產(chǎn)生不同的結果。此時對于 coordinate 的分發(fā)策略要求如下:
不能造成更新覆蓋,同一行的更新必須被分發(fā)到同一個Worker 中;
同一個事務不能被拆開,必須被分配到同一個Worker 中。
按表分發(fā)策略
按表分發(fā)的基本思路是,如果兩個事務更新不同的表,就可以并行處理。此時因為在不同 Worker 處理時,也不會更新到同一行的數(shù)據(jù)。如果有跨表的事務,則需要 2 張表都需要考慮了。

從上圖可以看出,每個 Worker 都對應一個 hash 表,用于保存當前 Worker 執(zhí)行的 binlog 涉及的表。hash 表的 key 是 “庫名+表名”,value 表示 Worker 中有多少個事務操作這個表。當事務執(zhí)行完成后,其所涉及的表的計數(shù)會從hash 表中移除。上面 Worker_1 中的hash 表中 db1.t1:4 ,表示:Worker_1 中修改 db1.t1 表的事務數(shù)有 4 個。
假設 coordinate 讀取一個事務 T (涉及 t1 和 t3的改動),此時分配規(guī)則如下:
Worker_1 中有事務在處理 t1 的改動,此時和 Worker_1 是沖突的,對于Worker_2 有在處理 t3 的改動,此時和Worker_2 也是沖突的;
當事務 T 和多于 1 個Worker 沖突時,coordinate 就進入等待狀態(tài);
此時如果 Worker_2 中執(zhí)行完事務后,對應 t3 的計數(shù)就會減 1,此時事務 T 就只和 Worker_1 沖突了,此時就會將其加入到 Worker_1 的隊列中;
coordinate 讀取新事務 T2 繼續(xù)執(zhí)行 步驟 1 。
總結以上,coordinate 在分發(fā)事務的時候,會考慮以下沖突情況:
當沒有 Worker 沖突的時候,會將其加入到空間的Worker 中去;
當有只有 1 個Worker 沖突的時候,會將其加入到?jīng)_突的Worker 中去;
當有多于 1 個Worker 沖突的時候,coordinate 會進入等待狀態(tài),直到?jīng)_突數(shù) <= 1。
按行分發(fā)策略
按行分發(fā)的策略和按表分發(fā)的策略類似,但是在考慮Worker 沖突的時候?qū)膆ash key 就是“庫名+表名+唯一鍵的值”,原理類似,這里就不做過多的介紹了。這里需要注意的點是,按行進行沖突檢測,其消耗的計算量還是比較大的。
MySQL5.6 的并行復制
MySQL 從 5.6 版本開始支持按庫級別的并行復制。對于一個應用來說,如果我們按照業(yè)務分庫,從應用的層面上面講是提高了 binlog 的復制能力的。按庫級別的并行相比按表和按行級別的,有以下 2 個優(yōu)勢:
構造 Worker 的 hash 表很快,因為庫的個數(shù)不會太多,而且計算量也會減少;
不需要要求 binlog 的格式。
MariaDB 的并行復制
在之前介紹的 redo log 組提交時,有以下特點:
在一個組里提交的事務,一定不會修改同一行;
主庫上面可以并行的事務,在從庫上面也是可以并行的。
MariaDB 并行復制的實現(xiàn)上,操作流程如下:
在一個組里面提交的事務有一個相同的 commit_id,下一個組就是 commit_id + 1;
commit_id 直接寫到 binlog 里面;
傳到備庫的時候,相同 commit_id 的事務會被分發(fā)到不同的 Worker 中執(zhí)行;
這一組執(zhí)行完成后,再去取下一組重復以上步驟。
從上面流程可以看出,下一個組提交的執(zhí)行依賴上一個組提交的執(zhí)行完成。此時如果上一個組提交中有大事務,就會影響下一個組提交的執(zhí)行,容易造成阻塞。
MySQL5.7 的并行復制
MariaDB 在實現(xiàn)了并行復制能力之后,MySQL 也提供了類似的功能。由 slave-parallel-type 參數(shù)來控制并行復制的策略:
配置為 DATABASE,表示使用 MySQL 5.6 開始提供的按庫并行復制的策略;
配置為 LOGICAL_CLICK,表示就是使用類似于 MariaDB 并行復制策略。
對于 LOGICAL_CLICK 這種策略,MySQL 5.7 對其做了優(yōu)化。說優(yōu)化之前,我們先看一下之前提到的“事務兩階段提交的細化流程”。

上面提到的 MariaDB 并行復制的核心是:一個組內(nèi),已經(jīng)提交的事務是可以并行的。但是從上面流程可以看出,只要 redo log prepare (第一步)完成之后,事務之間就已經(jīng)完成沖突檢測了。因此 MySQL 5.7 的優(yōu)化思想如下:
同時處于 prepare 階段的事務是可以并行執(zhí)行的;
處于 prepare 階段的事務與處于 commit 狀態(tài)的事務之間,也是可以并行執(zhí)行的。
前面在 binlog 組提交的時候,介紹了下面 2 個參數(shù):
binlog_group_commit_sync_delay 參數(shù):表示延遲多少個微妙之后,再執(zhí)行 fsync;
binlog_group_commit_sync_no_delay_count 參數(shù):表示累計多少次之后,再執(zhí)行 fsync。
上面 2 個參數(shù)是提高組提交里面的事務批量,簡單的理解可以是:減慢主庫的 binlog 寫入,讓備庫能趕得上。
MySQL5.7.22 的并行復制
在 2018年4月發(fā)布的 5.7.22 版本里面,新增了基于 writeset 的并行復制。新增了
binlog-transaction-dependency-tracking,用來控制是否啟用這個新策略,這個參數(shù)有以下 3 個可選值:
COMMIT_ORDER:就是前面提到的處于 prepare 和 commit 狀態(tài)的 binlog 都可以被分發(fā)到 Worker 上面處理;
WRITESET:對于事務更新的每一行,都計算出一個 hash 值,組成集合 writeset。如果兩個事務的 writeset 沒有交集,說明事務沒有操作相同的行,事務之間是可以并行的;
WRITESET_SESSION:是在 WRITESET 的基礎上新增了一個約束,就是在主庫上面同一個線程執(zhí)行的 2 個事務的執(zhí)行順序,在從庫上面也需要保證順序性。
可以看出,MySQL 5.7.22 提出的并行復制策略和之前說的按表、按行的并行復制原理類似。另外其還有一些優(yōu)化點:
writeset 是在主庫上面生成后直接寫到 binlog 里面的,這樣在備庫執(zhí)行時,就需要解析 binlog 的內(nèi)容,節(jié)省了很多計算量;
不需要把整個事務的 binlog 都掃描一遍后,才決定分發(fā)到哪個 Worker,更節(jié)省內(nèi)存;
由于備庫的分發(fā)策略不依賴于 binlog 的內(nèi)容,因此對 binlog 的格式?jīng)]有要求。
當然上面所說的并行復制的前提都是,沒有外鍵約束,所有表都有主鍵的場景。如果不滿足,則會退化成單線程的模式。