概念
主從復制,是指建立一個和主數據庫完全一樣的數據庫環境(稱為從數據庫),並將主庫的操作行為進行復制的過程:將主數據庫的DDL和DML的操作日志同步到從數據庫上,
然后在從數據庫上對這些日志進行重新執行,來保證從數據庫和主數據庫的數據的一致性。
為什么要做主從復制
1、在復雜的業務操作中,經常會有操作導致鎖行甚至鎖表的情況,如果讀寫不解耦,會很影響運行中的業務,使用主從復制,讓主庫負責寫,從庫負責讀。
即使主庫出現了鎖表的情景,通過讀從庫也可以保證業務的正常運行。
2、保證數據的熱備份,主庫宕機后能夠及時替換主庫,保障業務可用性。
3、架構的演進:業務量擴大,I/O訪問頻率增高,單機無法滿足,主從復制可以做多庫方案,降低磁盤I/O訪問的頻率,提高單機的I/O性能。
4、本質上也是分治理念,主從復制、讀寫分離即是壓力分拆的過程。
5、讀寫比也影響整個拆分方式,讀寫比越高,主從庫比例應越高,才能保證讀寫的均衡,才能保證較好的運行性能。讀寫比下的主從分配方法下:
讀寫比 | 主庫 | 從庫 |
50:50 | 1 | 1 |
66.6:33.3 | 1 | 2 |
80:20 | 1 | 4 |
-- -- | -- -- | -- -- |
主從復制的原理
當在從庫上啟動復制時,首先創建I/O線程連接主庫,主庫隨后創建Binlog Dump線程讀取數據庫事件並發送給I/O線程,I/O線程獲取到事件數據后更新到從庫的中繼日志Relay Log中去,之后從庫上的SQL線程讀取中繼日志Relay Log中更新的數據庫事件並應用,
如下圖所示:
細化一下有如下幾個步驟:
1、MySQL主庫在事務提交時把數據變更(insert、delet、update)作為事件日志記錄在二進制日志表(binlog)里面。
2、主庫上有一個工作線程 binlog dump thread,把binlog的內容發送到從庫的中繼日志relay log中。
3、從庫根據中繼日志relay log重做數據變更操作,通過邏輯復制來達到主庫和從庫的數據一致性。
4、MySQL通過三個線程來完成主從庫間的數據復制,其中binlog dump線程跑在主庫上,I/O線程和SQL線程跑在從庫上。擁有多個從庫的主庫會為每一個連接到主庫的從庫創建一個binlog dump線程。
搭建主從實例
我們這邊在個人PC機上進行MySQL主從復制的搭建測試,所以使用Docker會更方便。有如下優勢:
1、可以節省資源。
2、相對於Docker來說,虛擬機搭建對機器配置有要求,且安裝MySQL步驟繁瑣。
3、一台機器上可以運行多個Docker容器,所以我們可以部署多個MySQL服務。
4、Docker容器之間相互獨立,有獨立ip,互不沖突
5、Docker使用步驟簡便,啟動容器在秒級別。
Daocker安裝
我這邊是mac機,以此作為示范。可以用Homebrew 進行安裝,也可以手動下載安裝。
手動下載的話可以點擊以下鏈接下載 Edge Docker for Mac。
如同Mac OS 其它軟件一樣,安裝也非常簡單,雙擊下載的 .dmg 文件,然后將鯨魚圖標拖拽到 Application 文件夾即可。
從應用中找到 Docker 圖標並點擊運行。去Docker Hub上去注冊一個Docker ID,進行登錄即可。
打開終端,可以查看Docker相關的版本及相關信息。可以輸入如下信息:
1 # 查看docker版本 2 docker --version 3 # 查看docker基本信息 4 docker info
搭建主從服務器
1、拉取docker的MySQL鏡像,這邊以5.7的版本為准:
1 docker pull mysql:5.7
2、使用此鏡像啟動主庫容器:
1 docker run -p 3307:3306 --name master -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7
從這邊可以看出,Master主庫容器對外映射的端口是3307,賬號默認root,密碼是123456,執行完之后,docker中運行了一個名為master的MySQL實例。
docker容器是相互獨立的,每個容器有其獨立的ip,所以不同容器使用相同的端口並不會沖突。這里我們應該盡量使用mysql默認的3306端口,否則可能會出現無法通過ip連接docker容器內mysql的問題。
因為容器內的3306端口映射的對外端口是3307,所以外部主機可以通過 宿主機ip:3307 訪問到MySQL的服務,而實際是訪問容器內MySQL的3306端口,密碼就是我們前面設置好的123456。
登錄完之后可以看到一個空庫,這個就是我們在docker里面拉的MySQL鏡像。
3、同樣的道理,我們繼續建一個slave(從庫)的MySQL實例。注意名稱為slave,端口為3308
1 docker run -p 3308:3306 --name slave -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7
4、主從都搭建完成之后,我們可以使用 docker ps 來查看當前運行的容器信息,如下:
注意下第一個參數為 container id,是我們后續進入服務進行配置的容器識別編號。
配置主服務器(Master)
首先,進入到 Master 服務器。
1 wengzhihua@B000000147796DS ~ % docker exec -it 777fe9ce7f9d /bin/bash 2 root@777fe9ce7f9d:/#
這邊注意:777fe9ce7f9d 是 master 的 container id,然后查看MySQL的狀態。
1 root@777fe9ce7f9d:/# service mysql status 2 [info] MySQL Community Server 5.7.35 is running.
然后到MySQL的目錄下去修改配置,切換到/etc/mysql目錄下,然后 vi my.cnf 對my.cnf進行編輯:
1 root@777fe9ce7f9d:/# cd /etc/mysql 2 root@777fe9ce7f9d:/etc/mysql# vi my.cnf 3 bash: vi: command not found
此時會報出 bash: vi: command not found ,需要我們在docker容器內部自行安裝vim。我們使用 apt-get install vim 命令來安裝vim
1 root@777fe9ce7f9d:/etc/mysql# apt-get install vim 2 Reading package lists... Done 3 Building dependency tree 4 Reading state information... Done 5 E: Unable to locate package vim
這邊又提示 Unable to locate package vim。
執行 apt-get update ,然后再次執行 apt-get install vim 即可成功安裝vim。
然后我們就可以使用vim編輯my.cnf,在my.cnf中添加如下配置:
1 [mysqld] 2 ## 設置server_id,一般設置為IP,同一局域網內使用唯一值即可,注意要保證唯一,這邊我們暫且使用主庫的映射端口,方便識別 3 server_id=3307 4 ## 復制過濾:也就是指定哪個數據庫不用同步(mysql庫一般不同步) 5 binlog-ignore-db=mysql 6 ## 開啟二進制日志功能,可以隨便取,最好有含義(關鍵就是這里了) 7 log-bin=test-mysql-bin 8 ## 為每個session 分配的內存,在事務過程中用來存儲二進制日志的緩存 9 binlog_cache_size=1M 10 ## 主從復制的格式(mixed,statement,row,默認格式是statement) 11 binlog_format=mixed 12 ## 二進制日志自動刪除/過期的天數。默認值為0,表示不自動刪除。 13 expire_logs_days=7 14 ## 跳過主從復制中遇到的所有錯誤或指定類型的錯誤,避免slave端復制中斷。 15 ## 如:1062錯誤是指一些主鍵重復,1032錯誤是因為主從數據庫數據不一致 16 slave_skip_errors=1062
配置完成之后重啟服務:
1 service mysql restart
這個命令會使得容器停止,重新啟動就可以了。
接下來創建數據同步用戶:
1 root@777fe9ce7f9d:/etc/mysql# mysql -u root -p 2 Enter password: 3 Welcome to the MySQL monitor. Commands end with ; or \g. 4 Your MySQL connection id is 2 5 Server version: 5.7.35-log MySQL Community Server (GPL) 6 7 Copyright (c) 2000, 2021, Oracle and/or its affiliates. 8 9 Oracle is a registered trademark of Oracle Corporation and/or its 10 affiliates. Other names may be trademarks of their respective 11 owners. 12 13 Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. 14 15 mysql> CREATE USER 'slave'@'%' IDENTIFIED BY '123456'; 16 Query OK, 0 rows affected (0.02 sec) 17 18 mysql> GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'slave'@'%'; 19 Query OK, 0 rows affected (0.00 sec)
這里主要是要授予用戶 slave,REPLICATION SLAVE權限和REPLICATION CLIENT權限,用於同步數據。
配置從服務器(Slave)
同主服務器一樣的操作方式,先進入MySQL配置文件
1 wengzhihua@B000000147796DS ~ % docker exec -it d4ba5e063deb /bin/bash 2 root@d4ba5e063deb:/# cd etc/mysql 3 root@d4ba5e063deb:/etc/mysql# vi my.cnf
然后配置從庫的信息:
1 [mysqld] 2 ## 設置server_id,一般設置為IP,注意要唯一,這邊我們使用從庫的映射端口,方便識別 3 server_id=3308 4 ## 復制過濾:也就是指定哪個數據庫不用同步(mysql庫一般不同步) 5 binlog-ignore-db=mysql 6 ## 開啟二進制日志功能,以備Slave作為其它Slave的Master時使用 7 log-bin=test-mysql-slave1-bin 8 ## 為每個session 分配的內存,在事務過程中用來存儲二進制日志的緩存 9 binlog_cache_size=1M 10 ## 主從復制的格式(mixed,statement,row,默認格式是statement) 11 binlog_format=mixed 12 ## 二進制日志自動刪除/過期的天數。默認值為0,表示不自動刪除。 13 expire_logs_days=7 14 ## 跳過主從復制中遇到的所有錯誤或指定類型的錯誤,避免slave端復制中斷。 15 ## 如:1062錯誤是指一些主鍵重復,1032錯誤是因為主從數據庫數據不一致 16 slave_skip_errors=1062 17 ## relay_log配置中繼日志 18 relay_log=edu-mysql-relay-bin 19 ## log_slave_updates表示slave將復制事件寫進自己的二進制日志 20 log_slave_updates=1 21 ## 防止改變數據(除了特殊的線程) 22 read_only=1
配置完成之后重啟服務 service mysql restart :
1 root@d4ba5e063deb:/etc/mysql# service mysql restart 2 [info] Stopping MySQL Community Server 5.7.35. 3 ... 4 [info] MySQL Community Server 5.7.35 is stopped. 5 [info] Re-starting MySQL Community Server 5.7.35.
跟上面一樣,這個命令會使得容器停止,重新啟動就可以了。
完成Master和Slave的連接
注意,需要保證 Master 和 Slave 除了不同步的數據庫,其他數據庫的數據要一致。
1、在 Master 進入 MySQL, 然后執行命令:
1 mysql> show master status; 2 +----------------------+----------+--------------+------------------+-------------------+ 3 | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set | 4 +----------------------+----------+--------------+------------------+-------------------+ 5 | test-mysql-bin.000001 | 617 | | mysql | | 6 +----------------------+----------+--------------+------------------+-------------------+ 7 1 row in set (0.00 sec)
記錄下 File 和 Position 字段的值,后面會用到。
2、然后再查詢一下主從兩個容器所對應的IP,主庫對應 172.17.0.2,從庫對應172.17.0.3:
1 wengzhihua@B000000147796DS ~ % docker ps 2 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 3 d4ba5e063deb mysql:5.7 "docker-entrypoint.s…" 5 hours ago Up 19 minutes 33060/tcp, 0.0.0.0:3308->3306/tcp, :::3308->3306/tcp slave 4 777fe9ce7f9d mysql:5.7 "docker-entrypoint.s…" 5 hours ago Up 51 minutes 33060/tcp, 0.0.0.0:3307->3306/tcp, :::3307->3306/tcp master 5 wengzhihua@B000000147796DS ~ % docker inspect --format='{{.NetworkSettings.IPAddress}}' 777fe9ce7f9d 6 172.17.0.2 7 wengzhihua@B000000147796DS ~ % docker inspect --format='{{.NetworkSettings.IPAddress}}' d4ba5e063deb 8 172.17.0.3
3、然后到 Slave 中進入 mysql,執行命令:
1 mysql> change master to master_host='172.17.0.2', master_user='slave', master_password='123456', master_port=3306, master_log_file='test-mysql-bin.000001', master_log_pos=617, master_connect_retry=30; 2 Query OK, 0 rows affected, 2 warnings (0.02 sec)
大意就是在從庫使用什么賬號(slave)進行同步,同步的主庫的IP(master_host)、密碼(master_password)、端口(master_port)、binlog日志文件(master_log_file)以及其實同步的位置(master_log_pos)等,
理解下這個命令的各個參數:
1 master_host: Master 的IP地址 2 master_user: 在 Master 中授權的用於數據同步的用戶,就我們之前在主庫容器里創建的哪個slave用戶 3 master_password: 同步數據的用戶的密碼 4 master_port: Master 的數據庫的端口號,注意,這邊是3306,不是3307,3307是映射外部宿主主機的,寫成3307,會造成 Slave_IO_Running 一直是 Connecting 狀態,在這邊踩坑了 5 master_log_file: 指定 Slave 從哪個日志文件開始復制數據,即上文中提到的 File 字段的值 6 master_log_pos: 從哪個 Position 開始讀,即上文中提到的 Position 字段的值 7 master_connect_retry: 當重新建立主從連接時,如果連接失敗,重試的時間間隔,單位是秒,默認是60秒。
在 Slave 的 MySQL 終端執行查看主從同步狀態
1 mysql> show slave status \G; 2 *************************** 1. row *************************** 3 Slave_IO_State: 4 Master_Host: 172.17.0.2 5 Master_User: slave 6 Master_Port: 3307 7 Connect_Retry: 30 8 Master_Log_File: edu-mysql-bin.000001 9 Read_Master_Log_Pos: 617 10 Relay_Log_File: edu-mysql-relay-bin.000001 11 Relay_Log_Pos: 4 12 Relay_Master_Log_File: edu-mysql-bin.000001 13 Slave_IO_Running: No 14 Slave_SQL_Running: No 15 Replicate_Do_DB: 16 Replicate_Ignore_DB: 17 Replicate_Do_Table: 18 Replicate_Ignore_Table: 19 Replicate_Wild_Do_Table: 20 Replicate_Wild_Ignore_Table: 21 Last_Errno: 0 22 Last_Error: 23 Skip_Counter: 0 24 Exec_Master_Log_Pos: 617 25 Relay_Log_Space: 154 26 Until_Condition: None 27 Until_Log_File: 28 Until_Log_Pos: 0 29 Master_SSL_Allowed: No
1 start slave;
1 mysql> show slave status \G; 2 *************************** 1. row *************************** 3 Slave_IO_State: Waiting for master to send event 4 Master_Host: 172.17.0.2 5 Master_User: slave 6 Master_Port: 3306 7 Connect_Retry: 30 8 Master_Log_File: edu-mysql-bin.000001 9 Read_Master_Log_Pos: 2351 10 Relay_Log_File: edu-mysql-relay-bin.000006 11 Relay_Log_Pos: 1259 12 Relay_Master_Log_File: edu-mysql-bin.000001 13 Slave_IO_Running: Yes 14 Slave_SQL_Running: Yes 15 Replicate_Do_DB: 16 Replicate_Ignore_DB: 17 Replicate_Do_Table: 18 Replicate_Ignore_Table: 19 Replicate_Wild_Do_Table: 20 Replicate_Wild_Ignore_Table: 21 Last_Errno: 0 22 Last_Error: 23 Skip_Counter: 0 24 Exec_Master_Log_Pos: 2351 25 Relay_Log_Space: 1640 26 Until_Condition: None
驗證同步是否成功:
1、我們在主服務這邊創建一個test庫,庫下面創建了一個person表,並增加了一條數據。
2、轉到從庫這邊,馬上就查詢到數據了
主從同步延遲解決方案
最近部門在招人,面試Java程序員的時候在數據庫部分問的最頻繁的問題就是這個,問題不難,不過很多候選人同學沒辦法比較完整的回答,跟今天的標題有點相關,我就放上來。
按照我們這邊的要求,主從庫同步應該是近實時的,極端情況下也不應該超過8s,如果超過,我們認為是有問題的。
- 保證數據庫處在最有狀態下:優化系統配置(鏈接層或者存儲引擎層):最大連接數、允許錯誤數、允許超時時間、pool_size、log_size,保證內存、CPU、存儲空間的擴容(硬件部分)。
- 業務量不多的情況下,不做讀寫分離。既然主從延遲是由於從庫同步寫庫不及時引起的,那我們也可以在有主從延遲的地方改變讀庫方式,由原來的讀從庫改為讀主庫。當然這也會增加代碼的一些邏輯復雜性。(部分業務讀主庫)
- 假如你的業務時間允許,你可以在寫入主庫的時候,確保數據都同步到從庫了之后才返回這條數據寫入成功,當然如果有多個從庫,你也必須確保每個從庫都寫入成功。顯然,這個方案對性能和時間的消耗是極大的,不推薦。
- 可以引入redis或者其他nosql數據庫來存儲我們經常會產生主從延遲的業務數據。當我在寫入數據庫的同時,我再寫入一份到redis中。我們可以先去查看redis中是否有這個數據,如果有我們就可以直接從redis中讀取這個數據。當數據真正同步到數據庫中的時候,再從redis中把數據刪除。
- 任何的服務器都是有吞吐量的限制的,沒有任何一個方案可以無限制的承載用戶的大量流量。所以我們必須估算好我們的服務器能夠承載的流量上限是多少。達到這個上限之后,就要采取緩存,限流,降級的方式來應對我們的流量。這也是應對主從延遲的根本處理辦法。
- 如果系統流量確實龐大,單純的讀寫分離已經無法解決問題了,那么就應該對數據庫進一步治理,垂直分區和水平分區是不錯的方式,下一章我們會詳細說說。