MySQL系列:innodb引擎分析之線程並發同步機制,mysqlinnodb

來源:互聯網
上載者:User

MySQL系列:innodb引擎分析之線程並發同步機制,mysqlinnodb

innodb是一個多線程並發的儲存引擎,內部的讀寫都是用多線程來實現的,所以innodb內部實現了一個比較高效的並發同步機制。innodb並沒有直接使用系統提供的鎖(latch)同步結構,而是對其進行自己的封裝和實現最佳化,但是也相容系統的鎖。我們先看一段innodb內部的注釋(MySQL-3.23):

Semaphore operations in operating systems are slow: Solaris on a 1993 Sparc takes 3 microseconds (us) for a lock-unlock pair and Windows NT on a 1995 Pentium takes 20 microseconds for a lock-unlock pair. Therefore, we have toimplement our own efficient spin lock mutex. Future operating systems mayprovide efficient spin locks, but we cannot count on that.

大概意思是說1995年的時候,一個Windows NT的 lock-unlock所需要耗費20us,即使是在Solaris 下也需要3us,這也就是他為什麼要實現自訂latch的目的,在innodb中作者實現了系統latch的封裝、自訂mutex和自訂rw_lock。下面我們來一一做分析。

1 系統的mutex和event    在innodb引擎當中,封裝了作業系統提供的基本mutex(互斥量)和event(訊號量),在WINDOWS下的實現暫時不做記錄,主要還是對支援POSIX系統來做介紹。在POSIX系統的實現是os_fast_mutex_t和os_event_t。os_fast_mutex_t相對簡單,其實就是pthread_mutex。定義如下:
typedef pthread_mutex os_fast_mutex_t;
而os_event_t相對複雜,它是通過os_fast_mutex_t和一個pthread_cond_t來實現的,定義如下:
typedef struct os_event_struct    {        os_fast_mutex_t        os_mutex;        ibool                  is_set;        pthread_cond_t         cond_var;    }os_event_t;
以下是os_event_t的兩線程訊號控制的例子流程:

對於系統的封裝,最主要的就是os_event_t介面的封裝,而在os_event_t的封裝中,os_event_set、os_event_reset、os_event_wait這三個方法是最關鍵的。
2 CPU原子操作在innodb的mutex(互斥量)的實現中,除了引用系統的os_mutex_t以外,還使用了原子操作來進行封裝一個高效的mutex實現。在系統支援原子操作的情況下,會採用自己封裝的mutex來做互斥,如果不支援,就使用os_mutex_t。在gcc 4.1.2之前,編譯器是不提供原子操作的API的,所以在MySQL-.3.23的innodb中自己實現了一個類似__sync_lock_test_and_set的實現,代碼是採用了彙編實現:
  asm volatile("movl $1, %%eax; xchgl (%%ecx), %%eax" :               "=eax" (res), "=m" (*lw) :               "ecx" (lw));
這段代碼是什麼意思呢?其實就是將lw的值設定成1,並且返回設定lw之前的值(res),這個過程都是CPU需要回寫記憶體的,也就是CPU和記憶體是完全一致的。除了上面設定1以外,還有一個複位的實現,如下:
 asm volatile("movl $0, %%eax; xchgl (%%ecx), %%eax" :               "=m" (*lw) :   "ecx" (lw) :  "eax"); 
這兩個函數交叉起來使用,就是gcc-4.1.2以後的__sync_lock_test_and_set的基本實現了。在MySQL-5.6的Innodb引擎當中,將以上彙編代碼採用了__sync_lock_test_and_set代替,我們可以採用原子操作實現一個簡單的mutex.
#define LOCK() while(__sync_lock_test_and_set(&lock, 1)){}#define UNLOCK() __sync_lock_release(&lock)
以上就是一個基本的無鎖結構的mutex,在linux下測試確實比pthread_mutex效率要高出不少。當然在innodb之中的mutex實現不會僅僅這麼簡單,需要考慮的因素還是比較多的,例如:同線程多次lock、lock自旋的周期、死結檢測等。
3 mutex的實現在innodb中,帶有原子操作的mutex自訂互斥量是基礎的並發和同步的機制,目的是為了減少CPU的環境切換和提供高效率,一般mutex等待的時間不超過100微秒的條件下,這種mutex效率是非常高的。如果等待的時間長,建議選擇os_mutex方式。雖然自訂mutex在自旋時間超過自旋閾值會進入訊號等待狀態,但是整個過程相對os_mutex來說,效率太低,這不是自訂mutex的目的。自訂mutex的定義如下:
struct mutex_struct{ ulint lock_word;                             /*mutex原子控制變數*/ os_fast_mutex_t os_fast_mutex;     /*在編譯器或者系統部支援原子操作的時候採用的系統os_mutex來替代mutex*/ ulint waiters;                                  /*是否有線程在等待鎖*/ UT_LIST_NODE_T(mutex_t)list;     /*mutex list node*/ os_thread_id_t thread_id;              /*獲得mutex的線程ID*/ char* file_name;                            /*mutex lock操作的檔案/ ulint line;                                       /*mutex lock操作的檔案的行數*/ ulint level;                                     /*鎖層ID*/ char* cfile_name;                          /*mute建立的檔案*/ ulint cline;                                    /*mutex建立的檔案行數*/ ulint magic_n;                              /*魔法字*/};
在自訂mute_t的介面方法中,最核心的兩個方法是:mutex_enter_func和mutex_exit方法
    mutex_enter_func                    獲得mutex鎖,如果mutex被其他線程佔用,先會自旋SYNC_SPIN_ROUNDS,然後                                                         再等待佔用鎖的線程的訊號
    mutex_exit                                 釋放mutex鎖,並向等待線程發送可以搶佔mutex的訊號量
3.1 mutex_enter_func流程圖:


以上流程主要是在mutex_spin_wait這個函數中實現的,從其代碼中可以看出,這個函數是儘力讓線程在自旋周期內獲得鎖,因為一旦進入cell_wait狀態,至少的耗費1 ~ 2次系統調用,在cell_add的時候有可能觸發os_mutex_t的鎖等待和一定會event_wait等待。這比系統os_mutex效率會低得多。如果在調試狀態下,獲得鎖的同時會向thread_levels的添加一條正在使用鎖的資訊,以便死結檢查和調試。
3.2 mutex_exit流程圖
3.4 mutex_t的記憶體結構關係圖
3.4mutex獲得鎖和釋放鎖的
4 rw_lock的實現innodb為了提高讀的效能,自訂了read write lock,也就是讀寫鎖。其設計原則是:
    1、同一時刻允許多個線程同時讀取記憶體中的變數
    2、同一時刻只允許一個線程更改記憶體中的變數
    3、同一時刻當有線程在讀取變數時不允許任何線程寫存在
    4、同一時刻當有線程在更改變數時不允許任何線程讀,也不允許出自己以外的線程寫(線程內可以遞迴佔有鎖)。
    5、當有rw_lock處於線程讀模式下是有線程寫等待,這時候如果再有其他線程讀請求鎖的時,這個讀請求將處於等待前面寫完成。
從上面5點我們可以看出,rw_lock在被佔用是會處於讀狀態和寫狀態,我們稱之為S-latch(讀共用)和X-latch(寫獨佔),《MySQL技術內幕:innodb引擎》對S-latch和X_latch的描述如下:
  S-latch X-latch
S-latch 相容 不相容
X-latch 不相容 不相容
innodb中的rw_lock是在建立在自訂mutex_t之上的,所有的控制是基於mutex和thread_cell的。以下是rw_lock_t的結構定義:
struct rw_lock_struct{ ulint reader_count;                         /*獲得S-LATCH的讀者個數,一旦不為0,表示是S-LATCH鎖*/ ulint writer;                                     /*獲得X-LATCH的狀態,主要有RW_LOCK_EX、RW_LOCK_WAIT_EX、                                                                                           RW_LOCK_NOT_LOCKED, 處於RW_LOCK_EX表示是一個x-latch                                                            鎖,RW_LOCK_WAIT_EX的狀態表示是一個S-LATCH鎖*/  os_thread_id_t writer_thread;        /*獲得X-LATCH的線程ID或者第一個等待成為x-latch的線程ID*/ ulint writer_count;                         /*同一線程中X-latch lock次數*/ mutex_t mutex;                             /*保護rw_lock結構中資料的互斥量*/ ulint pass;                                      /*預設為0,如果是非0,表示線程可以將latch控制權轉移給其他線程,                                                            在insert buffer有相關的調用*/  ulint waiters;                                 /*有讀或者寫在等待獲得latch*/ ibool writer_is_wait_ex; UT_LIST_NODE_T(rw_lock_t) list; UT_LIST_BASE_NODE_T(rw_lock_debug_t) debug_list; ulint level;                                     /*level標示,用於檢測死結*/ /*用於調試的資訊*/ char* cfile_name;                          /*rw_lock建立時的檔案*/ ulint cline;                                     /*rw_lock建立是的檔案行位置*/ char* last_s_file_name;                 /*最後獲得S-latch時的檔案*/ char* last_x_file_name;                 /*最後獲得X-latch時的檔案*/ ulint last_s_line;                            /*最後獲得S-latch時的檔案行位置*/ ulint last_x_line;                           /*最後獲得X-latch時的檔案行位置*/ ulint magic_n;                              /*魔法字*/};

 在rw_lock_t獲得鎖和釋放鎖的主要介面是:rw_lock_s_lock_func、rw_lock_x_lock_func、rw_lock_s_unlock_func、rw_lock_x_unlock_func四個關鍵函數。 其中rw_lock_s_lock_func和rw_lock_x_lock_func中定義了自旋函數,這兩個自旋函數的流程和mutex_t中的自旋函數實現流程是相似的,其目的是要在自旋期間就完成鎖的獲得。具體細節可以查看sync0rw.c中的rw_lock_s_lock_spin/rw_lock_x_lock_func的代碼實現。從上面結構的定義和函數的實現可以知道rw_lock有四種狀態:
  RW_LOCK_NOT_LOCKED                    空閑狀態
  RW_LOCK_SHARED                             處於多線程並發都狀態
  RW_LOCK_WAIT_EX                            等待從S-latch成為X-latch狀態
  RW_LOCK_EX                                       處於單線程寫狀態
 以下是這四種狀態遷移:
通過上面的遷徙我們可以很清楚的瞭解rw_lock的運作機理,除了狀態處理以外,rw_lock還為debug提供了介面,我們可以通過記憶體關係圖來瞭解他們的關係:
5 死結檢測與調試  innodb除了實現自訂mutex_t和rw_lock_t以外,還對這兩個類型的latch做了調試性死結檢測,這大大簡化了innodb的latch調試,latch的狀態和資訊在可以即時查看到,但這僅僅是在innodb的調試版本中才能看到。與死結檢測相關的模組主要是mutex level、rw_lock level和sync_cell。latch level相關的定義:
/*sync_thread_t*/    struct sync_thread_struct    {         os_thread_id_tid;            /*佔用latch的thread的id*/         sync_level_t*levels;         /*latch的資訊,sync_level_t結構內容*/     };        /*sync_level_t*/    struct sync_level_struct    {         void*latch;                    /*latch控制代碼,是mute_t或者rw_lock_t的結構指標*/         ulintlevel;                     /*latch的level標識ID*/    };

在latch獲得的時候,innodb會調用mutex_set_debug_info函數向sync_thread_t中加入一個latch被獲得的狀態資訊,其實就是包括獲得latch的線程id、獲得latch的檔案位置和latch的層標識(具體的細節可以查看mutex_enter_func和mutex_spin_wait)。只有佔用了latch才會體現在sync_thread_t中,如果只是在等待獲得latch是不會加入到sync_thread_t當中的。innodb可以通過sync_thread_levels_empty_gen函數來輸出所有latch等待依賴的cell_t序列,追蹤線程等待的位置。5.1sync_thread_t與sync_level_t的記憶體結構關係:
sync_thread_level_arrays的長度是OS_THREAD_MAX_N(linux下預設是10000),也就是和最大線程個數是一樣的。
levels的長度是SYNC_THREAD_N_LEVELS(10000)。
5.2死結與死結檢測什麼是死結,通過以下的例子我們可以做個簡單的描述:
    線程A                                         線程B
    mutex1    enter                 mutex2        enter
    mutex2    enter                 mutex1        enter
    執行任務                           執行任務
    mutex2    release             mutex1          release
    mutex1    release             mutex2           release
   上面兩個線程同時啟動並執行時候,可能產生死結的情況,就是A線程獲得了mutex1正在等待mutex2的鎖,同時線程2獲得了mutex2正在等待mutex1的鎖。在這種情況下,線程1在等線程2,線程2在等線程就造成了死結。

  瞭解了死結的概念後,我們就可以開始分析innodb中關於死結檢測的流程細節,innodb的檢車死結的實質就是判斷要進行鎖的latch是否會產生所有線程的閉環,這個是通過sync_array_cell_t的內容來判斷的。在開始等待cell訊號的時候,會判斷將自己的狀態資訊放入sync_array_cell_t當中,在進入os event wait之前會調用sync_array_detect_deadlock來判斷是否死結,如果死結,會觸發一個異常。死結檢測的關鍵在與sync_array_detect_deadlock函數。以下是檢測死結的流程描述:
    1、將進入等待的latch對應的cell作為參數傳入到sync_array_detect_deadlock當中,其中start的參數和依賴的cell參 數填寫的都是這個cell自己。
    2、進入sync_array_detect_deadlock先判斷依賴的cell是否正在等待latch,如果沒有,表示沒有死結,直接返回.如果有,先判斷等待的鎖被哪個線程佔用,並獲得佔用線程的id,通過佔用線程的id和全域的sync_array_t  等待cell數組狀態資訊調用sync_array_deadlock_step來判斷等待線程的鎖依賴。    3、進入sync_array_deadlock_step先找到佔用線程的對應cell,如果cell和最初的需要event wait的cell是同一個cell,表示是一個閉環,將產生死結。如果沒有,繼續將查詢到的cell作為參數遞迴調用sync_array_detect_deadlock執行第2步。這是個兩函數交叉遞迴判斷的過程。在檢測死結過程latch控制代碼、thread id、cell控制代碼三者之間環環相扣和遞迴,通過latch的本身的狀態來判斷閉環死結。在上面的第2步會根據latch是mutex和rw_lock的區別做區分判斷,這是由於mutex和rw_lock的運作機制不同造成的。因為關聯式資料庫的latch使用非常頻繁和複雜,檢查死結對於鎖的調試是非常有效,尤其是配合thread_levels狀態資訊輸出來做調試,對死結排查是非常有意義的。
死結:6.總結通過上面的分析可以知道innodb除了實現對作業系統提供的latch結構封裝意外,還提供了原子操作層級的自訂latch,那麼它為什麼要實現自訂latch呢?我個人理解主要是減少作業系統內容相關的切換,提高並發的效率。innodb中實現的自訂latch只適合短時間的鎖等待(最好不超過50us),如果是長時間鎖等待,最好還是使用作業系統提供的,雖然自訂鎖在等待一個自旋周期會進入作業系統的event_wait,但這無疑比系統的mutex lock耗費的資源多。最後我們還是看作者在代碼中的總結:We conclude that the best choice is to set the spin time at 20 us. Then the system should work well on a multiprocessor. On a uniprocessor we have to make sure that thread swithches due to mutex collisions are not frequent, i.e., they do not happen every 100 us or so, because that wastes too much resources. If the thread switches are not frequent, the 20 us wasted in spin loop is not too much. 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.