mysql的group commit的概念以及实现很久就出来 ,组提交,即一组事务一起提交?为什么要组提交?是怎么实现组提交的? 组提交跟单个事务提交的区别是什么?当组提交的时候,某个事物要回滚了怎么办?有没有可能出现同一个组提交的事物,因为某个事务commit失败,而导致回滚,该怎么回滚?组提交,是否改变主从同步binlog的粒度? 是否主从同步也按照一个group来进行同步?
上面的诸多问题,亲是否曾在心中也有同样的疑问?下面happypig将通过核心的源码解析来告诉上面这些问题的答案(限于文章篇幅,本篇只解决部分疑问)。同时,在阅读代码的同时,对其中的一段很相当程度的疑惑,貌似处理的不够严谨,不知是否可能引发bug 。所以在文章的最后部分指出,希望大神看到后,能帮忙确认一下并指点一二。
或许很多朋友都知道group commit的核心作用,就是减少sync_binlog的次数,将一组事务的binlog一次性刷新到磁盘。这个是group commit 最最核心的作用。明白了这点,后面的解析将会变得简单。
group commit ,即组提交,所以如何分组就成了核心的内容。该怎么分?
实际上,mysql是用一个队列来进行分组的。本质上,事物提交依然是串行。
下面来讲mysql是如何进行事务分组的。
首先来看从网上找来的一张图,原来地址 “http://www.cnblogs.com/cchust/p/4439107.html“, 图形简单明了。但要了解详细细节,建议还是继续看happypig的源码解析。
mysql的group commit 概念是在binlog层来实现的,事务提交时,要写binlog,但通过MYSQL_BIN_LOG::orderde_commit的函数,可以实现组提交。
下面该函数的开头部分:
标红的两句:
thd->get_transaction()->m_flags.pending= true; 初始设置这个要提交的事务的标志为pending .
thd->next_to_commit= NULL; 这是指向一个线程的指针,指向下一个要提交事务的线程。初始值为null. 估计亲已经猜到,这个next_to_commit的指针跟group commit 很有关系。
接着往下看:
我们知道,事物提交的第一步动作就是将线程的binlog cache 刷新到binlog file的cache .
而下面change_stage(thd, Stage_manager::FLUSH_STAGE, thd, NULL, &LOCK_log) 函数的作用就是将该线程排队, 在flush_stage阶段排队。
真正实现排队的函数如下, 也就是上面标红的函数,(当然,下面的mysql_mutex_lock(enter_mutex)也很重要,其他线程执行到这步时,必须等待持有该锁的其他线程释放该锁,何时释放该锁咱们这次不展开,也就意味着,线程之间,在这个函数的这一步是无法并行的) 下面我们来看enroll_for函数的代码:
(上面的代码是在源码的基础上被作者剔除了非必需的部分):
第一个标红的行: bool leader= m_queue[stage].append(thd);
该行的作用是将线程加入到 m_queue[stage] 队列里面。在此步骤,就是m_queue[FLUSH_STAGE]的队列里面,并返回该线程是否是在队列的头部。 处于队列的头部的前提条件是:当该线程加入的时候,该队列已是一个空队列。 也就意味是一个新的分组,并且该线程leader这个分组。
接着往下看:
if ( ! leader) , 即如果该线程没有被放置在队列m_queue[stage] 的头部,则执行下面的内容:
while (thd->get_transaction()->m_flags.pending)
mysql_cond_wait(&m_cond_done, &m_lock_done);
在文章的开头部分,我们就已经说明要提交的事物的pending标志初始值为true. 所以该标志没有其他线程将其设置为false之前,while条件成立,然后进入mysql_cond_wait(&m_cond_done, &m_lock_done);该函数的作用就是将该线程去“睡大觉“,等待被别的线程唤醒。 但何时被唤醒——当这个分组的leader线程将它的事物提交之后,再将它唤醒。
也就是,一个分组的事务,都是被leader线程去提交的,其他的线程都在”睡大觉”,等待leader线程来帮它执行事务提交。 我们来看看change_stage函数的源码注释吧,change_stage的函数是Stage_manager::enroll_for ()的父函数。
– Atomically enqueueing a queue of processes (which is just one for
the first phase).
自动将线程排队。
– If the queue was empty, the thread is the leader for that stage
and it should process the entire queue for that stage.
如果队列为空,则该线程就是leader , 他会处理整个队列的线程的事物。
– If the queue was not empty, the thread is a follower and can go
waiting for the commit to finish
如果队列不会空,则该线程就是这个队列的follower, 然后该线程可以去“睡觉”,等待leader线程将该线程的事物提交完成。
当线程排队之后,后续的步骤都按照该分组的队列顺序执行,在此不再展开。
当线程进入到flush队列之后,该线程(自己睡觉)的binlog cache后续就会被所属队列的leader线程刷新到 binlog file的cache, 也就是函数process_flush_stage_queue的作用。 该函数如下:
我们来解析一下该函数:
第一步: 设置flush_error的初始值为1
第二步:THD *first_seen= stage_manager.fetch_queue_for(Stage_manager::FLUSH_STAGE); 该函数的作用是取出FLUSH_STAGE 队列的leader线程,然后该m_queue[FLUSH_STAGE]队列清空,清空之后,第一个新进来的线程将作为新的分组的leader .
第三步: ha_flush_logs(NULL, true); 存储引擎innodb 刷新redo log,确保事务在存储引擎层物理提交。
第四步:利用for循环将分组中线程逐个取出,然后将该线程的binlog cache刷新到binlog file的cache.
for (THD *head= first_seen ; head ; head = head->next_to_commit)
{
std::pair<int,my_off_t> result= flush_thread_caches(head);
total_bytes+= result.second;
if (flush_error == 1)
flush_error= result.first;
}
正是上面的代码,happypig有点疑问? 因为flush_error的初始值为1,所以第一轮循环的时候,会执行flush_error= result.first; 也就是将flush_thread_caches(head)函数返回的错误值赋值给 flush_error,如果对第一个线程执行flush_thread_caches()函数返回的结果为0, 而对第二个线程执行flush_thread_caches()返回的值是1, 但最终的flush_error 却是0 。而该函数是flush_error 作为返回值。 该值并没有代表同一个分组的所有的thread的flush 该线程binlog cache的结果。
该段代码是否会存在问题? 如有大神看到,请大神确认与赐教。
另外,在说一下happypig的另外一个猜想——-为什么mysql的开发者写这段代码时,只取了第一个线程的刷新结果作为整个分组的线程的flush cache的结果?原因如下:
1。开发者认为第一个线程刷新成功,就意味着后面的线程也会刷新成功,因为不成功,则意味着mysql的线程出问题,或者binlog file的cache出问题,意味着机器宕机,所以线程也就无法继续往下走,整组事务都无法提交。
2。其他的想法,太大胆,不说了。。。。哈哈。。。。。。。