首頁  >  文章  >  資料庫  >  一文聊聊Mysql鎖的內部實作機制

一文聊聊Mysql鎖的內部實作機制

青灯夜游
青灯夜游轉載
2022-09-13 19:45:402400瀏覽

這篇文章帶大家聊聊Mysql鎖的內部實作機制,希望對大家有幫助。

一文聊聊Mysql鎖的內部實作機制

附註:所列舉程式碼皆出自Mysql-5.6

        雖然現在關聯式資料庫越來越相似,但背後的實現機制可能大相逕庭。實際使用方面,因為SQL語法規範的存在使得我們熟悉多種關係型資料庫並非難事,但是有多少種資料庫可能就有多少種鎖的實作方法。

      Microsoft Sql Server2005之前只提供頁鎖定,直到2005版本才開始支援樂觀並發悲觀並發,樂觀模式下允許實現行級鎖定,在Sql Server的設計中鎖是一種稀缺資源,鎖的數量越多,開銷就越大,為了避免因為鎖的數量快速攀升導致性能斷崖式下跌,其支援一種稱為鎖定升級的機制,一旦行鎖升級為頁鎖,並發效能就回到原點。

      事實上,即使在同一個資料庫,不同的執行引擎對鎖定這項功能的詮釋依然是百家爭鳴。對於MyISAM而言僅支援表鎖,並發讀取尚可,並發修改可就捉襟見肘了。 Innodb則和Oracle非常相似,提供非鎖定一致性讀取行鎖定支持,與Sql Server明顯不同的是隨著鎖定總數的上升,Innodb只需要付出一點點代價。

行鎖定結構

      Innodb支援行鎖定,且鎖定的說明不會有特別大的開銷。因此不需要鎖定升級此機製作為大量鎖定導致性能下降之後的搶救措施。

      摘自lock0priv.h文件,Innodb定義為行鎖的定義如下:

/** Record lock for a page */
struct lock_rec_t {
    /* space id */
    ulint  space;	
    
    /* page number */
    ulint  page_no;
    
    /**
     * number of bits in the lock bitmap; 
     * NOTE: the lock bitmap is placed immediately after the lock struct 
     */
    ulint  n_bits;			
};

      不難看出雖然並行控制可以細化至行級別,但鎖定以頁數的粒度組織管理。 Innodb的設計中透過space id、page number兩個必要條件就可以確定唯一一個資料頁,n_bits表示描述該頁行鎖定資訊需要多少bit位元。

      在同一資料頁中每個記錄都分配唯一的連續的遞增序號:heap_no,若要知道某一行記錄是否上鎖,則只需要判斷位圖heap_no位置的數字是否為一即可。由於lock bitmap根據資料頁的記錄數量進行記憶體空間分配的,因此沒有明確定義,且該頁記錄可能還會繼續增加,因此預留了LOCK_PAGE_BITMAP_MARGIN大小的空間。

/** 
 * Safety margin when creating a new record lock: this many extra records
 * can be inserted to the page without need to create a lock with 
 * a bigger bitmap
 */
#define LOCK_PAGE_BITMAP_MARGIN	 64

      假設space id = 20,page number = 100的資料頁目前有160筆記錄,heap_no為2、3、4的記錄已經被鎖定,則對應的lock_rec_t結構與資料頁應該被這樣刻畫:

一文聊聊Mysql鎖的內部實作機制

註:

  • #記憶體中的lock bitmap應該是線性分佈的,圖中所示二維結構是為了方便描述
  • bitmap與lock_rec_t結構是一塊連續內存,圖中引用關係也是繪圖需要

      可以看到該頁對應的bitmap第二三四位置全部置一,描述一個資料頁行鎖所消耗記憶體從感官上相當有限,那具體佔用多少呢?我們可以計算一下:
160 / 8 8 1 = 29byte。

  • 160筆記錄對應160bit
  • 8是因為需要預留64bit
  • 1是因為原始碼中也預留了1位元組

      這裡又多1,應該是為了避免因為整除導致的結果數值偏小的問題。假如是161筆記錄如果不 1則計算出來的20byte不夠描述所有記錄的鎖定資訊(不動用預留位)。

      摘自lock0priv.h檔案:

/* lock_rec_create函数代码片段 */
n_bits = page_dir_get_n_heap(page) + LOCK_PAGE_BITMAP_MARGIN;
n_bytes = 1 + n_bits / 8;

/* 注意这里是分配的连续内存 */
lock = static_cast<lock_t>(
    mem_heap_alloc(trx->lock.lock_heap, sizeof(lock_t) + n_bytes)
);


/**
 * Gets the number of records in the heap.
 * @return number of user records 
 */
UNIV_INLINE ulint page_dir_get_n_heap(const page_t* page)	
{
    return(page_header_get_field(page, PAGE_N_HEAP) & 0x7fff);
}</lock_t>

表鎖定結構

      Innodb也支援表單鎖定,錶鎖可分為兩大類別:意願鎖定,自增加鎖定其資料結構定義如下:

      摘自lock0priv.h檔案

struct lock_table_t {
    /* database table in dictionary cache */
    dict_table_t*  table;
    
    /* list of locks on the same table */
    UT_LIST_NODE_T(lock_t)  locks;
};

      摘自ut0lst.h

1
struct ut_list_node {
    /* pointer to the previous node, NULL if start of list */
    TYPE*  prev;
    
    /* pointer to next node, NULL if end of list */
    TYPE*  next;
};


#define UT_LIST_NODE_T(TYPE)  ut_list_node<type></type>

事務中鎖定的描述

      上述lock_rec_t、lock_table_t結構只是單獨的定義,鎖定產生於事務之中,因此每個事務對應的行鎖定、表單鎖定會有一個對應的鎖定的結構,定義如下:

      摘自lock0priv.h檔案

#
/** Lock struct; protected by lock_sys->mutex */
struct lock_t {
    /* transaction owning the lock */
    trx_t*  trx;
    
    /* list of the locks of the transaction */
    UT_LIST_NODE_T(lock_t)  trx_locks;	
    
    /** 
     * lock type, mode, LOCK_GAP or LOCK_REC_NOT_GAP,
     * LOCK_INSERT_INTENTION, wait flag, ORed 
     */
    ulint  type_mode;
    
    /* hash chain node for a record lock */
    hash_node_t  hash;	
    
    /*!<p>      lock_t是根据每个事务每个页(或表)来定义的,但是一个事务往往涉及到多个页,因此需要链表<strong>trx_locks</strong>串联起一个事务相关的所有锁信息。除了需要根据事务查询到所有锁信息,实际场景还要求系统必须能够快速高效的检测出某个行记录是否已经上锁。因此必须有一个全局变量支持对行记录进行锁信息的查询。Innodb选择了哈希表,其定义如下:</p><p>      摘自<strong>lock0lock.h</strong>文件</p><pre class="brush:php;toolbar:false">/** The lock system struct */
struct lock_sys_t {
    /* Mutex protecting the locks */
    ib_mutex_t  mutex;		
    
    /* 就是这里: hash table of the record locks */
    hash_table_t*  rec_hash;	
    
    /* Mutex protecting the next two fields */
    ib_mutex_t  wait_mutex;
    
    /** 
     * Array  of user threads suspended while waiting forlocks within InnoDB,
     * protected by the lock_sys->wait_mutex 
     */
    srv_slot_t*  waiting_threads;
    
    /*
     * highest slot ever used in the waiting_threads array,
     * protected by lock_sys->wait_mutex 
     */
    srv_slot_t*  last_slot;
    
    /** 
     * TRUE if rollback of all recovered transactions is complete. 
     * Protected by lock_sys->mutex 
     */
    ibool  rollback_complete;
		
    /* Max wait time */
    ulint  n_lock_max_wait_time;

    /**
     * Set to the event that is created in the lock wait monitor thread.
     * A value of 0 means the thread is not active
     */
    os_event_t	timeout_event;		

    /* True if the timeout thread is running */
    bool  timeout_thread_active;
};

      函数lock_sys_create在database start之际负责初始化lock_sys_t结构。rec_hash的hash slot数量由srv_lock_table_size变量决定。rec_hash哈希表的key值通过页的space id,page number计算得出。

      摘自lock0lock.icut0rnd.ic 文件

/**
 * Calculates the fold value of a page file address: used in inserting or
 * searching for a lock in the hash table.
 *
 * @return folded value 
 */
UNIV_INLINE ulint lock_rec_fold(ulint space, ulint page_no)
{
    return(ut_fold_ulint_pair(space, page_no));
}


/**
 * Folds a pair of ulints.
 *
 * @return folded value 
 */
UNIV_INLINE ulint ut_fold_ulint_pair(ulint n1, ulint n2)
{
    return (
        (
            (((n1 ^ n2 ^ UT_HASH_RANDOM_MASK2) <p>      这将意味着无法提供一个手段使得我们可以直接得知某一行是否上锁。而是应该先通过其所在的页得到space id、page number通过<strong>lock_rec_fold</strong>函数得出key值而后经过hash查询得到lock_rec_t,而后根据heap_no扫描bit map,最终确定锁信息。<strong>lock_rec_get_first</strong>函数实现了上述逻辑:</p><p>      这里返回的其实是<strong>lock_t</strong>对象,摘自<strong>lock0lock.cc</strong>文件</p><pre class="brush:php;toolbar:false">/**
 * Gets the first explicit lock request on a record.
 *
 * @param block   : block containing the record 
 * @param heap_no : heap number of the record 
 *
 * @return first lock, NULL if none exists 
 */
UNIV_INLINE lock_t* lock_rec_get_first(const buf_block_t* block, ulint heap_no)
{
    lock_t*  lock;

    ut_ad(lock_mutex_own());

    for (lock = lock_rec_get_first_on_page(block); lock;
         lock = lock_rec_get_next_on_page(lock)
    ) {
        if (lock_rec_get_nth_bit(lock, heap_no)) {
            break;
        }
    }

    return(lock);
}

      锁维护以页的粒度,不是一个最高效直接的方式,明显的时间换空间,这种设计使得锁的开销很小。某一事务对任一行上锁的开销都是一样的,锁数量的上升也不会带来额外的内存消耗。

      每个事务都对应一个trx_t的内存对象,其中保存着该事务锁信息链表和正在等待的锁信息。因此存在如下两种途径对锁进行查询:

  • 根据事务: 通过trx_t对象的trx_locks链表,再通过lock_t对象中的trx_locks遍历可得某事务持有、等待的所有锁信息。
  • 根据记录: 根据记录所在的页,通过space id、page number在lock_sys_t结构中定位到lock_t对象,扫描bitmap找到heap_no对应的bit位。

      上述各种数据结构,对其整理关系如下图所示:

一文聊聊Mysql鎖的內部實作機制

注:

  • lock_sys_t中的slot颜色与lock_t颜色相同则表明lock_sys_t slot持有lock_t
    指针信息,实在是没法连线,不然图很混乱

【相关推荐:mysql视频教程

以上是一文聊聊Mysql鎖的內部實作機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除