常常聽到別人說Mysql的SBR、RBR、MBR,若是不清楚,那麼能夠跟着文章一塊兒來學習。因爲涉及到主從的內容比較多,須要拆分紅多篇內容來概述,這章先從基礎知識和主從關係創建開始講起。還會出一篇文章詳細講解從主同步。mysql
1.瞭解什麼是SBR、RBR、MBR?sql
2.瞭解下主從配置該如何配置?數據庫
3.瞭解主從關係如何創建?緩存
在本文中,分爲一主一從。主監聽的端口爲3306,從監聽的端口爲3309。安全
master配置,配置文件my.cnf:bash
[mysqld] port=3306 basedir=/usr/local/mysql8.0.20 datadir=/usr/local/mysql8.0.20/data socket=/tmp/mysql.sock #explicit_defaults_for_timestamp=true lower_case_table_names=2 #表名存儲爲給定的大小寫可是比較的時候是小寫的 log_bin=mysql-bin server_id =10
主服務啓動Mysql命令:服務器
# sudo bin/mysqld --defaults-file=/usr/local/mysql8.0.20/etc/my.cnf --user=root
客戶端鏈接master端多線程
# mysql --socket=/tmp/mysql.sock -u root
啓動master後,須要建立一個用戶,用於主從同步:併發
mysql> GRANT REPLICATION SLAVE ON *.* TO repl@'localhost' IDENTIFIED BY '123456';
查看主服務狀態socket
mysql> show master status \G;
slave配置,配置文件salve.conf:
[mysqld] port=3309 basedir=/usr/local/mysql8.0.20 datadir=/usr/local/mysql8.0.20/data1 socket=/tmp/mysqlslave.sock #explicit_defaults_for_timestamp=true lower_case_table_names=2 #表名存儲爲給定的大小寫可是比較的時候是小寫的 log_bin=mysql-bin server_id=2 relay_log=/usr/local/mysql8.0.20/mysql-relay-bin read_only=1 #執行模式
從服務啓動Mysql命令:
# sudo bin/mysqld --defaults-file=/usr/local/mysql8.0.20/etc/salve.conf --user=root
鏈接Slave端:
# mysql --socket=/tmp/mysqlslave.sock -u root
Slave端主從同步配置:
mysql> change master to master_host='localhost', master_user='repl', master_password='123456', master_log_file='mysql-bin.000001', master_log_pos=0;
Slave/Master
#查看binlog事件 mysql> SHOW BINLOG EVENTS;
若是發現主從沒有同步,可使用以下命令查看相應的狀態:
#查看slave狀態 mysql> show slave status;
若是遇到從庫報這個錯誤:Got fatal error 1236 from master when reading data from binary log: 'Could not find first log file name in binary log index file'
Got fatal error 1236 from master when reading data from binary log: 'could not find next log'
此時能夠操做以下三個命令進行處理:
#關閉slave mysql> stop slave; #重置slave mysql> reset slave; #啓動slave mysql> start slave;
mysql三種複製方式:基於SQL語句的複製(statement-based replication, SBR),基於行的複製(row-based replication, RBR),混合模式複製(mixed-based replication, MBR)。對應的,binlog的格式也有三種:STATEMENT,ROW,MIXED。
每一條會修改數據的sql語句會記錄到binlog中。優勢是並不須要記錄每一條sql語句和每一行的數據變化,減小了binlog日誌量,節約IO,提升性能。缺點是在某些狀況下會致使master-slave中的數據不一致(如sleep()函數, last_insert_id(),以及user-defined functions(udf)等會出現問題)
不記錄每條sql語句的上下文信息,僅需記錄哪條數據被修改了,修改爲什麼樣了。並且不會出現某些特定狀況下的存儲過程、或function、或trigger的調用和觸發沒法被正確複製的問題。缺點是會產生大量的日誌,尤爲是alter table的時候會讓日誌暴漲。
以上兩種模式的混合使用,通常的複製使用STATEMENT模式保存binlog,對於STATEMENT模式沒法複製的操做使用ROW模式保存binlog,MySQL會根據執行的SQL語句選擇日誌保存方式。
binlog複製配置在mysql的配置文件my.cnf中,能夠經過一下選項配置binglog相關,配置以下:
#binlog日誌格式,mysql默認採用statement,建議使用mixed binlog_format = MIXED #binlog日誌文件 log-bin = /data/mysql/mysql-bin.log #binlog過時清理時間 expire_logs_days = 7 #binlog每一個日誌文件大小 max_binlog_size = 100m #binlog緩存大小 binlog_cache_size = 4m #最大binlog緩存大小 max_binlog_cache_size = 512m
對於執行的SQL語句中包含now()這樣的時間函數,會在日誌中產生對應的unix_timestamp()*1000的時間字符串,slave在完成同步時,取用的是sqlEvent發生的時間來保證數據的準確性。另外對於一些功能性函數slave能完成相應的數據同步,而對於上面指定的一些相似於UDF函數,致使Slave沒法知曉的狀況,則會採用ROW格式存儲這些Binlog,以保證產生的Binlog能夠供Slave完成數據同步。
如今來比較如下 SBR 和 RBR 這2種模式各自的優缺點:
使用如下函數的語句也沒法被複制:
INSERT ... SELECT 會產生比 RBR 更多的行級鎖
複製須要進行全表掃描(WHERE 語句中沒有使用到索引)的 UPDATE 時,須要比 RBR 請求更多的行級鎖
對於有 AUTO_INCREMENT 字段的 InnoDB表而言,INSERT 語句會阻塞其餘 INSERT 語句
對於一些複雜的語句,在從服務器上的耗資源狀況會更嚴重,而 RBR 模式下,只會對那個發生變化的記錄產生影響
存儲函數(不是存儲過程)在被調用的同時也會執行一次 NOW() 函數,這個能夠說是壞事也多是好事
肯定了的 UDF 也須要在從服務器上執行
數據表必須幾乎和主服務器保持一致才行,不然可能會致使複製出錯
執行復雜語句若是出錯的話,會消耗更多資源
任何狀況均可以被複制,這對複製來講是最安全可靠的和其餘大多數數據庫系統的複製技術同樣多數狀況下,從服務器上的表若是有主鍵的話,複製就會快了不少。複製如下幾種語句時的行鎖更少:
主服務器上執行 UPDATE 語句時,全部發生變化的記錄都會寫到 binlog 中,而 SBR 只會寫一次,這會致使頻繁發生 binlog 的併發寫問題
沒法從 binlog 中看到都複製了寫什麼語句
當在非事務表上執行一段堆積的SQL語句時,最好採用 SBR 模式,不然很容易致使主從服務器的數據不一致狀況發生。
另外,針對系統庫 mysql 裏面的表發生變化時的處理規則以下:
若是是採用 INSERT,UPDATE,DELETE 直接操做表的狀況,則日誌格式根據 binlog_format 的設定而記錄
若是是採用 GRANT,REVOKE,SET PASSWORD 等管理語句來作的話,那麼不管如何都採用 SBR 模式記錄
注:採用 RBR 模式後,能解決不少原先出現的主鍵重複問題。
經過以下命令便可查看複製格式,如圖2-4-1:
mysql> show variables like 'binlog_format';
<center>圖2-4-1 查看複製格式</center>
默認binlog_format參數爲行復制,在源碼mysql-8.0.20/sql/sys_vars.cc中
static Sys_var_enum Sys_binlog_format( "binlog_format", "What form of binary logging the master will " "use: either ROW for row-based binary logging, STATEMENT " "for statement-based binary logging, or MIXED. MIXED is statement-" "based binary logging except for those statements where only row-" "based is correct: those which involve user-defined functions (i.e. " "UDFs) or the UUID() function; for those, row-based binary logging is " "automatically used. If NDBCLUSTER is enabled and binlog-format is " "MIXED, the format switches to row-based and back implicitly per each " "query accessing an NDBCLUSTER table", SESSION_VAR(binlog_format), CMD_LINE(REQUIRED_ARG, OPT_BINLOG_FORMAT), binlog_format_names, DEFAULT(BINLOG_FORMAT_ROW), //默認binlog的同步爲行復制 NO_MUTEX_GUARD, NOT_IN_BINLOG, ON_CHECK(binlog_format_check), ON_UPDATE(fix_binlog_format_after_update));
經過上面代碼能夠看出mysql-8.0.20默認爲行復制。
那麼繼續來看一下binlog_format都有那些模式,能夠看mysql-8.0.20/sql/system_variables.h文件
// Values for binlog_format sysvar enum enum_binlog_format { BINLOG_FORMAT_MIXED = 0, ///<混合模式 statement if safe, otherwise row - autodetected BINLOG_FORMAT_STMT = 1, ///<SQL複製 statement-based BINLOG_FORMAT_ROW = 2, ///<行復制 row-based BINLOG_FORMAT_UNSPEC = 3 ///< thd_binlog_format() returns it when binlog is closed };
基於以下代碼能夠得知,binlog_format包含BINLOG_FORMAT_MIXED、BINLOG_FORMAT_STMT、BINLOG_FORMAT_ROW三種模式,也就是對應:STATEMENT模式(SBR)、ROW模式(RBR)、MIXED模式(MBR)
經過張圖來看一下主從關係創建的相關流程,如圖3-1-1:
<center>圖3-1-1 主從同步創建</center>
在mysql主從創建中,是由slave端先發起,當執行「start slave;」 語句時,會調用rpl_slave.cc中的start_slave方法,其中實現以下:
bool start_slave(THD *thd, LEX_SLAVE_CONNECTION *connection_param, LEX_MASTER_INFO *master_param, int thread_mask_input, Master_info *mi, bool set_mts_settings) { bool is_error = false; int thread_mask; DBUG_TRACE; lock_slave_threads(mi); //中止運行線程 // 獲取已中止線程的掩碼 init_thread_mask(&thread_mask, mi, true /* inverse */); /* 咱們將中止下面的全部線程。但若是用戶想只啓動一個線程, 就好像另外一個線程正在運行同樣(就像咱們不要想碰另外一根線), 因此將位設置爲0其餘線程 */ if (thread_mask_input) { thread_mask &= thread_mask_input; } if (thread_mask) // 一些線程中止,啓動它們 { if (load_mi_and_rli_from_repositories(mi, false, thread_mask)) { is_error = true; my_error(ER_MASTER_INFO, MYF(0)); } else if (*mi->host || !(thread_mask & SLAVE_IO)) { /* 若是咱們要啓動IO線程,咱們須要考慮經過啓動從機提供的選項。 */ if (thread_mask & SLAVE_IO) { if (connection_param->user) { //設置用戶 mi->set_start_user_configured(true); mi->set_user(connection_param->user); } if (connection_param->password) { //設置密碼 mi->set_start_user_configured(true); mi->set_password(connection_param->password); } if (connection_param->plugin_auth) //設置受權插件 mi->set_plugin_auth(connection_param->plugin_auth); if (connection_param->plugin_dir) //插件目錄 mi->set_plugin_dir(connection_param->plugin_dir); } //... //初始化設置 int slave_errno = mi->rli->init_until_option(thd, master_param); if (slave_errno) { my_error(slave_errno, MYF(0)); is_error = true; } if (!is_error) is_error = check_slave_sql_config_conflict(mi->rli); } else if (master_param->pos || master_param->relay_log_pos || master_param->gtid) push_warning(thd, Sql_condition::SL_NOTE, ER_UNTIL_COND_IGNORED, ER_THD(thd, ER_UNTIL_COND_IGNORED)); if (!is_error) //啓動slave線程 is_error = start_slave_threads(false /*need_lock_slave=false*/, true /*wait_for_start=true*/, mi, thread_mask); } else { is_error = true; my_error(ER_BAD_SLAVE, MYF(0)); } } else { /* 若是全部線程都已啓動,則沒有錯誤,只有一個警告 */ push_warning_printf( thd, Sql_condition::SL_NOTE, ER_SLAVE_CHANNEL_WAS_RUNNING, ER_THD(thd, ER_SLAVE_CHANNEL_WAS_RUNNING), mi->get_channel()); } /* 若是有人試圖啓動,請清除啓動信息,IO線程以免任何安全問題。 */ if (is_error && (thread_mask & SLAVE_IO) == SLAVE_IO) mi->reset_start_info(); unlock_slave_threads(mi); mi->channel_unlock(); return is_error; }
從如上代碼能夠得知調用「start slave;」時,會對線程作一些中止操做。而後進行一些設置後,調用start_slave_threads方法啓動slave線程。而後start_slave_threads是一個比較關鍵的方法。
那麼接下來看一下start_slave_threads方法,實現以下:
bool start_slave_threads(bool need_lock_slave, bool wait_for_start, Master_info *mi, int thread_mask) { mysql_mutex_t *lock_io = nullptr, *lock_sql = nullptr, *lock_cond_io = nullptr, *lock_cond_sql = nullptr; mysql_cond_t *cond_io = nullptr, *cond_sql = nullptr; bool is_error = false; DBUG_TRACE; DBUG_EXECUTE_IF("uninitialized_master-info_structure", mi->inited = false;); //... if (thread_mask & SLAVE_IO) //判斷是否支持SLAVE_IO is_error = start_slave_thread( #ifdef HAVE_PSI_THREAD_INTERFACE key_thread_slave_io, #endif handle_slave_io, lock_io, lock_cond_io, cond_io, &mi->slave_running, &mi->slave_run_id, mi); //調用handle_slave_io方法 if (!is_error && (thread_mask & SLAVE_SQL)) { //判斷是否支持SLAVE_SQL //... if (!is_error) is_error = start_slave_thread( #ifdef HAVE_PSI_THREAD_INTERFACE key_thread_slave_sql, #endif handle_slave_sql, lock_sql, lock_cond_sql, cond_sql, &mi->rli->slave_running, &mi->rli->slave_run_id, mi); //調用handle_slave_sql方法 if (is_error) terminate_slave_threads(mi, thread_mask & SLAVE_IO, rpl_stop_slave_timeout, need_lock_slave); } return is_error; }
經過如上方法能夠得知thread_mask掩碼是用於判斷是否支持SLAVE_IO與支持SLAVE_SQL。若是支持則線程調用對應的方法。由於考慮到主題爲主從關係創建,這裏主要關注一下handle_slave_io方法。
<center>圖3-1-2 主從同步協議</center>
如圖3-1-2中IO Thread與SQL Thread其實就是對應handle_slave_io方法與handle_slave_sql方法。
handle_slave_io方法爲創建主從的主要方法,其中包含了初始化slave線程、發起鏈接master、註冊slave到master,發起COM_BINLOG_DUMP或COM_BINLOG_DUMP_GTID操做等,實現以下:
extern "C" void *handle_slave_io(void *arg) { //... my_thread_init(); { //初始化slave線程 if (init_slave_thread(thd, SLAVE_THD_IO)) { mysql_cond_broadcast(&mi->start_cond); mysql_mutex_unlock(&mi->run_lock); mi->report(ERROR_LEVEL, ER_SLAVE_FATAL_ERROR, ER_THD(thd, ER_SLAVE_FATAL_ERROR), "Failed during slave I/O thread initialization "); goto err; } //... mysql_cond_broadcast(&mi->start_cond); //調用作喚醒操做 //... //發起登錄操做 successfully_connected = !safe_connect(thd, mysql, mi); // we can get killed during safe_connect #ifdef HAVE_SETNS if (mi->is_set_network_namespace()) { // Restore original network namespace used to be before connection has // been created successfully_connected = restore_original_network_namespace() | successfully_connected; } #endif //... /* 註冊slave到master */ THD_STAGE_INFO(thd, stage_registering_slave_on_master); if (register_slave_on_master(mysql, mi, &suppress_warnings)) { if (!check_io_slave_killed(thd, mi, "Slave I/O thread killed " "while registering slave on master")) { LogErr(ERROR_LEVEL, ER_RPL_SLAVE_IO_THREAD_CANT_REGISTER_ON_MASTER); if (try_to_reconnect(thd, mysql, mi, &retry_count, suppress_warnings, reconnect_messages[SLAVE_RECON_ACT_REG])) goto err; } else goto err; goto connected; } //... while (!io_slave_killed(thd, mi)) { MYSQL_RPL rpl; THD_STAGE_INFO(thd, stage_requesting_binlog_dump); if (request_dump(thd, mysql, &rpl, mi, &suppress_warnings)) { //發起dump指令 LogErr(ERROR_LEVEL, ER_RPL_SLAVE_ERROR_REQUESTING_BINLOG_DUMP, mi->get_for_channel_str()); if (check_io_slave_killed(thd, mi, "Slave I/O thread killed while \ requesting master dump") || try_to_reconnect(thd, mysql, mi, &retry_count, suppress_warnings, reconnect_messages[SLAVE_RECON_ACT_DUMP])) goto err; goto connected; } //... } //... my_thread_end(); //線程結束 #if OPENSSL_VERSION_NUMBER < 0x10100000L ERR_remove_thread_state(0); #endif /* OPENSSL_VERSION_NUMBER < 0x10100000L */ my_thread_exit(nullptr); //退出線程 return (nullptr); // Avoid compiler warnings }
從代碼中能夠得知調用safe_connect進行slave對master的登錄,具體登錄能夠協議能夠看一下以前寫的文章:https://blog.csdn.net/byxiaoyuonly/article/details/108212013。
而後又調用register_slave_on_master會發送COM_REGISTER_SLAVE指令進行把slave註冊到master,再調用request_dump發起binlog_dump指令。
<center>圖3-2-1 發送COM_BINLOG_DUMP</center>
在request_dump發送指令其實支持COM_BINLOG_DUMP與COM_BINLOG_DUMP_GTID兩種,可是具體發什麼取決因而否開啓gtid設置,如圖3-2-1所示。
經過對上面內容的瞭解,咱們得知register_slave_on_master會發起發起COM_REGISTER_SLAVE對把slave註冊到master,而後調用request_dump發起binlog_dump指令。master指令處理以下:
bool dispatch_command(THD *thd, const COM_DATA *com_data, enum enum_server_command command) { //... switch (command) { case COM_REGISTER_SLAVE: { //註冊slave到master // TODO: access of protocol_classic should be removed if (!register_slave(thd, thd->get_protocol_classic()->get_raw_packet(), thd->get_protocol_classic()->get_packet_length())) my_ok(thd); break; } //... case COM_BINLOG_DUMP_GTID: //binlog_dump_gtid // TODO: access of protocol_classic should be removed error = com_binlog_dump_gtid( thd, (char *)thd->get_protocol_classic()->get_raw_packet(), thd->get_protocol_classic()->get_packet_length()); break; case COM_BINLOG_DUMP: //binlog_dump // TODO: access of protocol_classic should be removed error = com_binlog_dump( thd, (char *)thd->get_protocol_classic()->get_raw_packet(), thd->get_protocol_classic()->get_packet_length()); break; } return error; }
註冊slave到master過程能夠看後續的數據包,這邊能夠接着看一下對應的com_binlog_dump方法實現:
bool com_binlog_dump(THD *thd, char *packet, size_t packet_length) { DBUG_TRACE; ulong pos; ushort flags = 0; const uchar *packet_position = (uchar *)packet; size_t packet_bytes_todo = packet_length; DBUG_ASSERT(!thd->status_var_aggregated); thd->status_var.com_other++; thd->enable_slow_log = opt_log_slow_admin_statements; if (check_global_access(thd, REPL_SLAVE_ACL)) return false; /* 4 bytes is too little, but changing the protocol would break compatibility. This has been fixed in the new protocol. @see com_binlog_dump_gtid(). */ READ_INT(pos, 4); READ_INT(flags, 2); READ_INT(thd->server_id, 4); DBUG_PRINT("info", ("pos=%lu flags=%d server_id=%d", pos, flags, thd->server_id)); kill_zombie_dump_threads(thd); query_logger.general_log_print(thd, thd->get_command(), "Log: '%s' Pos: %ld", packet + 10, (long)pos); mysql_binlog_send(thd, thd->mem_strdup(packet + 10), (my_off_t)pos, nullptr, flags); //發送binlog unregister_slave(thd, true, true /*need_lock_slave_list=true*/); /* 若是咱們到了這裏,線程須要終止 */ return true; error_malformed_packet: my_error(ER_MALFORMED_PACKET, MYF(0)); return true; }
在mysql_binlog_send中其實調用Binlog_sender的run方法,sender.run()方法中又調用Binlog_sender::init 初始化檢測、Binlog_sender::check_start_file() 檢查文件等。最終調用Binlog_sender::send_binlog對從服務發送binlog,如圖3-4-1所示。
<center>圖3-4-1 send binlog</center>
<center>圖4-1 主從創建過程數據包</center>
經過圖4-1能夠得知在主從關係創建會發起以下操做:
#查詢當前時間戳 SELECT UNIX_TIMESTAMP() #查詢master的serverid SELECT @@GLOBAL.SERVER_ID #設置心跳週期,單位爲納秒,其實只有30s。初始化心跳如圖4-2 SET @master_heartbeat_period= 30000001024 #設置master_binlog_checksum SET @master_binlog_checksum= @@global.binlog_checksum #查詢master_binlog_checksum SELECT @master_binlog_checksum #得到是否支持gtid SELECT @@GLOBAL.GTID_MODE #查詢server uuid SELECT @@GLOBAL.SERVER_UUID #設置slave uuid SET @slave_uuid= '2dc27df4-e143-11ea-b396-cc679ee1902b'
<center>圖4-2 初始化心跳週期</center>