做者:高鵬
文章末尾有他著做的《深刻理解MySQL主從原理 32講》,深刻透徹理解MySQL主從,GTID相關技術知識。
本文節選自《深刻理解MySQL主從原理》第16節
注意:本文分爲正文和附件兩部分,都是圖片格式,若是正文有圖片不清晰能夠將附件的圖片保存到本地查看。mysql
注意:本文分爲正文和附件兩部分,都是圖片格式,若是正文有圖片不清晰能夠將附件的圖片保存到本地查看。算法
基於COMMIT_ORDER的並行複製只有在有壓力的狀況下才可能會造成一組,壓力不大的狀況下在從庫的並行度並不會高。可是基於WRITESET的並行複製目標就是在ORDER_COMMIT的基礎上再儘量的下降last commit,這樣在從庫得到更好的並行度(即使在主庫串行執行的事務在從庫也能並行應用)。它使用的方式就是經過掃描Writeset中的每個元素(行數據的hash值)在一個叫作Writeset的歷史MAP(行數據的hash值和seq number的一個MAP)中進行比對,尋找是否有衝突的行,而後作相應的處理,後面咱們會詳細描述這種行爲。若是要使用這種方式咱們須要在主庫設置以下兩個參數:sql
它們是在5.7.22才引入的。數據庫
咱們先來看一個截圖,仔細觀察其中的last commit:session
咱們能夠看到其中的last commit看起來是亂序的,這種狀況在基於COMMIT_ORDER 的並行複製方式下是不可能出現的。實際上它就是咱們前面說的基於WRITESET的並行複製再儘量下降的last commit的結果。這種狀況會在MTS從庫得到更好的並行回放效果,第19節將會詳細解釋並行斷定的標準。併發
實際上Writeset是一個集合,使用的是C++ STL中的set容器,在類Rpl_transaction_write_set_ctx中包含了以下定義:app
std::set<uint64> write_set_unique;
集合中的每個元素都是hash值,這個hash值和咱們的transaction_write_set_extraction參數指定的算法有關,其來源就是行數據的主鍵和惟一鍵。每行數據包含了兩種格式:函數
每行數據的具體格式爲:學習
主鍵/惟一鍵名稱 | 分隔符 | 庫名 | 分隔符 | 庫名長度 | 表名 | 分隔符 | 表名長度 | 鍵字段1 | 分隔符 | 長度 | 鍵字段2 | 分隔符 | 長度 | 其餘字段... |
---|
在Innodb層修改一行數據以後會將這上面的格式的數據進行hash後寫入到Writeset中。能夠參考函數add_pke,後面我也會以僞代碼的方式給出部分流程。ui
可是須要注意一個事務的全部的行數據的hash值都要寫入到一個Writeset。若是修改的行比較多那麼可能須要更多內存來存儲這些hash值。雖然8字節比較小,可是若是一個事務修改的行不少,那麼仍是須要消耗較多的內存資源的。
爲了更直觀的觀察到這種數據格式,可使用debug的方式獲取。下面咱們來看一下。
咱們使用以下表:
mysql> use test Database changed mysql> show create table jj10 \G *************************** 1. row *************************** Table: jj10 Create Table: CREATE TABLE `jj10` ( `id1` int(11) DEFAULT NULL, `id2` int(11) DEFAULT NULL, `id3` int(11) NOT NULL, PRIMARY KEY (`id3`), UNIQUE KEY `id1` (`id1`), KEY `id2` (`id2`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 1 row in set (0.00 sec)
咱們寫入一行數據:
insert into jj10 values(36,36,36);
這一行數據一共會生成4個元素分別爲:
注意:這裏顯示的?是分隔符
(gdb) p pke $1 = "PRIMARY?test?4jj10?4\200\000\000$?4" **注意:\200\000\000$ :爲3個八進制字節和ASCII字符 $, 其轉換爲16進制就是「0X80 00 00 24 」**
分解爲:
主鍵名稱 | 分隔符 | 庫名 | 分隔符 | 庫名長度 | 表名 | 分隔符 | 表名長度 | 主鍵字段1 | 分隔符 | 長度 |
---|---|---|---|---|---|---|---|---|---|---|
PRIMARY | ? | test | ? | 4 | jj10 | ? | 4 | 0x80 00 00 24 | ? | 4 |
(gdb) p pke $2 = "PRIMARY?test?4jj10?436?2"
分解爲:
主鍵名稱 | 分隔符 | 庫名 | 分隔符 | 庫名長度 | 表名 | 分隔符 | 表名長度 | 主鍵字段1 | 分隔符 | 長度 |
---|---|---|---|---|---|---|---|---|---|---|
PRIMARY | ? | test | ? | 4 | jj10 | ? | 4 | 36 | ? | 2 |
(gdb) p pke $3 = "id1?t 上
(gdb) p pke $4 = "id1?test?4jj10?436?2"
解析同上
最終這些數據會經過hash算法後寫入到Writeset中。
下面是一段僞代碼,用來描述這種生成過程:
若是表中存在索引: 將數據庫名,表名信息寫入臨時變量 循環掃描表中每一個索引: 若是不是惟一索引: 退出本次循環繼續循環。 循環兩種生成數據的方式(二進制格式和字符串格式): 將索引名字寫入到pke中。 將臨時變量信息寫入到pke中。 循環掃描索引中的每個字段: 將每個字段的信息寫入到pke中。 若是字段掃描完成: 將pke生成hash值而且寫入到寫集合中。 若是沒有找到主鍵或者惟一鍵記錄一個標記,後面經過這個標記來 斷定是否使用Writeset的並行複製方式
前一節咱們討論了基於ORDER_COMMIT的並行複製是如何生成last_commit和seq number的。實際上基於WRITESET的並行複製方式只是在ORDER_COMMIT的基礎上對last_commit作更進一步處理,並不影響原有的ORDER_COMMIT邏輯,所以若是要回退到ORDER_COMMIT邏輯很是方便。能夠參考MYSQL_BIN_LOG::write_gtid函數。
根據binlog_transaction_dependency_tracking取值的不一樣會作進一步的處理,以下:
這段描述的代碼對應:
case DEPENDENCY_TRACKING_COMMIT_ORDER: m_commit_order.get_dependency(thd, sequence_number, commit_parent); break; case DEPENDENCY_TRACKING_WRITESET: m_commit_order.get_dependency(thd, sequence_number, commit_parent); m_writeset.get_dependency(thd, sequence_number, commit_parent); break; case DEPENDENCY_TRACKING_WRITESET_SESSION: m_commit_order.get_dependency(thd, sequence_number, commit_parent); m_writeset.get_dependency(thd, sequence_number, commit_parent); m_writeset_session.get_dependency(thd, sequence_number, commit_parent); break;
咱們到這裏已經討論了Writeset是什麼,也已經說過若是要下降last commit的值咱們須要經過對事務的Writeset和Writeset的歷史MAP進行比對,看是否衝突才能決定下降爲何值。那麼必須在內存中保存一份這樣的一個歷史MAP才行。在源碼中使用以下方式定義:
/* Track the last transaction sequence number that changed each row in the database, using row hashes from the writeset as the index. */ typedef std::map<uint64,int64> Writeset_history; //map實現 Writeset_history m_writeset_history;
咱們能夠看到這是C++ STL中的map容器,它包含兩個元素:
它是按照Writeset的hash值進行排序的。
其次內存中還維護一個叫作m_writeset_history_start的值,用於記錄Writeset的歷史MAP中最先事務的seq number。若是Writeset的歷史MAP滿了就會清理這個歷史MAP而後將本事務的seq number寫入m_writeset_history_start,做爲最先的seq number。後面會看到對於事務last commit的值的修改老是從這個值開始而後進行比較判斷修改的,若是在Writeset的歷史MAP中沒有找到衝突那麼直接設置last commit爲這個m_writeset_history_start值便可。下面是清理Writeset歷史MAP的代碼:
if (exceeds_capacity || !can_use_writesets) //Writeset的歷史MAP已滿 { m_writeset_history_start= sequence_number; //若是超過最大設置,清空writeset history。從當前seq number 從新記錄, 也就是最小的那個事務seq number m_writeset_history.clear(); //清空歷史MAP }
這裏介紹一下整個處理的過程,假設以下:
初始化狀況以下圖(圖16-1,高清原圖請關注文末的課程):
整個過程結束。last commit由之前的125下降爲120,目的達到了。實際上咱們能夠看出Writeset歷史MAP就至關於保存了一段時間以來修改行的快照,若是保證本次事務修改的數據在這段時間內沒有衝突,那麼顯然是能夠在從庫並行執行的。last commit下降後以下圖(圖16-2,高清原圖請關注文末的課程):
整個邏輯就在函數Writeset_trx_dependency_tracker::get_dependency中,下面是一些關鍵代碼,代碼稍多:
if (can_use_writesets) //若是可以使用writeset 方式 { /* Check if adding this transaction exceeds the capacity of the writeset history. If that happens, m_writeset_history will be cleared only after 而 add_pke using its information for current transaction. */ exceeds_capacity= m_writeset_history.size() + writeset->size() > m_opt_max_history_size; //若是大於參數binlog_transaction_dependency_history_size設置清理標記 /* Compute the greatest sequence_number among all conflicts and add the transaction's row hashes to the history. */ int64 last_parent= m_writeset_history_start; //臨時變量,首先設置爲最小的一個seq number for (std::set<uint64>::iterator it= writeset->begin(); it != writeset->end(); ++it) //循環每個Writeset中的每個元素 { Writeset_history::iterator hst= m_writeset_history.find(*it); //是否在writeset history中 已經存在了。 map中的元素是 key是writeset 值是sequence number if (hst != m_writeset_history.end()) //若是存在 { if (hst->second > last_parent && hst->second < sequence_number) last_parent= hst->second; //若是已經大於了不須要設置 hst->second= sequence_number; //更改這行記錄的sequence_number } else { if (!exceeds_capacity) m_writeset_history.insert(std::pair<uint64, int64>(*it, sequence_number)); //沒有衝突則插入。 } } ...... if (!write_set_ctx->get_has_missing_keys()) //若是沒有主鍵和惟一鍵那麼不更改last commit { /* The WRITESET commit_parent then becomes the minimum of largest parent found using the hashes of the row touched by the transaction and the commit parent calculated with COMMIT_ORDER. */; commit_parent= std::min(last_parent, commit_parent); //這裏對last commit作更改了。下降他的last commit } } } } if (exceeds_capacity || !can_use_writesets) { m_writeset_history_start= sequence_number; //若是超過最大設置 清空writeset history。從當前sequence 從新記錄 也就是最小的那個事務seqnuce number m_writeset_history.clear();//清空真個MAP }
前面說過這種方式就是在WRITESET的基礎上繼續處理,實際上它的含義就是同一個session的事務不容許在從庫並行回放。代碼很簡單,以下:
int64 session_parent= thd->rpl_thd_ctx.dependency_tracker_ctx(). get_last_session_sequence_number(); //取本session的上一次事務的seq number if (session_parent != 0 && session_parent < sequence_number) //若是本session已經作過事務而且本次當前的seq number大於上一次的seq number commit_parent= std::max(commit_parent, session_parent); //說明這個session作過屢次事務不容許併發,修改成order_commit生成的last commit thd->rpl_thd_ctx.dependency_tracker_ctx(). set_last_session_sequence_number(sequence_number); //設置session_parent的值爲本次seq number的值
通過這個操做後,咱們發現這種狀況最後last commit恢復成了ORDER_COMMIT的方式。
本參數默認值爲25000。表明的是咱們說的Writeset歷史MAP中元素的個數。如前面分析的Writeset生成過程當中修改一行數據可能會生成多個HASH值,所以這個值還不能徹底等待於修改的行數,能夠理解爲以下:
咱們經過前面的分析能夠發現若是這個值越大那麼在Writeset歷史MAP中能容下的元素也就越多,生成的last commit就可能更加精確(更加小),從庫併發的效率也就可能越高。可是咱們須要注意設置越大相應的內存需求也就越高了。
實際上在函數add_pke中就會判斷是否有主鍵或者惟一鍵,若是存在惟一鍵也是能夠。Writeset中存儲了惟一鍵的行數據hash值。參考函數add_pke,下面是判斷:
if (!((table->key_info[key_number].flags & (HA_NOSAME )) == HA_NOSAME)) //跳過非惟一的KEY continue;
若是沒有主鍵或者惟一鍵那麼下面語句將被觸發:
if (writeset_hashes_added == 0) ws_ctx->set_has_missing_keys();
而後咱們在生成last commit會判斷這個設置以下:
if (!write_set_ctx->get_has_missing_keys()) //若是沒有主鍵和惟一鍵那麼不更改last commit { /* The WRITESET commit_parent then becomes the minimum of largest parent found using the hashes of the row touched by the transaction and the commit parent calculated with COMMIT_ORDER. */; commit_parent= std::min(last_parent, commit_parent);//這裏對last commit作更改了。下降他的last commit } }
所以沒有主鍵可使用惟一鍵,若是都沒有的話WRITESET設置就不會生效回退到老的ORDER_COMMIT方式。
有了前面的基礎,咱們就很容易解釋這種現象了。其主要緣由就是Writeset的歷史MAP的存在,只要這些事務修改的行沒有衝突,也就是主鍵/惟一鍵不相同,那麼在基於WRITESET的並行複製方式中就能夠存在這種現象,可是若是binlog_transaction_dependency_tracking設置爲WRITESET_SESSION則不會出現這種現象。
好了到這裏咱們明白了基於WRITESET的並行複製方式的優勢,可是它也有明顯的缺點以下:
若是從庫沒有延遲,則不須要考慮這種方式,即使有延遲咱們也應該先考慮其餘方案。第28節咱們將會描述有哪些致使延遲的可能。
第16節結束
最後推薦高鵬的專欄《深刻理解MySQL主從原理 32講》,想要透徹瞭解學習MySQL 主從原理的朋友不容錯過。