做者:高鹏
文章末尾有他著做的《深刻理解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 主从原理的朋友不容错过。