Innodb行鎖(1):加鎖流程


本文討論的鎖都是innodb 的行鎖,不涉及譬如MDL LOCK/TABLE LOCK等鎖,這也是最常見(jiàn)的。


一、LOCK SYSTEM的拆鎖改進(jìn)簡(jiǎn)述

在8.0種lock system和5.7顯著的不同就是進(jìn)行的鎖的拆分,主要是分為2個(gè)方面

  • 拆分鎖為GLOBAL鎖和shard鎖。
  • 對(duì)于shard鎖來(lái)講,一共有512個(gè)鎖,通過(guò)page和heap no在LOCK SYSTEM獲取鎖的時(shí)候通常只需要上對(duì)應(yīng)部分的shard鎖
  /** Number of page shards, and also number of table shards.
  Must be a power of two */
  static constexpr size_t SHARDS_COUNT = 512;

因此8.0在LOCK SYSTEM MUTEX熱點(diǎn)鎖競(jìng)爭(zhēng)方面已經(jīng)有了改進(jìn),因此在大批量加鎖的情況下,本熱點(diǎn)鎖已經(jīng)在實(shí)際環(huán)境中少見(jiàn)了。比如在5.7 我們大批量加鎖的時(shí)候可能有如下:


image.png

而在實(shí)際運(yùn)維中8.0貌似還沒(méi)見(jiàn)到過(guò),這可能就得益于這里的拆分。

二、對(duì)行進(jìn)行加鎖

如果某個(gè)page已經(jīng)包含了鎖信息,通常會(huì)使用lock_rec_lock_slow函數(shù)進(jìn)行加鎖判定,

  1. 首先需要通過(guò)本事務(wù)本次需要加的鎖和已經(jīng)持有的鎖進(jìn)行判定,如果已經(jīng)有強(qiáng)度更高或者相同的鎖,則不需要加鎖了。(lock_rec_has_expl函數(shù)開(kāi)始)
    判定的時(shí)候就是通過(guò)page no和heap no在LOCK SYSTEM的rec_hash中進(jìn)行查找,而這里加鎖就是前面說(shuō)的拆分后的。其中主要通過(guò)Lock_iter::for_each通過(guò)lambda函數(shù),先定位到rec_hash的某個(gè)鏈表,然后順序訪問(wèn)通過(guò)函數(shù)lock_mode_stronger_or_eq進(jìn)行比較。
    而函數(shù)lock_mode_stronger_or_eq就是進(jìn)行鎖強(qiáng)度的判定的,實(shí)際上就一句
 return (lock_strength_matrix[mode1][mode2] != 0)

static const byte lock_strength_matrix[5][5] = {
    /**         IS     IX       S     X       AI */
    /* IS */ {TRUE, FALSE, FALSE, FALSE, FALSE},
    /* IX */ {TRUE, TRUE, FALSE, FALSE, FALSE},
    /* S  */ {TRUE, FALSE, TRUE, FALSE, FALSE},
    /* X  */ {TRUE, TRUE, TRUE, TRUE, TRUE},
    /* AI */ {FALSE, FALSE, FALSE, FALSE, TRUE}};

也就是通過(guò)兼容矩陣進(jìn)行強(qiáng)度判定。
當(dāng)然這里判定的時(shí)候除了兼容矩陣強(qiáng)度判定還必須是當(dāng)前事務(wù)的鎖才行,因此如下:

lock->trx == trx

要滿(mǎn)足這一條才是前提。并且這里從語(yǔ)法來(lái)講有個(gè)完美轉(zhuǎn)發(fā)右值引入的方式,盡量節(jié)省內(nèi)存消耗。

  1. 當(dāng)然如果不滿(mǎn)足上面的條件,則需要進(jìn)行加鎖,但是在加鎖之前理所應(yīng)當(dāng)?shù)男枰卸ㄊ欠裥枰却#╨ock_rec_other_has_conflicting函數(shù)開(kāi)始)

實(shí)際上需要找到的是是否有事務(wù)持有鎖,而堵塞了本事務(wù)需要獲取的鎖,那么也需要對(duì)LOCK SYSTEM的rec_hash進(jìn)行迭代,同上面一樣,也是通過(guò)Lock_iter::for_each進(jìn)行的遍歷,但是lambda函數(shù)不一樣,這里主要調(diào)用的是lock_rec_has_to_wait函數(shù)進(jìn)行判定,需要滿(mǎn)足的條件主要是:

  • 根據(jù)兼容矩陣判定lock_mode_compatible函數(shù),其實(shí)也是一句話(huà)如下,
return (lock_compatibility_matrix[mode1][mode2]);
  • 其次找到的鎖的持有者不是本事務(wù),如下,
trx != lock2->trx 

這里對(duì)于SQL線(xiàn)程持有的鎖會(huì)設(shè)置為高優(yōu)先級(jí),這個(gè)會(huì)特殊處理,對(duì)于高優(yōu)先級(jí)來(lái)講當(dāng)持有鎖的trx釋放后,會(huì)優(yōu)先持有鎖,這個(gè)在后面會(huì)看到。

經(jīng)過(guò)這個(gè)過(guò)程就能找到本次加鎖需要等待的鎖資源是哪個(gè)。

  1. 如果需要等待,也就是上面的步驟找到堵塞的鎖,那么需要的事情比較多,主要集中在函數(shù)rec_lock.add_to_waitq中。

先是需要新建一個(gè)RecLock的輔助結(jié)構(gòu),主要是線(xiàn)程結(jié)構(gòu)、事務(wù)結(jié)構(gòu)、鎖模式、索引名稱(chēng)、page no/heap no作為構(gòu)造參數(shù)。
然后調(diào)用rec_lock.add_to_waitq,先將鎖模式設(shè)置為 LOCK_WAIT,如下:

bool wait = m_mode & LOCK_WAIT

然后創(chuàng)建lock_t結(jié)構(gòu)并且為其分配內(nèi)存,在分配內(nèi)存的時(shí)候,innodb有一個(gè)rec_pool的概念,在初始化的時(shí)候就已經(jīng)初始化了REC_LOCK_CACHE(8)個(gè)lock_t結(jié)構(gòu),如果加鎖不多那么分配內(nèi)存這一塊就直接從rec_pool中拿,否則需要實(shí)際分配內(nèi)存,其構(gòu)造主要是輸入的space/page no/heap no/鎖模型等信息,主要最后會(huì)調(diào)用lock_rec_set_nth_bit來(lái)設(shè)置lock_t結(jié)構(gòu)的bit位,并且lock->trx->lock.n_rec_locks這個(gè)信息+1,這個(gè)信息是我們show engine看到的鎖數(shù)量的信息。
接著需要做的就是將建立好的lock_t結(jié)構(gòu)放入響應(yīng)的鏈表或者屬性中,調(diào)用的是RecLock::lock_add,這是我們?nèi)缦?/p>

  • lock_rec_insert_to_waiting:將鎖放到相應(yīng)LOCK SYSTEM的rec_hash某個(gè)鏈表(cell)的尾部( Insert lock record to the tail of the queue where the WAITING locks reside)
  • locksys::add_to_trx_locks:將鎖放入到事務(wù)相關(guān)的鏈表trx->lock.trx_locks的尾部
  • lock_set_lock_and_trx_wait:將鎖放入到本事務(wù)trx->lock.wait_lock中

上面將本次等待鎖的信息建立好了,并且放入了響應(yīng)的鏈表或者屬性中,接下來(lái)就是要設(shè)置本事務(wù)的等待事務(wù),這個(gè)主要用于進(jìn)行解鎖或者死鎖監(jiān)控會(huì)用到,調(diào)用的函數(shù)為lock_create_wait_for_edge,其中就是將本事務(wù)的lock.blocking_trx設(shè)置為堵塞者的事務(wù)id,同時(shí)這個(gè)信息也是打印堵塞源頭的信息。

接下來(lái)要設(shè)置本事務(wù)本鎖的相關(guān)信息,調(diào)用RecLock::set_wait_state,比如如下,

  • lock.wait_started:等待的開(kāi)始時(shí)間
  • lock.que_state:鎖的狀態(tài),是否為等待TRX_QUE_LOCK_WAIT(/*!< transaction is waiting for a lock */)。

innodb_trx中的trx_wait_started和事務(wù)狀態(tài)就來(lái)自這里,show engine 里面的事務(wù)狀態(tài)也是這個(gè)。

接下來(lái)一個(gè)簡(jiǎn)單的函數(shù) que_thr_stop(m_thr),這個(gè)函數(shù)主要是如果處于等待啊狀態(tài)TRX_QUE_LOCK_WAIT,則設(shè)置線(xiàn)程屬性QUE_THR_LOCK_WAIT,一旦設(shè)置為這個(gè)屬性就會(huì)獲取一個(gè)LOCK_SYSTEM的slot(srv_slot_t),然后根據(jù)slot的event進(jìn)行等待和喚醒,這個(gè)在lock monitor監(jiān)控線(xiàn)程再詳細(xì)描述。

然后就是返回DB_LOCK_WAIT給上層,最后會(huì)通過(guò)狀態(tài)QUE_THR_LOCK_WAIT將本線(xiàn)程堵塞,并且等待lock monitor監(jiān)控線(xiàn)程的喚醒。

這里還需要注意一點(diǎn),對(duì)于SQL線(xiàn)程的判定有額外的流程,也就是thd_report_row_lock_wait函數(shù)調(diào)入,可以看到SQL線(xiàn)程的鎖有更改的優(yōu)先級(jí)。

  1. 如果不需要等待,則同樣需要將鎖信息寫(xiě)入到lock_t結(jié)構(gòu),可能還需要更新響應(yīng)的鏈表,主要函數(shù)為lock_rec_add_to_queue。

這里需要注意的是,并不一定一定要新建lock_t結(jié)構(gòu),如果本事務(wù)已經(jīng)有了相關(guān)的lock_t結(jié)構(gòu),則設(shè)置一個(gè)bit位就可以了,判定的標(biāo)準(zhǔn)如下,(函數(shù)lock_rec_find_similar_on_page):

  • 事務(wù)是同一個(gè)
  • 鎖模式相同
  • 并且此lock_t的bit結(jié)構(gòu)能夠容納下

但是這里好像只是獲取了LOCK SYSTEM的rec_hash響應(yīng)鏈表(cell)的第一個(gè)lock_t結(jié)構(gòu),并沒(méi)有全部獲取如下:

first_lock = lock_rec_get_first_on_page(hash, block)

當(dāng)然如果上面的條件不成立就需要調(diào)用RecLock::create,新建lock_t結(jié)構(gòu),并且調(diào)用如下:

  • lock_rec_insert_to_granted:將鎖信息放到LOCK SYSTEM的rec_hash相應(yīng)鏈表(cell)的頭部,這和堵塞放到尾部不同,這里也說(shuō)明鎖是有隊(duì)列的。
  • locksys::add_to_trx_locks:將鎖放入到事務(wù)相關(guān)的鏈表trx->lock.trx_locks的尾部,這個(gè)和上面發(fā)生堵塞的情況一致。

三、總結(jié)

這里我們看到如下:

  • 對(duì)于行鎖信息要放到LOCK SYSTEM的rec_hash中,8.0加的是512個(gè) shard lock中的一個(gè),這個(gè)和5.7不同,5.7是一把大鎖。
  • 如果加鎖需要堵塞,則本session會(huì)處于等待狀態(tài),等待的釋放權(quán)歸lock monitor線(xiàn)程所有,也就是說(shuō)如果一個(gè)鎖堵塞了并且超時(shí)了,然后要喚醒那些被其堵塞的事務(wù)是lock monitor線(xiàn)程來(lái)決定的,而且本事務(wù)的超時(shí)也是lock monitor線(xiàn)程來(lái)喚醒的。
  • 加鎖并不一定要新建內(nèi)存,因?yàn)槟J(rèn)有8個(gè)系統(tǒng)持有的rec_pool內(nèi)存。
  • 加鎖并不一定要新建鎖結(jié)構(gòu),因?yàn)榭赡鼙臼聞?wù)持有了響應(yīng)的鎖結(jié)構(gòu),設(shè)置bit就好了
  • 沖突或者兼容的判定來(lái)自static的那個(gè)兼容矩陣
  • SQL線(xiàn)程的事務(wù)的鎖優(yōu)先級(jí)更高,處理上應(yīng)該在等待隊(duì)列中有更高的優(yōu)先級(jí)喚醒。
  • 鎖定成功的結(jié)構(gòu)將會(huì)放到LOCK SYSTEM的rec_hash鏈表(cell)的頭部,而被堵塞的則會(huì)放入其尾部。
  • 鎖的存儲(chǔ)主要集中在trx->lock.trx_locks中和LOCK SYSTEM的rec_hash中,前者比如事務(wù)提交事務(wù)鎖的時(shí)候或者打印事務(wù)鎖信息的時(shí)候肯定是按照事務(wù)為單位的來(lái)進(jìn)行的,比如lock_print_info_all_transactions函數(shù),后者主要用于鎖沖突的查找,因?yàn)闉閔ash結(jié)構(gòu)肯定還是比較快的。

下一篇我們來(lái)分析一下lock monitor線(xiàn)程的超時(shí)喚醒方式,然后再來(lái)看lock monitor線(xiàn)程的死鎖檢測(cè)方式。

四、代碼流程

lock_rec_lock_slow
   locksys::owns_page_shard(block->get_page_id())
   注意這個(gè)鎖,對(duì)lock_sys mutex的優(yōu)化
  ->lock_rec_has_expl(checked_mode, block, heap_no, trx); 
    該session 是否已經(jīng)包含了 本模式的或者更強(qiáng)模式的鎖
    如果是則不需要鎖判斷了
  ->lock_rec_other_has_conflicting,本函數(shù)返回是否有需要等待的鎖
    進(jìn)行鎖沖突判定,如果返回找到的鎖wait_for,就是擁有更強(qiáng)鎖
    ->RecID rec_id{block, heap_no}
      通過(guò)block和heapno構(gòu)建rec id
    ->is_supremum = rec_id.is_supremum()
      是否為sup偽列
    ->Lock_iter::for_each(rec_id, [=](const lock_t *lock) {return (!(lock_rec_has_to_wait(trx, mode, lock, is_supremum))
      使用迭代,帶入的為lambda函數(shù),內(nèi)部也就是循環(huán)比對(duì)
      ->hash_get_nth_cell(hash_table,hash_calc_hash(rec_id.m_fold, hash_table))
        首先根據(jù)rec信息進(jìn)行hash查找,在lock_sys的hash結(jié)構(gòu)中進(jìn)行查找,找到相應(yīng)的鏈表
        也就是lock_sys->rec_hash
      ->for (auto lock = first(list, rec_id); lock != nullptr; lock = advance(rec_id, lock))
        遍歷鏈表信息,每個(gè)信息是一個(gè)lock_rec_t結(jié)構(gòu)
        ->!std::forward<F>(f)(lock) 完美轉(zhuǎn)發(fā),右值引用
          回調(diào) lambda函數(shù),進(jìn)去= 值捕獲
          ->lock_rec_has_to_wait
            ->is_hp = trx_is_high_priority(trx)
              sql線(xiàn)程擁有更高的優(yōu)先級(jí)
            ->lock_mode_compatible
              進(jìn)行鎖強(qiáng)度判定,特殊情況為如果線(xiàn)程優(yōu)先級(jí)更高則忽略
              及l(fā)ock_compatibility_matrix兼容性矩陣
            ->進(jìn)行額外的判定
            如果返回為true則表明有其他事務(wù)的鎖需要等待,如果為false
            則不需要等待
    如果需要等待,則 return (lock),返回等待的lock   
  ->(wait_for != nullptr)
     如果等待的鎖不為空,這里注意幾個(gè)屬性SKIP LOCKED / NOWAIT是語(yǔ)句設(shè)置的時(shí)候的屬性
     這里會(huì)進(jìn)行鎖的判定,這里跳過(guò)SELECT_SKIP_LOCKED/SELECT_NOWAIT
  ->SELECT_ORDINARY
    ->RecLock rec_lock(thr, index, block, heap_no, mode)
      新建一個(gè)RecLock結(jié)構(gòu),lock rec的內(nèi)存結(jié)構(gòu),調(diào)用的為其構(gòu)造函數(shù)
       m_thr(thr),m_trx(thr_get_trx(thr)),m_mode(mode),m_index(index),m_rec_id(rec_id)       
    ->trx_mutex_enter(trx)
      上事務(wù)結(jié)構(gòu)鎖
      Mutex protecting the fields `state` and `lock`
    ->rec_lock.add_to_waitq(wait_for) RecLock::add_to_waitq
      輸入?yún)?shù)為需要等待的lock_t結(jié)構(gòu)
      ->m_mode |= LOCK_WAIT
        首先將鎖設(shè)置為L(zhǎng)OCK_WAIT
      ->prepare()
        RecLock::prepare
        做簡(jiǎn)單檢查先不考慮
      ->lock_t *lock = create(m_trx, prdt)
        進(jìn)行創(chuàng)建lock_t結(jié)構(gòu),調(diào)用RecLock::create
        ->lock_alloc(trx, m_index, m_mode, m_rec_id, m_size)
          RecLock::lock_alloc,這里可以看到輸入的信息都是
          已經(jīng)通過(guò)前面獲取的信息比如鎖模型,當(dāng)前事務(wù)結(jié)構(gòu),space/page no/heap no等等
          ->(trx->lock.rec_cached >= trx->lock.rec_pool.size() ||sizeof(*lock) + size > REC_LOCK_SIZE)
            如果((gdb) p trx->lock.rec_pool.size() $2 = 8)加鎖的lock_t結(jié)構(gòu)數(shù)量大于了8個(gè)就需要分配內(nèi)存了
            否則直接從pool里面拿,系統(tǒng)預(yù)先分配了REC_LOCK_CACHE的lock_t結(jié)構(gòu)內(nèi)存,就是8個(gè)。
          ->lock->trx = trx/lock->index = index/lock->type_mode = LOCK_REC | (mode & ~LOCK_TYPE_MASK)
            /memset(&lock[1], 0x0, size)/ rec_lock.page_id = rec_id.get_page_id()
            /lock_rec_set_nth_bit(lock, rec_id.m_heap_no)
            設(shè)置相關(guān)信息,將鎖的信息進(jìn)行記錄,并且lock->trx->lock.n_rec_locks.fetch_add +1
          最后返回這個(gè)lock_t結(jié)構(gòu)相關(guān)的變量
        ->RecLock::lock_add
          將這個(gè)lock_t結(jié)構(gòu)放入到hash結(jié)構(gòu)中
          ->bool wait = m_mode & LOCK_WAIT;
            是否處于等待狀態(tài)               
          ->lock->index->table->n_rec_locks.fetch_add(1, std::memory_order_relaxed);
            增加鎖定的行數(shù) +1
          ->if (!wait)
            是否處于等待狀態(tài),如果不是
            lock_rec_insert_to_granted(lock_hash, lock, m_rec_id)
             ->首先獲取cell,使用頭插法插入到鏈表的頭部
            否則
            lock_rec_insert_to_waiting(lock_hash, lock, m_rec_id)      
             ->直接插入到響應(yīng)cell的尾部       
          ->locksys::add_to_trx_locks(lock)
            ->UT_LIST_ADD_LAST(lock->trx->lock.trx_locks, lock)
              將鎖結(jié)構(gòu)加入到事務(wù)的trx_lock_t中
            ->lock->trx->lock.trx_locks_version++
              locks版本+1
          ->if (wait)
            ->lock_set_lock_and_trx_wait
             ->trx->lock.wait_lock = lock; //寫(xiě)入 ,寫(xiě)入的當(dāng)前的這個(gè)鎖,也就是事務(wù)正在等待這個(gè)鎖資源
             ->trx->lock.wait_lock_type = lock_get_type_low(lock);
               如果是處于等待狀態(tài),則將這個(gè)鎖放入到事務(wù)的trx_lock_t->wait_lock中          
      ->lock_create_wait_for_edge     
        輸入?yún)?shù)為當(dāng)前事務(wù)的trx(waiter)和等待的trx(被堵塞blocker)
        waiter->lock.blocking_trx.store(blocker)
        記錄當(dāng)前等待的事務(wù)的堵塞源頭事務(wù),存儲(chǔ)一條邊,記錄了堵塞者
      ->RecLock::set_wait_state
        m_trx->lock.wait_started = ut_time();
        m_trx->lock.que_state = TRX_QUE_LOCK_WAIT;
        m_trx->lock.was_chosen_as_deadlock_victim = false;
        以上設(shè)置事務(wù)lock的狀態(tài),show engine中事務(wù)的狀態(tài)就是它
            case TRX_QUE_LOCK_WAIT:fputs("LOCK WAIT ", f);break;
        并且innodb_trx中的狀態(tài)也會(huì)根據(jù)其進(jìn)行判定,fill_trx_row->trx_get_que_state_str
        也是一樣的狀態(tài)
        同時(shí)innodb_trx中的trx_wait_started也是來(lái)自這里,如果等待了就設(shè)置為時(shí)間,沒(méi)有等待則為NULL
        
         stopped = que_thr_stop(m_thr)
         用于返回用于加鎖等待的 判定QUE_THR_LOCK_WAIT
      ->thd_report_row_lock_wait
        使用當(dāng)前線(xiàn)程和需要等待的線(xiàn)程,如果是SQL線(xiàn)程則需要根據(jù)seqnumber進(jìn)行死鎖判定
        if (self != nullptr && wait_for != nullptr && is_mts_worker(self) &&is_mts_worker(wait_for))
        ->Commit_order_manager::check_and_report_deadlock(self, wait_for);
           if (mngr != nullptr && self_w->c_rli == wait_for_w->c_rli &&wait_for_w->sequence_number() > self_w->sequence_number())
           這里根據(jù)sequence_number進(jìn)行判定
           ->Commit_order_manager::report_deadlock
             在研究
      ->return (DB_LOCK_WAIT)
       返回狀態(tài)DB_LOCK_WAIT
    ->trx_mutex_exit(trx);
      進(jìn)行解鎖,也就是上面的過(guò)程是在trx_t的mutex下進(jìn)行的
    ->返回堵塞狀態(tài)DB_LOCK_WAIT
    以上是堵塞的流程
    ->如果沒(méi)有堵塞則
    ->lock_rec_add_to_queue
     -> type_mode |= LOCK_REC;
     -> if (!(type_mode & LOCK_WAIT))
        如果不處于等待狀態(tài),因?yàn)閘ock_rec_add_to_queue函數(shù)并不是只有
        這里才會(huì)調(diào)用,調(diào)用的地方還很多,這里看起來(lái)不會(huì)處于LOCK_WAIT
        狀態(tài)
        ->lock_hash_get(type_mode)/lock_rec_get_first_on_page(hash, block)
          通過(guò)lock_system hash結(jié)構(gòu)找到相關(guān)的第一個(gè)lock_t結(jié)構(gòu)
        ->lock_rec_find_similar_on_page
          主要是通過(guò)對(duì)hash的鏈表進(jìn)行for循環(huán),看本事務(wù)是否已經(jīng)有合適的lock_t結(jié)構(gòu)了,如果有則只需要設(shè)置響應(yīng)的bit位就可以了。主要比對(duì)的方式
          1、事務(wù)是同一個(gè)
          2、鎖模式相同
          3、并且此lock_t的bit結(jié)構(gòu)能夠容納下
          ->lock_rec_set_nth_bit(lock, heap_no)
            如果找到就進(jìn)行bit位設(shè)置
        ->否則就需要新建新建lock_t結(jié)構(gòu),并且調(diào)用的也是RecLock::create
          并且加鎖trx_mutex_enter(trx),當(dāng)然也包含了加入到lock_system
          hash結(jié)構(gòu)中,流程如上,不在詳細(xì)描述。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容