1. 程式人生 > 其它 >MySQL筆記三-事務-group commit

MySQL筆記三-事務-group commit

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 說明日誌已經被刷到磁盤裡了,跳過 3,if 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; 2,第二階段,commit階段,binlog write&&flush,redo log commit。 設定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了。(丁奇老師一圖勝過千言萬語,我就不自己畫了)
什麼是xid值 binlog 解析後可以看到 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執行緒dumpbinlog。 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操作所以也很快。