基于COMMIT_ORDER的并行復(fù)制只有在有壓力的情況下才可能會(huì)形成一組,壓力不大的情況下在從庫(kù)的并行度并不會(huì)高。但是基于WRITESET的并行復(fù)制目標(biāo)就是在ORDER_COMMIT的基礎(chǔ)上再盡可能的降低last commit,這樣在從庫(kù)獲得更好的并行度(即便在主庫(kù)串行執(zhí)行的事務(wù)在從庫(kù)也能并行應(yīng)用)。它使用的方式就是通過(guò)掃描Writeset中的每一個(gè)元素(行數(shù)據(jù)的hash值)在一個(gè)叫做Writeset的歷史MAP(行數(shù)據(jù)的hash值和seq number的一個(gè)MAP)中進(jìn)行比對(duì),尋找是否有沖突的行,然后做相應(yīng)的處理,后面我們會(huì)詳細(xì)描述這種行為。如果要使用這種方式我們需要在主庫(kù)設(shè)置如下兩個(gè)參數(shù):
澤庫(kù)網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)!從網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、APP開發(fā)、響應(yīng)式網(wǎng)站設(shè)計(jì)等網(wǎng)站項(xiàng)目制作,到程序開發(fā),運(yùn)營(yíng)維護(hù)。創(chuàng)新互聯(lián)2013年開創(chuàng)至今到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來(lái)保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選創(chuàng)新互聯(lián)。
transaction_write_set_extraction=XXHASH64
binlog_transaction_dependency_tracking=WRITESET
它們是在5.7.22才引入的。
一、奇怪的last commit
我們先來(lái)看一個(gè)截圖,仔細(xì)觀察其中的last commit:
我們可以看到其中的last commit看起來(lái)是亂序的,這種情況在基于COMMIT_ORDER 的并行復(fù)制方式下是不可能出現(xiàn)的。實(shí)際上它就是我們前面說(shuō)的基于WRITESET的并行復(fù)制再盡可能降低的last commit的結(jié)果。這種情況會(huì)在MTS從庫(kù)獲得更好的并行回放效果,第19節(jié)將會(huì)詳細(xì)解釋并行判定的標(biāo)準(zhǔn)。
二、Writeset是什么
實(shí)際上Writeset是一個(gè)集合,使用的是C++ STL中的set容器,在類Rpl_transaction_write_set_ctx中包含了如下定義:
std::set write_set_unique;
集合中的每一個(gè)元素都是hash值,這個(gè)hash值和我們的transaction_write_set_extraction參數(shù)指定的算法有關(guān),其來(lái)源就是行數(shù)據(jù)的主鍵和唯一鍵。每行數(shù)據(jù)包含了兩種格式:
字段值為二進(jìn)制格式
字段值為字符串格式
每行數(shù)據(jù)的具體格式為:
在Innodb層修改一行數(shù)據(jù)之后會(huì)將這上面的格式的數(shù)據(jù)進(jìn)行hash后寫入到Writeset中??梢詤⒖己瘮?shù)add_pke,后面我也會(huì)以偽代碼的方式給出部分流程。
但是需要注意一個(gè)事務(wù)的所有的行數(shù)據(jù)的hash值都要寫入到一個(gè)Writeset。如果修改的行比較多那么可能需要更多內(nèi)存來(lái)存儲(chǔ)這些hash值。雖然8字節(jié)比較小,但是如果一個(gè)事務(wù)修改的行很多,那么還是需要消耗較多的內(nèi)存資源的。
為了更直觀的觀察到這種數(shù)據(jù)格式,可以使用debug的方式獲取。下面我們來(lái)看一下。
三、Writeset的生成
我們使用如下表:
MySQL> use testDatabase changedmysql> show create table jj10 \G*************************** 1. row *************************** Table: jj10Create 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=latin11 row in set (0.00 sec)
我們寫入一行數(shù)據(jù):
insert into jj10 values(36,36,36);
這一行數(shù)據(jù)一共會(huì)生成4個(gè)元素分別為:
注意:這里顯示的?是分隔符
1. 主鍵二進(jìn)制格式
(gdb) p pke$1 = "PRIMARY?test?4jj10?4\200\000\000$?4"**注意:\200\000\000$ :為3個(gè)八進(jìn)制字節(jié)和ASCII字符 $,其轉(zhuǎn)換為16進(jìn)制就是“0X80 00 00 24 ”**
分解為:
2. 主鍵字符串格式:
(gdb) p pke$2 = "PRIMARY?test?4jj10?436?2"
分解為:
3. 唯一鍵二進(jìn)制格式
(gdb) p pke$3 = "id1?test?4jj10?4\200\000\000$?4"
解析同上
4. 唯一鍵字符串格式:
(gdb) p pke$4 = "id1?test?4jj10?436?2"
解析同上
最終這些數(shù)據(jù)會(huì)通過(guò)hash算法后寫入到Writeset中。
四、函數(shù)add_pke的大概流程
下面是一段偽代碼,用來(lái)描述這種生成過(guò)程:
如果表中存在索引: 將數(shù)據(jù)庫(kù)名,表名信息寫入臨時(shí)變量 循環(huán)掃描表中每個(gè)索引: 如果不是唯一索引: 退出本次循環(huán)繼續(xù)循環(huán)。 循環(huán)兩種生成數(shù)據(jù)的方式(二進(jìn)制格式和字符串格式): 將索引名字寫入到pke中。 將臨時(shí)變量信息寫入到pke中。 循環(huán)掃描索引中的每一個(gè)字段: 將每一個(gè)字段的信息寫入到pke中。 如果字段掃描完成: 將pke生成hash值并且寫入到寫集合中。 如果沒(méi)有找到主鍵或者唯一鍵記錄一個(gè)標(biāo)記,后面通過(guò)這個(gè)標(biāo)記來(lái) 判定是否使用Writeset的并行復(fù)制方式
五、Writeset設(shè)置對(duì)last commit的處理方式
前一節(jié)我們討論了基于ORDER_COMMIT的并行復(fù)制是如何生成last_commit和seq number的。實(shí)際上基于WRITESET的并行復(fù)制方式只是在ORDER_COMMIT的基礎(chǔ)上對(duì)last_commit做更進(jìn)一步處理,并不影響原有的ORDER_COMMIT邏輯,因此如果要回退到ORDER_COMMIT邏輯非常方便??梢詤⒖糓YSQL_BIN_LOG::write_gtid函數(shù)。
根據(jù)binlog_transaction_dependency_tracking取值的不同會(huì)做進(jìn)一步的處理,如下:
ORDER_COMMIT:調(diào)用m_commit_order.get_dependency函數(shù)。這是前面我們討論的方式。
WRITESET:調(diào)用m_commit_order.get_dependency函數(shù),然后調(diào)用m_writeset.get_dependency??梢钥吹絤_writeset.get_dependency函數(shù)會(huì)對(duì)原有的last commit做處理。
WRITESET_SESSION:調(diào)用m_commit_order.get_dependency函數(shù),然后調(diào)用m_writeset.get_dependency再調(diào)用m_writeset_session.get_dependency。
m_writeset_session.get_dependency會(huì)對(duì)last commit再次做處理。
這段描述的代碼對(duì)應(yīng):
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的歷史MAP
我們到這里已經(jīng)討論了Writeset是什么,也已經(jīng)說(shuō)過(guò)如果要降低last commit的值我們需要通過(guò)對(duì)事務(wù)的Writeset和Writeset的歷史MAP進(jìn)行比對(duì),看是否沖突才能決定降低為什么值。那么必須在內(nèi)存中保存一份這樣的一個(gè)歷史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 Writeset_history; //map實(shí)現(xiàn) Writeset_history m_writeset_history;
我們可以看到這是C++ STL中的map容器,它包含兩個(gè)元素:
Writeset的hash值
最新一次本行數(shù)據(jù)修改事務(wù)的seq number
它是按照Writeset的hash值進(jìn)行排序的。
其次內(nèi)存中還維護(hù)一個(gè)叫做m_writeset_history_start的值,用于記錄Writeset的歷史MAP中最早事務(wù)的seq number。如果Writeset的歷史MAP滿了就會(huì)清理這個(gè)歷史MAP然后將本事務(wù)的seq number寫入m_writeset_history_start,作為最早的seq number。后面會(huì)看到對(duì)于事務(wù)last commit的值的修改總是從這個(gè)值開始然后進(jìn)行比較判斷修改的,如果在Writeset的歷史MAP中沒(méi)有找到?jīng)_突那么直接設(shè)置last commit為這個(gè)m_writeset_history_start值即可。下面是清理Writeset歷史MAP的代碼:
if (exceeds_capacity || !can_use_writesets)//Writeset的歷史MAP已滿 { m_writeset_history_start= sequence_number;//如果超過(guò)最大設(shè)置,清空writeset history。//從當(dāng)前seq number 重新記錄, 也就是最小的那個(gè)事務(wù)seq number m_writeset_history.clear();//清空歷史MAP }
七、Writeset的并行復(fù)制對(duì)last commit的處理流程
這里介紹一下整個(gè)處理的過(guò)程,假設(shè)如下:
當(dāng)前通過(guò)基于ORDER_COMMIT的并行復(fù)制方式后,構(gòu)造出來(lái)的是(last commit=125,seq number=130)。
本事務(wù)修改了4條數(shù)據(jù),我分別使用ROW1/ROW7/ROW6/ROW10代表。
表只包含主鍵沒(méi)有唯一鍵,并且我的圖中只保留行數(shù)據(jù)的二進(jìn)制格式的hash值,而沒(méi)有包含數(shù)據(jù)的字符串格式的hash值。
初始化情況如下圖:?
第一步 設(shè)置last commit為writeset_history_start的值也就是100。
第二步 ROW1.HASHVAL在Writeset歷史MAP中查找,找到?jīng)_突的行ROW1.HASHVAL將歷史MAP中這行數(shù)據(jù)的seq number更改為130。同時(shí)設(shè)置last commit為120。
第三步 ROW7.HASHVAL在Writeset歷史MAP中查找,找到?jīng)_突的行ROW7.HASHVAL將Writeset歷史MAP中這行數(shù)據(jù)的seq number更改為130。由于歷史MAP中對(duì)應(yīng)的seq number為114,小于120不做更改。last commit依舊為120。
第四步 ROW6.HASHVAL在Writeset歷史MAP中查找,找到?jīng)_突的行ROW6.HASHVAL將Writeset歷史MAP中這行數(shù)據(jù)的seq number更改為130。由于歷史MAP中對(duì)應(yīng)的seq number為105,小于120不做更改。last commit依舊為120。
第五步 ROW10.HASHVAL在Writeset歷史MAP中查找,沒(méi)有找到?jīng)_突的行,因此需要將這一行插入到Writeset歷史MAP中查找(需要判斷是否導(dǎo)致歷史MAP占滿,如果占滿則不需要插入,后面隨即要清理掉)。即要將ROW10.HASHVAL和seq number=130插入到Writeset歷史MAP中。
整個(gè)過(guò)程結(jié)束。last commit由以前的130降低為120,目的達(dá)到了。實(shí)際上我們可以看出Writeset歷史MAP就相當(dāng)于保存了一段時(shí)間以來(lái)修改行的快照,如果保證本次事務(wù)修改的數(shù)據(jù)在這段時(shí)間內(nèi)沒(méi)有沖突,那么顯然是可以在從庫(kù)并行執(zhí)行的。last commit降低后如下圖:
鄭州不孕不育醫(yī)院:http://wapyyk.39.net/zz3/zonghe/1d427.html
整個(gè)邏輯就在函數(shù)Writeset_trx_dependency_tracker::get_dependency中,下面是一些關(guān)鍵代碼,代碼稍多:
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;//如果大于參數(shù)binlog_transaction_dependency_history_size設(shè)置清理標(biāo)記 /* 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;//臨時(shí)變量,首先設(shè)置為最小的一個(gè)seq number for (std::set::iterator it= writeset->begin(); it != writeset->end(); ++it)//循環(huán)每一個(gè)Writeset中的每一個(gè)元素 { Writeset_history::iterator hst= m_writeset_history.find(*it);//是否在writeset history中 已經(jīng)存在了。//map中的元素是 key是writeset 值是sequence number if (hst != m_writeset_history.end()) //如果存在 { if (hst->second > last_parent && hst->second < sequence_number) last_parent= hst->second;//如果已經(jīng)大于了不需要設(shè)置 hst->second= sequence_number;//更改這行記錄的sequence_number } else { if (!exceeds_capacity) m_writeset_history.insert(std::pair(*it, sequence_number));//沒(méi)有沖突則插入。 } }...... if (!write_set_ctx->get_has_missing_keys())//如果沒(méi)有主鍵和唯一鍵那么不更改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);//這里對(duì)last commit做更改了。降低他的last commit } } } } if (exceeds_capacity || !can_use_writesets) { m_writeset_history_start= sequence_number;//如果超過(guò)最大設(shè)置 清空writeset history。//從當(dāng)前sequence 重新記錄 也就是最小的那個(gè)事務(wù)seqnuce number m_writeset_history.clear();//清空整個(gè)MAP }
八、WRITESET_SESSION的方式
前面說(shuō)過(guò)這種方式就是在WRITESET的基礎(chǔ)上繼續(xù)處理,實(shí)際上它的含義就是同一個(gè)session的事務(wù)不允許在從庫(kù)并行回放。代碼很簡(jiǎn)單,如下:
int64 session_parent= thd->rpl_thd_ctx.dependency_tracker_ctx(). get_last_session_sequence_number();//取本session的上一次事務(wù)的seq number if (session_parent != 0 && session_parent < sequence_number)//如果本session已經(jīng)做過(guò)事務(wù)并且本次當(dāng)前的seq number大于上一次的seq number commit_parent= std::max(commit_parent, session_parent);//說(shuō)明這個(gè)session做過(guò)多次事務(wù)不允許并發(fā),修改為order_commit生成的last commit thd->rpl_thd_ctx.dependency_tracker_ctx(). set_last_session_sequence_number(sequence_number);//設(shè)置session_parent的值為本次seq number的值
經(jīng)過(guò)這個(gè)操作后,我們發(fā)現(xiàn)這種情況最后last commit恢復(fù)成了ORDER_COMMIT的方式。
九、關(guān)于binlog_transaction_dependency_history_size參數(shù)說(shuō)明
本參數(shù)默認(rèn)值為25000。代表的是我們說(shuō)的Writeset歷史MAP中元素的個(gè)數(shù)。如前面分析的Writeset生成過(guò)程中修改一行數(shù)據(jù)可能會(huì)生成多個(gè)HASH值,因此這個(gè)值還不能完全等待于修改的行數(shù),可以理解為如下:
binlog_transaction_dependency_history_size/2=修改的行數(shù) * (1+唯一鍵個(gè)數(shù))
我們通過(guò)前面的分析可以發(fā)現(xiàn)如果這個(gè)值越大那么在Writeset歷史MAP中能容下的元素也就越多,生成的last commit就可能更加精確(更加小),從庫(kù)并發(fā)的效率也就可能越高。但是我們需要注意設(shè)置越大相應(yīng)的內(nèi)存需求也就越高了。
十、沒(méi)有主鍵的情況
實(shí)際上在函數(shù)add_pke中就會(huì)判斷是否有主鍵或者唯一鍵,如果存在唯一鍵也是可以。Writeset中存儲(chǔ)了唯一鍵的行數(shù)據(jù)hash值。參考函數(shù)add_pke,下面是判斷:
if (!((table->key_info[key_number].flags & (HA_NOSAME )) == HA_NOSAME))//跳過(guò)非唯一的KEY continue;
如果沒(méi)有主鍵或者唯一鍵那么下面語(yǔ)句將被觸發(fā):
if (writeset_hashes_added == 0) ws_ctx->set_has_missing_keys();
然后我們?cè)谏蒷ast commit會(huì)判斷這個(gè)設(shè)置如下:
if (!write_set_ctx->get_has_missing_keys())//如果沒(méi)有主鍵和唯一鍵那么不更改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. */; //這里對(duì)last commit做更改了。降低他的last commit commit_parent= std::min(last_parent, commit_parent); } }
因此沒(méi)有主鍵可以使用唯一鍵,如果都沒(méi)有的話WRITESET設(shè)置就不會(huì)生效回退到老的ORDER_COMMIT方式。
十一、為什么同一個(gè)session執(zhí)行的事務(wù)也能生成同樣的last commit
有了前面的基礎(chǔ),我們就很容易解釋這種現(xiàn)象了。其主要原因就是Writeset的歷史MAP的存在,只要這些事務(wù)修改的行沒(méi)有沖突,也就是主鍵/唯一鍵不相同,那么在基于WRITESET的并行復(fù)制方式中就可以存在這種現(xiàn)象,但是如果binlog_transaction_dependency_tracking設(shè)置為WRITESET_SESSION則不會(huì)出現(xiàn)這種現(xiàn)象。
寫在最后
好了到這里我們明白了基于WRITESET的并行復(fù)制方式的優(yōu)點(diǎn),但是它也有明顯的缺點(diǎn)如下:
Writeset中每個(gè)hash值都需要和Writeset的歷史MAP進(jìn)行比較。
Writeset需要額外的內(nèi)存空間。
Writeset的歷史MAP需要額外的內(nèi)存空間。
如果從庫(kù)沒(méi)有延遲,則不需要考慮這種方式,即便有延遲我們也應(yīng)該先考慮其他方案。下一次我們將會(huì)描述有哪些導(dǎo)致延遲的可能。
本文標(biāo)題:基于WRITESET的并行復(fù)制方式
文章路徑:http://jinyejixie.com/article26/gpgijg.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供關(guān)鍵詞優(yōu)化、外貿(mào)網(wǎng)站建設(shè)、做網(wǎng)站、品牌網(wǎng)站設(shè)計(jì)、域名注冊(cè)、App開發(fā)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)