Mysql之主從複製原理

1.主從複製步驟:

 具體步驟:

  • 1、從庫通過手工執行change master to 語句連接主庫,提供了連接的用戶一切條件(user 、password、port、ip),並且讓從庫知道,二進制日誌的起點位置(file名 position 號); start slave
  • 2、從庫的IO線程和主庫的dump線程建立連接。
  • 3、從庫根據change master to 語句提供的file名和position號,IO線程向主庫發起binlog的請求。
  • 4、主庫dump線程根據從庫的請求,將本地binlog以events的方式發給從庫IO線程。
  • 5、從庫IO線程接收binlog events,並存放到本地relay-log中,傳送過來的信息,會記錄到master.info中
  • 6、從庫SQL線程應用relay-log,並且把應用過的記錄到relay-log.info中,默認情況下,已經應用過的relay 會自動被清理purge

2. 如何查看同步延遲狀態

命令

show slave status\G;

有幾個參數比較重要:

master_log_file: slave中的IO線程正在讀取的主服務器二進制日誌文件的名稱

read_master_log_pos: 在當前的主服務器二進制日誌中,slave中的IO線程已經讀取的位置

relay_log_file: sql線程當前正在讀取和執行的中繼日誌文件的名稱

relay_log_pos: 在當前的中繼日誌中,sql線程已經讀取和執行的位置

relay_master_log_file: 由sql線程執行的包含多數近期事件的主服務器二進制日誌文件的名稱

slave_io_running: IO線程是否被啓動併成功的連接到主服務器上

slave_sql_running: sql線程是否被啓動

seconds_behind_master: 從屬服務器sql線程和從屬服務器IO線程之間的事件差距,單位以秒計
其中有一個最最重要的參數需要同學們引起注意,那就是seconds_behind_master,這個參數就表示當前備庫延遲了多長時間

3. 主從複製產生的原因

1.備庫的性能比主庫的差
2.備庫充當了寫庫,然後讓備庫查詢壓力過大,這樣也會影響到同步速度
3.大事務執行,比如一個事務執行了10秒,binlog寫入必須等待事務完成,纔會傳入備庫
4.從庫sql thread 從relay log中拉取數據的時候,是隨機的操作,不是順序操作,會影響到效率(優化點)
5.從庫在同步數據的同時,可能跟其他查詢的線程發生鎖搶佔的情況,此時也會發生延時
6.當主庫的TPS併發非常高的時候,產生的DDL數量超過了一個線程所能承受的範圍的時候,那麼也可能帶來延遲(優化點)
7.在進行binlog日誌傳輸的時候,如果網絡帶寬也不是很好,那麼網絡延遲也可能造成數據同步延遲

4. 如何解決複製延遲問題

4.1. 架構方面

1、業務的持久化層的實現採用分庫架構,讓不同的業務請求分散到不同的數據庫服務上,分散單臺機器的壓力

2、服務的基礎架構在業務和mysql之間加入緩存層,減少mysql的讀的壓力,但是需要注意的是,如果數據經常要發生修改,那麼這種設計是不合理的,因爲需要頻繁的去更新緩存中的數據,保持數據的一致性,導致緩存的命中率很低,所以此時就要慎用緩存了

3、使用更好的硬件設備,比如cpu,ssd等,但是這種方案一般對於公司而言不太能接受,原因也很簡單,會增加公司的成本,而一般公司其實都很摳門,所以意義也不大,但是你要知道這也是解決問題的一個方法,只不過你需要評估的是投入產出比而已。

4.2. 從庫配置方面

4.2.1 修改sync_binlog的參數的值

 可以看到,每個線程有自己的binlog cache,但是共用同一份binlog。

圖中的write,指的就是把日誌寫入到文件系統的page cache,並沒有把數據持久化到磁盤,所以速度快

圖中的fsync,纔是將數據持久化到磁盤的操作。一般情況下,我們認爲fsync才佔用磁盤的IOPS
而write和fsync的時機就是由參數sync_binlog來進行控制的。
1、當sync_binlog=0的時候,表示每次提交事務都只write,不fsync
2、當sync_binlog=1的時候,表示每次提交事務都執行fsync
3、當sync_binlog=N的時候,表示每次提交事務都write,但積累N個事務後才fsync。
一般在公司的大部分應用場景中,我們建議將此參數的值設置爲1,因爲這樣的話能夠保證數據的安全性,但是如果出現主從複製的延遲問題,可以考慮將此值設置爲100~1000中的某個數值,非常不建議設置爲0,因爲設置爲0的時候沒有辦法控制丟失日誌的數據量,但是如果是對安全性要求比較高的業務系統,這個參數產生的意義就不是那麼大了。
2、直接禁用salve上的binlog,當從庫的數據在做同步的時候,有可能從庫的binlog也會進行記錄,此時的話肯定也會消耗io的資源,因此可以考慮將其關閉,但是需要注意,如果你搭建的集羣是級聯的模式的話,那麼此時的binlog也會發送到另外一臺從庫裏方便進行數據同步,此時的話,這個配置項也不會起到太大的作用。
3、設置innodb_flush_log_at_trx_commit 屬性,這個屬性在我講日誌的時候講過,用來表示每一次的事務提交是否需要把日誌都寫入磁盤,這是很浪費時間的,一共有三個屬性值,分別是0(每次寫到服務緩存,一秒鐘刷寫一次),1(每次事務提交都刷寫一次磁盤),2(每次寫到os緩存,一秒鐘刷寫一次),一般情況下我們推薦設置成2,這樣就算mysql的服務宕機了,卸載os緩存中的數據也會進行持久化。

5. 從根本上解決主從複製延遲問題

通過上圖我們可以發現其實所謂的並行複製,就是在中間添加了一個分發的環節,也就是說原來的sql_thread變成了現在的coordinator組件,當日志來了之後,coordinator負責讀取日誌信息以及分發事務,真正的日誌執行的過程是放在了worker線程上,由多個線程並行的去執行。

-- 查看並行的slave的線程的個數,默認是0.表示單線程
show global variables like 'slave_parallel_workers';
-- 根據實際情況保證開啓多少線程
set global slave_parallel_workers = 4;
-- 設置併發複製的方式,默認是一個線程處理一個庫,值爲database
show global variables like '%slave_parallel_type%';
-- 停止slave
stop slave;
-- 設置屬性值
set global slave_parallel_type='logical_check';
-- 開啓slave
start slave
-- 查看線程數
show full processlist;

通過上述的配置可以完成我們說的並行複製,但是此時你需要思考幾個問題

1、在並行操作的時候,可能會有併發的事務問題,我們的備庫在執行的時候可以按照輪訓的方式發送給各個worker嗎?

答案是不行的,因爲事務被分發給worker以後,不同的worker就開始獨立執行了,但是,由於CPU的不同調度策略,很可能第二個事務最終比第一個事務先執行,而如果剛剛好他們修改的是同一行數據,那麼因爲執行順序的問題,可能導致主備的數據不一致。

2、同一個事務的多個更新語句,能不能分給不同的worker來執行呢?

答案是也不行,舉個例子,一個事務更新了表t1和表t2中的各一行,如果這兩條更新語句被分到不同worker的話,雖然最終的結果是主備一致的,但如果表t1執行完成的瞬間,備庫上有一個查詢,就會看到這個事務更新了一半的結果,破壞了事務邏輯的隔離性。

我們通過講解上述兩個問題的最主要目的是爲了說明一件事,就是coordinator在進行分發的時候,需要遵循的策略是什麼?

1、不能造成更新覆蓋。這就要求更新同一行的兩個事務,必須被分發到同一個worker中。

2、同一個事務不能被拆開,必須放到同一個worker中。

聽完上面的描述,我們來說一下具體實現的原理和過程。

如果讓我們自己來設計的話,我們應該如何操作呢?這是一個值得思考的問題。其實如果按照實際的操作的話,我們可以按照粒度進行分類,分爲按庫分發,按表分發,按行分發。

其實不管按照什麼方式進行分發,大家需要注意的就是在分發的時候必須要滿足我們上面說的兩條規則,所以當我們進行分發的時候要在每一個worker上定義一個hash表,用來保存當前這個work正在執行的事務所涉及到的表。hash表的key值按照不同的粒度需要存儲不同的值:

按庫分發:key值是數據庫的名字,這個比較簡單

按表分發:key值是庫名+表名

按行分發:key值是庫名+表名+唯一鍵

5.2 mariaDB的並行複製策略

在mysql5.7的時候採用的是基於組提交的並行複製,換句話說,slave服務器的回放與主機是一致的,即主庫是如何並行執行的那麼slave就如何怎樣進行並行回放,這點其實是參考了mariaDB的並行複製,下面我們來看下其實現原理。

mariaDB的並行複製策略利用的就是這個特性:

1、能夠在同一組裏提交的事務,一定不會修改同一行;

2、主庫上可以並行執行的事務,備庫上也一定是可以並行執行的。

在實現上,mariaDB是這麼做的:

1、在一組裏面一起提交的事務,有一個相同的commit_id,下一組就是commit_id+1;

2、commit_id直接寫到binlog裏面;

3、傳到備庫應用的時候,相同commit_id的事務會分發到多個worker執行;

4、這一組全部執行完成後,coordinator再去取下一批。

這是mariaDB的並行複製策略,大體上看起來是沒有問題的,但是你仔細觀察的話會發現他並沒有實現“真正的模擬主庫併發度”這個目標,在主庫上,一組事務在commit的時候,下一組事務是同時處於“執行中”狀態的。

我們真正想要達到的並行複製應該是如下的狀態,也就是說當第一組事務提交的是,下一組事務是運行的狀態,當第一組事務提交完成之後,下一組事務會立刻變成commit狀態。

 但是按照mariaDB的並行複製策略,那麼備庫上的執行狀態會變成如下所示:

 可以看到,這張圖跟上面這張圖的最大區別在於,備庫上執行的時候必須要等第一組事務執行完成之後,第二組事務才能開始執行,這樣系統的吞吐量就不夠了。而且這個方案很容易被大事務拖後腿,如果trx2是一個大事務,那麼在備庫應用的時候,trx1和trx3執行完成之後,就只能等trx2完全執行完成,下一組才能開始執行,這段時間,只有一個worker線程在工作,是對資源的浪費。

5.3 mysql5.7的並行複製策略

mysql5.7版本的時候,根據mariaDB的並行複製策略,做了相應的優化調整後,提供了自己的並行複製策略,並且可以通過參數slave-parallel-type來控制並行複製的策略:

1、當配置的值爲DATABASE的時候,表示使用5.6版本的按庫並行策略;

2、當配置的值爲LOGICAL_CLOCK的時候,表示跟mariaDB相同的策略。
此時,大家需要思考一個問題:同時處於執行狀態的所有事務,是否可以並行?

答案是不行的,因爲多個執行中的事務是有可能出現鎖衝突的,鎖衝突之後就會產生鎖等待問題。
在mariaDB中,所有處於commit狀態的事務是可以並行,因爲如果能commit的話就說明已經沒有鎖的問題,但是大家回想下,我們mysql的日誌提交是兩階段提交,如下圖,其實只要處於prepare狀態就已經表示沒有鎖的問題了。

 因此,mysql5.7的並行複製策略的思想是:

1、同時處於prepare狀態的事務,在備庫執行是可以並行的。

2、處於prepare狀態的事務,與處於commit狀態的事務之間,在備庫上執行也是可以並行的。

基於這樣的處理機制,我們可以將大部分的日誌處於prepare狀態,因此可以設置

1、binlog_group_commit_sync_delay 參數,表示延遲多少微秒後才調用 fsync;

2、binlog_group_commit_sync_no_delay_count 參數,表示累積多少次以後才調用 fsync。 

6 mysql 組提交的概念

組提交(group commit)是mysql處理日誌的一種優化方式,主要爲了解決寫日誌時頻繁刷磁盤的問題。組提交伴隨着mysql的發展,已經支持了redo log和bin log的組提交。

6.1 redo logo的組提交

組提交思想是,將多個事務redo log的刷盤動作合併,減少磁盤順序寫。Innodb的日誌系統裏面,每條redo log都有一個LSN(Log Sequence Number),LSN是單調遞增的。每個事務執行更新操作都會包含一條或多條redo log,各個事務將日誌拷貝到log_sys_buffer時(log_sys_buffer 通過log_mutex保護),都會獲取當前最大的LSN,因此可以保證不同事務的LSN不會重複。

假設現在有三個併發事務(tx1,tx2,tx3),這三個事務所對應的LSN的值分別是50,120,160。

 從圖中可以看到:

1、trx1是第一個到達的,會被選爲這組的leader;

2、等trx1要開始寫盤的時候,這個組裏面已經有了三個事務,這時候LSN也變成了160,;

3、trx1去寫盤的時候,帶的就是LSN=160,因此等trx1返回時,所有LSN小於等於160的redo log都已經被持久化到磁盤;

4、這時候trx2和trx3就可以直接返回了

因此,在一個組提交裏面,組員越多,節約磁盤IOPS的效果就越好。但如果只有單線程壓測,那就只能老老實實地一個事務對應一次持久化操作了。

6.2 bin log的組提交

在之前的版本中,mysql的binlog是無法實現組提交的,原因在於redo log和binlog的刷盤串行化問題,而實現串行化的目的也是爲了保證兩份日誌保持一致,而在5.6版本之後提供了一種解決方案,能夠保證binlog實現組提交。基本思想是:引入隊列機制保證 innodb commit順序與binlog落盤順序一致,並將事務分組,組內的binlog刷盤動作交給一個事務進行,實現組提交的目的。binlog提交將提交分爲了3個階段,flush階段,sync階段和commit階段。每個階段都有一個隊列,每個隊列有一個mutex保護,預定進入隊列的第一個線程爲leader,其他線程爲follower,所有事情交給leader去做,leader做完所有的動作之後,通知follower刷盤結束。

其實就是說如果想要提高binlog組提交的效率的話,那麼可以通過設置一下兩個參數:

binlog_group_commit_sync_delay 參數,表示延遲多少微秒後才調用fsync;

binlog_group_commit_sync_no_delay_count 參數,表示累積多少次以後才調用 fsync。

7.基於GTID的主從複製

在我們之前講解的主從複製實操中,每次想要複製,必須要在備機上執行對應的命令,如下所示:

change master to master_host='192.168.37.129',master_user='root',master_password='root',master_port=3306,master_log_file='master-bin.000001',master_log_pos=154;

在此配置中我們必須要知道具體的binlog是哪個文件,同時在文件的哪個位置開始複製,正常情況下也沒有問題,但是如果是一個主備主從集羣,那麼如果主機宕機,當從機開始工作的時候,那麼備機就要同步從機的位置,此時位置可能跟主機的位置是不同的,因此在這種情況下,再去找位置就會比較麻煩,所以在5.6版本之後出來一個基於GTID的主從複製。

GTID(global transaction id)是對於一個已提交事務的編號,並且是一個全局唯一的編號。GTID實際上是由UUID+TID組成的,其中UUID是mysql實例的唯一標識,TID表示該實例上已經提交的事務數量,並且隨着事務提交單調遞增。這種方式保證事務在集羣中有唯一的ID,強化了主備一致及故障恢復能力。

7.1 基於GTID的搭建

7.1.1 修改mysql配置文件,添加如下配置,

主從mysql 服務器都需要加這個配置

gtid_mode=on
enforce-gtid-consistency=true

7.1.2 重啓mysql 主從的服務

7.1.3從庫執行如下命令

change master to master_host='192.168.37.129',master_user='root',master_password='root',master_auto_position=1;

7.1.4 主庫從庫插入數據測試。

7.1.5 遇到的問題:

找不到主庫的gtid

 

 解決方案:將主庫的gtid 在從庫上設置一下

stop slave;
set global gtid_purged='5df008a1-12fd-11ed-b7bd-000c297a16e8:1';
start salve;

 

7.2 2、基於GTID的並行複製

無論是什麼方式的主從複製其實原理相差都不是很大,關鍵點在於將組提交的信息存放在GTId中。

    show binlog events in 'lian-bin.000001';

previous_gtids:用於表示上一個binlog最後一個gtid的位置,每個binlog只有一個。

gtid:當開啓gtid的時候,每一個操作語句執行前會添加一個gtid事件,記錄當前全局事務id,組提交信息被保存在gtid事件中,有兩個關鍵字段,last_committed,sequence_number用來標識組提交信息。

 上述日誌看起來可能比較麻煩,可以使用如下命令執行:

其中last_committed表示事務提交的時候,上次事務提交的編號,如果事務具有相同的last_committed值表示事務就在一個組內,在備庫執行的時候可以並行執行。同時大家還要注意,每個last_committed的值都是上一個組事務的sequence_number值。

看到此處,大家可能會有疑問,如果我們不開啓gtid,分組信息該如何保存呢?

其實是一樣的,當沒有開啓的時候,數據庫會有一個Anonymous_Gtid,用來保存組相關的信息。

如果大家想看並行的效果的話,可以執行如下代碼:

package com.mashibing;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Date;

public class ConCurrentInsert  extends Thread{
    public void run() {
        String url = "jdbc:mysql://192.168.85.111/lian2";
        String name = "com.mysql.jdbc.Driver";
        String user = "root";
        String password = "123456";
        Connection conn = null;
        try {
            Class.forName(name);
            conn = DriverManager.getConnection(url, user, password);//獲取連接
            conn.setAutoCommit(false);//關閉自動提交,不然conn.commit()運行到這句會報錯
        } catch (Exception e1) {
            e1.printStackTrace();
        }
        // 開始時間
        Long begin = new Date().getTime();
        // sql前綴
        String prefix = "INSERT INTO t1 (id,age) VALUES ";
        try {
            // 保存sql後綴
            StringBuffer suffix = new StringBuffer();
            // 設置事務爲非自動提交
            conn.setAutoCommit(false);
            // 比起st,pst會更好些
            PreparedStatement pst = (PreparedStatement) conn.prepareStatement("");//準備執行語句
            // 外層循環,總提交事務次數
            for (int i = 1; i <= 10; i++) {
                suffix = new StringBuffer();
                // 第j次提交步長
                for (int j = 1; j <= 10; j++) {
                    // 構建SQL後綴
                    suffix.append("(" +i*j+","+i*j+"),");
                }
                // 構建完整SQL
                String sql = prefix + suffix.substring(0, suffix.length() - 1);
                // 添加執行SQL
                pst.addBatch(sql);
                // 執行操作
                pst.executeBatch();
                // 提交事務
                conn.commit();
                // 清空上一次添加的數據
                suffix = new StringBuffer();
            }
            // 頭等連接
            pst.close();
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // 結束時間
        Long end = new Date().getTime();
        // 耗時
        System.out.println("100萬條數據插入花費時間 : " + (end - begin) / 1000 + " s"+"  插入完成");
    }

    public static void main(String[] args) {
        for (int i = 1; i <=10; i++) {
            new ConCurrentInsert().start();
        }
    }
}

 參考文檔:https://blog.csdn.net/yaoxie1534/article/details/126259320

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章