redo log的组提交

    WAL是事务实现持久化的常用技术,基本原理是在非只读事务提交时,将redo log顺序写入磁盘,就视为事务提交完成,不需要等到事务的修改页落盘,这是为了用顺序写代替随机写,提高事务的commit速度。redo log落盘后,即使事务没有正常commit,crash recover也可以保证不丢数据,保证了D持久性。但是每次事务提交时,都有一次fsync操作,此操作耗时长,往往是事务并发的瓶颈。

    所以,redo log可以采用组提交的方式,将多个事务的redo log的flush操作 合并成一次磁盘的顺序写,可以显著的提升事务并发量。每条redo log都会有LSN号(MySQL笔记三-事务-redolog有提到)。例如trx1 ,trx2,trx3 事务的LSN 满足LSN1<LSN2<LSN3,他们三同时被write进系统缓存内,这时flush需要做一下判断,

    1.获取log_mutex

    2,if  flushed_to_disk_lsn>=lsn 说明日志已经被刷到磁盘里了,跳过

current_flush_lsn>=lsn 说明日志正在刷到磁盘,跳过

    4,将LSN< LSN3 的redo log 刷新到磁盘

    5,退出并释放 log_mutex。

 

二阶段提交

    为了保证mysql 能crash recover,在开启binlog的条件下,采用二阶段提交。

    1,第一阶段,prepare阶段,binlog不做任何操作,write && flush redo log, 设置undo state=TRX_UNDO_PREPARED;

设置undo页的状态,置为TRX_UNDO_TO_FREE或TRX_UNDO_TO_PURGE

如果在commit阶段发生crash,crash recover 会扫描最后一个binlog文件提取xid值,这个xid之后的事务全部purge,之前的事务全部任务commit成功。

淘宝做了一个优化,将prepare阶段的flush redo动作移到了binlog write之后,binlog flush之前,保证了flush binlog之前,一定会flush redo。这样就不会违背原有的crash recover,好处是,binlog write需要时间,这样就可以多积累集合write redo log,然后一次 flush,并且binlog flush 也可以多个一起flush了。(丁奇老师一图胜过千言万语,我就不自己画了)

    

mysql group by只能一个字段 mysql多个group by_MySQL

 

什么是xid值

COMMIT /* xid=xxxx */这样的行,其中xid是事务标识符,可以认为在程序中xid是事务的唯一标识符,直接指代具体的事务。生成了xid,说明binlog已经写完了,根据二阶段提交,也意味着事务commit。

 

binlog的group commit

    看了上文,一切都那么美好,redo log和binlog 都可以group commit了,但事实却不是这样。MySQL 5.6版本之前,如果开启了binlog,将导致group commi失效。

    首先,设想这样一个场景,trx1 和trx2先后准备commit,于是进入二阶段提交,prapare阶段一切正常,commit阶段时,trx1和trx2都write binlog成功,但是trx2先commit成功,trx1还没来得及commit就crash了。这是的crash recover会找trx2 的xid,发现已经commit成功了,那么只有trx2之后的事务purge,但是trx1其实并没有commit完成,binlog里没有trx1的记录,所以这时的物理数据和binlog出现了不同。这时如果用这个数据库进行备份&&恢复,就会导致主从数据不一致,而这是不能接受的。

    如果解决这个问题呢?就是要保证binlog 的写入顺序和事务的commit顺序必须要一致。MySQL引入了“臭名昭著”的prepare_commit_mutex,这个锁将redo log和binlog 的flush出串行化,即trx1不commit成功,trx2甚至不能write redolog,这就导致group commit失效,严重影响性能。

    最终解决方案

    BLGC (Binary Log Group Commit)

    MySQL将事务的提交分成了三个阶段,flush,sync, commit

MySQL 5.6的binary log group commit的逻辑主要在binlog.cc的ordered_commit方法实现,主干逻辑如下:

int MYSQL_BIN_LOG::ordered_commit(THD *thd, bool all, bool skip_commit)
{ ......
/*这里暂不做介绍,*/
thd->durability_property= HA_IGNORE_DURABILITY;
        ......
        // 进入第一阶段FLUSH_STAGE,主要完成的是Flush 各个线程的binlog cache到binary log文件中。
 /* Stage #1: flushing transactions to binary log
        While flushing, we allow new threads to enter and will process
        them in due time. Once the queue was empty, we cannot reap
        anything more since it is possible that a thread entered and
        appointed itself leader for the flush phase.
*/
change_stage(thd, Stage_manager::FLUSH_STAGE, thd, NULL, &LOCK_log)
        ......
process_flush_stage_queue(&total_bytes, &do_rotate, &wait_queue);
    /*
        Stage #2: Syncing binary log file to disk
    */
change_stage(thd, Stage_manager::SYNC_STAGE, wait_queue,
need_LOCK_log ? NULL : &LOCK_log, &LOCK_sync)
......
/*
Stage #3: Commit all transactions in order.
 
This stage is skipped if we do not need to order the commits and
each thread have to execute the handlerton commit instead.
 
Howver, since we are keeping the lock from the previous stage, we
need to unlock it if we skip the stage. */
 
change_stage(thd, Stage_manager::COMMIT_STAGE,
final_queue, &LOCK_sync, &LOCK_commit)
......
}

  

change_stage 函数的注释如下(翻译)

Enter a stage of the ordered commit procedure.
进入一个有序提交过程
  entering is stage is done by:
 
  - 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.
    如果队列是空,则该线程是这个阶段的领导者,它负责处理整个队列中的线程
 
  - If the queue was not empty, the thread is a follower and can go
    waiting for the commit to finish.
    如果队列不是空,该线程是随从者,应该等待直到提交完成
 
  The function will lock the stage mutex if it was designated the
  leader for the phase.
    如果他是这个阶段的领导者,这个函数将会锁住 状态互斥器(应该是有leader时,函数会加锁的意思)
 
  @param thd    Session structure
  @param stage  The stage to enter
  @param queue  Queue of threads to enqueue for the stage
  @param stage_mutex Mutex for the stage
 
  @retval true  The thread should "bail out" and go waiting for the
                commit to finish
  @retval false The thread is the leader for the stage and should do
                the processing.

  

 

flush阶段

    线程们首先进入队列,第一个进入的是leader 后面的flower。flower等待,leader 持有Lock_log_mutex,

然后获取队列中所有事务的binlog,将binlog write到磁盘,最后通知dump线程dump binlog。

 

sync阶段

    flush阶段的leader带领整个队列在sync队列里排队,如果这时sync 队列为空,leader仍为leader,如果非空,则leader已经他的flower都变成flower。所以一日为flower终身为flower,一日为leader可能为flower。

    此时的leader带领队列sync binlog 将binlog 落盘。

    如果sync_binlog !=1 在进入sync阶段后,马上释放Lock_log_mutex,获取lock_sync_mutex,让其他事务进入flush阶段。

    如果sync_binlog==1,则不能释放Lock_log_mutex,如果释放,那么有可能dump线程将还未落盘的binlog 发送到从库,一旦主库宕机,会导致从库数据比主库多。所以实际上flush 和 sync阶段是不能并行的。

 

commit阶段

    leader线程首先释放lock_sync_mutex 和 Lock_log_mutex(如果有),然后获取lock_commit_mutex。接下来逻辑如下:

    核心逻辑在process_commit_stage_queue函数中

void MYSQL_BIN_LOG::process_commit_stage_queue(THD *thd, THD *first)
{
for (THD *head= first ; head ; head = head->next_to_commit){
      ......
      ha_commit_low(head, all, false);
      ......
      }
}
 
int ha_commit_low(THD *thd, bool all, bool run_after_commit){
     for (; ha_info; ha_info= ha_info_next)
    {
        ht->commit(ht, thd, all)
    }
}
 
下面是 innodb 的commit函数
innobase_commit(  handlerton*        hton, THD*       thd, bool       commit_trx){
    ......
    /* Don't do write + flush right now. For group commit
  to work we want to do the flush later.*/
    现在不执行 write & flush 操作。对于group commit我们想要稍后再flush。
  trx->flush_log_later = TRUE;
  innobase_commit_low(trx);
  trx->flush_log_later = FALSE;
    .......
    
/* Now do a write + flush of logs. */
  trx_commit_complete_for_mysql(trx);
}
 
void trx_commit_in_memory(  trx_t*    trx,  lsn_t     lsn){
    ......
    if (trx->flush_log_later) {
   trx->must_flush_log_later = TRUE;
  }
    ......
}
 
UNIV_INTERN
void trx_commit_complete_for_mysql(
/*==========================*/
 trx_t*  trx)    /*!< in/out: transaction */
{
 ut_a(trx);
 
if (!trx->must_flush_log_later || thd_requested_durability(trx->mysql_thd) == HA_IGNORE_DURABILITY) {
  return;
 }
 
trx_flush_log_if_needed(trx->commit_lsn, trx);
trx->must_flush_log_later = FALSE;}

  

 

从函数数可见,是将队列中的事务一次按顺序提交,注意commit阶段没有新队列哦,还是sync阶段的队列。这就保证了binlog刷盘的顺序和事务提交的顺序是一致的。

从第二段程序中可以看到 thd->durability_property设置为HA_IGNORE_DURABILITY,这意味着,innndb不会持久化redo log

 

从整体看,三个阶段包含了两个队列,各个阶段之间有mutex保护,队列之间是顺序的,flush 和 sync阶段内,binlog和redo log 是group commit,所以效率是非常高的,commit阶段是顺序commit的。但是不涉及flush操作所以也很快。