程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 數據庫知識 >> MYSQL數據庫 >> MySQL綜合教程 >> MySQL源代碼:如何對讀寫鎖進行處理

MySQL源代碼:如何對讀寫鎖進行處理

編輯:MySQL綜合教程

轉載請署名:印風
-----------------------------------------------------------
最近碰到一個問題,線上一台機器在等待信號量時間過長,mysql的監控線程認為此時mysqld已經hang住了,於是自殺重啟。這裡涉及到一個有趣的問題,也就是mysql如何對讀寫鎖進行處理。
主要包括三個部分:
1. 建鎖
2. 加鎖
3. 解鎖
4. 監控鎖
 以下內容基於Percona5.5.18進行分析
 
1.創建鎖
鎖的創建實際上就是初始化一個RW結構體(rw_lock_t),實際調用函數如下:
 
# define rw_lock_create(K, L, level)                                 \ 
         rw_lock_create_func((L),#L) 
 
在rw_lock_create上有三個參數,在實際場景鎖時只用到第2個參數
其中K表示mysql_pfs_key_t,level顯示當前的操作類型(起碼看起來是的,在文件sync0sync.h中定義),看起來k是為performance schema准備的,而k代表了當前操作所在的層次。
例如:purge線程的讀寫鎖創建:
 
rw_lock_create(trx_purge_latch_key, 
                 &purge_sys->latch,SYNC_PURGE_LATCH); 
 
我們進去rw_lock_create_func看看到底是怎麼創建的。
可以看到這個函數的邏輯其實很簡單:
lock->lock_word =X_LOCK_DECR;    //關鍵字段
用於限制讀寫鎖的最大並發數,代碼裡的注釋如下:
 
/* We decrement lock_word by this amountfor each x_lock. It is also the
start value for the lock_word, meaning thatit limits the maximum number
of concurrent read locks before the rw_lockbreaks. The current value of
0x00100000 allows 1,048,575 concurrentreaders and 2047 recursive writers.*/ 
 
在嘗試加鎖時會調用rw_lock_lock_word_decr減少lock_word
 在初始化一系列變量後,執行:
 
lock->event = os_event_create(NULL); 
lock->wait_ex_event = os_event_create(NULL); 
os_event_create用於創建一個系統信號,實際上最終創建的還是互斥量(os_fast_mutex_init(&(event->os_mutex));以及條件變量(os_cond_init(&(event->cond_var));)
最後將lock加入到全局鏈表rw_lock_list中
 
2.加鎖
加鎖函數由宏定義,實際調用函數為:
1)寫鎖
 
# define rw_lock_x_lock(M)                                          \ 
         rw_lock_x_lock_func((M),0, __FILE__, __LINE__) 
 
當申請寫鎖時,執行如下步驟:
(1).調用rw_lock_x_lock_low函數去獲取鎖,如果得到鎖,則rw_x_spin_round_count += i後直接返回,如果得不到鎖,繼續執行
(2).loop過程中只執行一次rw_x_spin_wait_count++
(3).在毫秒級別的loop多次等待
 
while (i < SYNC_SPIN_ROUNDS 
                          && lock->lock_word <= 0) { 
                            if(srv_spin_wait_delay) { 
                                     ut_delay(ut_rnd_interval(0, 
                                                                  srv_spin_wait_delay)); 
                            } 
                            i++; 
                   } 
 
這裡涉及到兩個系統變量:
innodb_sync_spin_loops(SYNC_SPIN_ROUNDS)
innodb_spin_wait_delay(srv_spin_wait_delay)
 
在SYNC_SPIN_ROUNDS循環裡調用函數ut_delay,這個函數很簡單,就是做了delay*50次空循環
 
Ut_delay(uint delay): 
         for(i = 0; i < delay * 50; i++) { 
                   j+= i; 
                   UT_RELAX_CPU(); 
         } 
其中,UT_RELAX_CPU()會調用匯編指令來獨占CPU,以防止線程切換
(4).如果loop的次數等於SYNC_SPIN_ROUNDS,調用os_thread_yield(實際調用pthread_yield,導致調用線程放棄CPU的占用)將線程掛起;否則挑到1繼續loop
(5).在sync_primary_wait_array裡獲取一個cell(占個坑?)。調用sync_array_reserve_cell,看起來有1000個坑位(sync_primary_wait_array->n_cells)
(6).再次調用rw_lock_x_lock_low函數嘗試獲取鎖,若成功獲得,則返回
(7).調用sync_array_wait_event等待條件變量,然後返回1繼續loop
具體的加鎖函數(rw_lock_x_lock_low)稍後分析
 
2)讀鎖
 
# define rw_lock_s_lock(M)                                          \ 
         rw_lock_s_lock_func((M),0, __FILE__, __LINE__) 
 
這個函數定義在sync0rw.ic裡,函數也很簡單,如下:
 
   if (rw_lock_s_lock_low(lock, pass, file_name, line)) { 
       return; /* Success */ 
    }else { 
       /* Did not succeed, try spin wait */ 
       rw_lock_s_lock_spin(lock, pass, file_name, line); 
       return; 
}   
 
這裡首先調用rw_lock_s_lock_low進行加鎖,如果加鎖不成功,則調用rw_lock_s_lock_spin進行等待,rw_lock_s_lock_spin的代碼邏輯與rw_lock_x_lock_func有些相似,這裡不再贅述。
在rw_lock_s_lock_spin裡會遞歸的調用到rw_lock_s_lock_low函數;
 
看起來實際的加鎖和解鎖操作是通過對計數器來控制的,
(1)在函數rw_lock_s_lock_low中
rw_lock_lock_word_decr (lock, 1),對lock->lock_word減去1
減數成功返回true,否則返回false
這部分的邏輯還是很簡單的。
 
(2)在函數rw_lock_x_lock_low中,調用:
rw_lock_lock_word_decr(lock, X_LOCK_DECR),對lock->lock_word減去X_LOCK_DECR
減數成功後,執行:
 
rw_lock_set_writer_id_and_recursion_flag(lock,pass ? FALSE : TRUE)來設置: 
lock->writer_thread = s_thread_get_curr_id() 
lock->recursive = TRUE 
 
然後調用rw_lock_x_lock_wait函數等待lock->lock_word=0,也就是說等待所有的讀鎖退出。
 
看到一個比較有意思的現象,在.ic的代碼裡看到使用了宏
INNODB_RW_LOCKS_USE_ATOMICS,這是跟gcc的版本相關的,通過使用gcc的內建函數來實現原子操作。
 
3.解鎖
解鎖操作包括解除讀鎖(#define rw_lock_s_unlock(L) rw_lock_s_unlock_gen(L, 0))和解除寫鎖操作(#definerw_lock_x_unlock(L) rw_lock_x_unlock_gen(L, 0))
實際調用函數為rw_lock_s_unlock_func和rw_lock_x_unlock_func
 
1)解除讀鎖(rw_lock_s_unlock_func)
增加計數rw_lock_lock_word_incr(lock, 1)
 
2)解除寫鎖(rw_lock_x_unlock_func)
執行如下操作
(1)如果是最後一個遞歸調用鎖的線程,設置lock->recursive= FALSE; 代碼裡的注釋如下:
 
/* lock->recursive flag also indicatesif lock->writer_thread is
   valid or stale. If we are the last of the recursive callers
   then we must unset lock->recursive flag to indicate that the
   lock->writer_thread is now stale.
   Note that since we still hold the x-lock we can safely read the
   lock_word. */ 
 
(2)增加計數rw_lock_lock_word_incr(lock,X_LOCK_DECR) == X_LOCK_DECR,這時候需要向等待鎖的線程發送信號:
 
if (lock->waiters) { 
     rw_lock_reset_waiter_flag(lock); 
     os_event_set(lock->event);    
     sync_array_object_signalled(sync_primary_wait_array); 

 
os_event_set函數會發送一個pthread_cond_broadcast給等待的線程
 
4.監控讀寫鎖
為了防止mysqld被hang住導致的長時間等待rw鎖,error監控線程會對長時間等待的線程進行監控。這個線程每1秒loop一次
(os_event_wait_time_low(srv_error_event, 1000000, sig_count);)
函數入口:srv_error_monitor_thread
函數sync_array_print_long_waits()用於處理長時間等待信號量的線程,流程如下:
1. 查看sync_primary_wait_array數組中的所有等待線程。
->大於240秒時,向錯誤日志中輸出警告,設置noticed = TRUE;
->大於600秒時,設置fatal =TRUE;
2.當noticed為true時,打印出innodb監控信息,然後sleep30秒
3. 返回fatal值
 
當函數sync_primary_wait_array返回true時,對於同一個等待線程還會有十次機會,也就是300 + 1*10(監控線程每次loop sleep 1s)秒的時間;如果挺不過去,監控線程就會執行一個斷言失敗:
 
if (fatal_cnt > 10) { 
                   fprintf(stderr, 
                            "InnoDB:Error: semaphore wait has lasted" 
                            "> %lu seconds\n" 
                            "InnoDB:We intentionally crash the server," 
                            "because it appears to be hung.\n", 
                             (ulong) srv_fatal_semaphore_wait_threshold); 
  
                            ut_error; 
                   } 
 
ut_error是一個宏:
 
#define ut_error      assert(0) 
斷言失敗導致mysqld crash
 在函數srv_error_monitor_thread裡發現一個比較有意思的參數srv_kill_idle_transaction,對應的系統變量為innodb_kill_idle_transaction,用於清理在一段時間內的空閒事務。這個變量指定了空閒事務的最長時間。具體實現分析,且聽下回分解

作者 記錄成長之路

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved