[mysql bug] bug#65389 mvcc is broken with implicit lock
該bug在5.5.26中被修複,changelog的描述如下:
innodb表的某行記錄被删除,然後再插入了一個相同pk值的行。另外一個并發事務能夠成功的lock住記錄。當使用到二級索引來掃描以lock這個記錄時,可能會觸發bug。
之前對隐式鎖的概念不是很清晰,周末用gdb簡單的跟了一下,理了一下backtrace。
.
先來理一理,什麼是隐式鎖implicit lock。
隐式鎖是innodb使用的一種延遲加鎖政策,當記錄鎖沖突并不頻繁時,頻繁加/釋放鎖的開銷是很大的。隐式鎖并不是真正的加鎖,隻是一種标記,是以開銷很低。
#############begin##############
隐式鎖
lock 是一種悲觀的順序化機制。它假設很可能發生沖突,是以在操作資料時,就加鎖。
如果沖突的可能性很小,多數的鎖都是不必要的。
innodb 實作了一個延遲加鎖的機制,來減少加鎖的數量,在代碼中稱為隐式鎖(implicit lock)。
隐式鎖中有個重要的元素,事務id(trx_id).隐式鎖的邏輯過程如下:
a. innodb的每條記錄中都一個隐含的trx_id字段,這個字段存在于簇索引的b+tree中。
b. 在操作一條記錄前,首先根據記錄中的trx_id檢查該事務是否是活動的事務(未送出或復原).
如果是活動的事務,首先将隐式鎖轉換為顯式鎖(就是為該事務添加一個鎖)。
c. 檢查是否有鎖沖突,如果有沖突,建立鎖,并設定為waiting狀态。如果沒有沖突不加鎖,跳到e。
d. 等待加鎖成功,被喚醒,或者逾時。
e. 寫資料,并将自己的trx_id寫入trx_id字段。page lock可以保證操作的正确性。
相關代碼:
a. lock_rec_convert_impl_to_expl()将隐式鎖轉換成顯示鎖。
b. 加鎖和測試行鎖沖突都用lock_rec_lock(),它的第一個參數表示是否是隐式鎖。是以要特别
注意這個參數。如果為true,在沒有沖突時并不會加鎖。
c. 測試行鎖的沖突的具體内容在lock_rec_has_wait()
d. 建立waiting鎖是lock_rec_enqueue_waiting()
e. 建立行鎖是lock_rec_add_to_queue()
– 隐式鎖的特點
a. 隻有在很可能發生沖突時才加鎖,減少了鎖的數量。
b. 隐式鎖是針對被修改的b+tree記錄,是以都是record類型的鎖。不可能是gap或next-key類型。
– 隐式鎖的使用
a. insert操作隻加隐式鎖,不需要顯示加鎖。
b. update,delete在查詢時,直接對查詢用的index和主鍵使用顯示鎖,其他索引上使用隐式鎖。
理論上說,可以對主鍵使用隐式鎖的。提前使用顯示鎖應該是為了減少死鎖的可能性。
insert,update,delete對b+tree們的操作都是從主鍵的b+tree開始,是以對主鍵加鎖可以
有效的阻止死鎖。
– secondary index上的隐式鎖
前邊說了, trx_id隻存在于主鍵上,那麼輔助索引上如何來實作隐式索引呢?
顯然是要通過輔助索引中的主鍵值,在主鍵b+tree上進行二次查找。這個開銷是很大的。
innodb對這個過程有一個優化:
a. 每個頁上有一個max_trx_id,每次修改輔助索引的記錄時,都會更新這個最大事務id。
b. 當判斷是否要将隐式鎖變為顯式鎖時,先将頁面的max_trx_id和事務清單的最小trx_id
比較。如果max_trx_id比事務清單的最小trx_id還小,那麼就不需要轉換為顯示鎖了。
###################end#########################
簡單的記錄下backtrace如下:
row_search_for_mysql
|–> sel_set_rec_lock 加記錄鎖函數
|–>如果是聚集索引:lock_clust_rec_read_check_and_lock
|–>如果是二級索引:lock_sec_rec_read_check_and_lock
|–>lock_rec_convert_impl_to_expl 隻有這個檔案page記錄的最大事務id>=目前事務清單上的最小事務id時,或者目前正在進行recovery時,一些事務可能在記錄上有一個隐式x鎖
|–>判斷是否存在持有隐式鎖的事務
>>如果是聚集索引:impl_trx = lock_clust_rec_some_has_impl
>>如果是二級索引:impl_trx = lock_sec_rec_some_has_impl_off_kernel
|–>做一些檢查
>>目前二級索引頁上的max_trx_id小于當先事務清單上最小事務id,
且不在recovery時,直接傳回null
>>檢查事務id是否有效lock_check_trx_id_sanity,頁面可能被損壞
|–>row_vers_impl_x_locked_off_kernel 檢查是否有事務插入或修改了二級索引記錄,如果有,則傳回該trx
|–>如果存在impl_trx
>>如果impl_trx沒有持有顯式鎖,則将其轉換為顯式鎖(lock_rec_has_expl) ,
并加入到隊列中(lock_rec_add_to_queue)
|–>lock_rec_lock
|–>lock_rec_queue_validate
函數row_vers_impl_x_locked_off_kernel用于檢查是否有事務插入或修改了二級索引記錄,
以下是簡單的記錄:
———————
1)查找二級索引對應的聚集索引記錄
clust_rec = row_get_clust_rec(btr_search_leaf, rec, index,
&clust_index, &mtr);
這是個耗時操作,是以會釋放kernel mutex;當然釋放鎖也要遵守latch order約定。在cluster index 記錄上的latch鎖住了版本棧的頂部,也會保留purge_latch來鎖住版本棧的底部。
row_get_clust_rec函數的注釋:
fetches the clustered index record for a secondary index record. the latches
on the secondary index record are preserved.
@return record or null, if no record found */
當clust_rec為null時,傳回null,這種情況比較少見。
根據注釋的解釋,在函數row_undo_mod_remove_clust_low()中我們已經移除了clust rec,而這時候purge還在清理和移除由之前版本的聚集索引記錄配置設定的二級索引記錄。這種情況下在二級索引記錄上沒有任何隐式鎖,因為一個已經修改了二級索引記錄的活躍事務同樣也修改了聚集索引記錄。在復原時也是在聚集索引之前undo二級索引。
2)加latch
mtr_s_lock(&(purge_sys->latch), &mtr);
3)判斷clust rec中的trx_id是否還是活躍id,如果不是活躍的,則表明沒有隐式鎖
4)檢視舊版本記錄
/* we look up if some earlier version, which was modified by the trx_id
transaction, of the clustered index record would require rec to be in
a different state (delete marked or unmarked, or have different field
values, or not existing). if there is such a version, then rec was
modified by the trx_id transaction, and it has an implicit x-lock on
rec. note that if clust_rec itself would require rec to be in a
different state, then the trx_id transaction has not yet had time to
modify rec, and does not necessarily have an implicit x-lock on rec. */
5)進入for(;;)循環
trx_undo_prev_version_build() 擷取前一個版本的聚集索引記錄
if (prev_version == null) {
如果事務是活躍事務,表面是剛插入的記錄,含有隐式x-lock,獲得根據trx_id獲得trx
如果非活躍事務,則沒有,trx=null
然後從for循環裡break
}
擷取prev_version的删除标記
vers_del = rec_get_deleted_flag(prev_version, comp);
擷取prev_version的事務id
prev_trx_id = row_get_rec_trx_id(prev_version, clust_index,
clust_offsets);
###
if (vers_del && trx_id != prev_trx_id) {
mutex_enter(&kernel_mutex);
break;
###這裡存在bug#65389,這段判斷是沒有必要的,應該删除
因為被删除的二級索引記錄項可能會被随後的insert重用,導緻這裡的判斷為true。
根據聚集索引記錄建構索引項
row = row_build(row_copy_pointers, clust_index, prev_version,
clust_offsets, null, &ext, heap);
entry = row_build_index_entry(row, ext, index, heap);
當繼續往下走時,該事務trx_id依然是活躍的,并修改了之前的版本,檢查prev_version是否需要rec在一個不同的狀态(翻譯自注釋)
if (0 == cmp_dtuple_rec(entry, rec, offsets)) //比較建構的entry和二級索引記錄是否相同,bug#65389的test case是不相同的
{
//還沒跟過,待定….
}else if (!rec_del) { //rec_del為false
trx = trx_get_on_id(trx_id);
繼續判斷如果trx_id和prev_trx_id不同,break。
version = prev_version;
繼續for循環
6)
最後傳回 trx或者null
留下的不太清楚的地方:
1.鎖的轉換、加入隊列、死鎖判斷、頁面分裂時的鎖遷移
2.二級索引/聚簇索引如何進行undo log管理