Mysql鎖內部實現機制之C源碼解析

概述

雖然現在關系型數據庫越來越相似,但其背後的實現機制可能大相徑庭。實際使用方面,因為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結構與數據頁應該被這樣刻畫:

註:

  • 內存中的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);
}

表鎖結構

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文件

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>

事務中鎖的描述

上述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;	
    /*!< index for a record lock */
    dict_index_t*  index;
    /* lock details */
    union {
        /* table lock */
        lock_table_t  tab_lock;
        /* record lock */
        lock_rec_t  rec_lock;
    } un_member;
};

lock_t是根據每個事務每個頁(或表)來定義的,但是一個事務往往涉及到多個頁,因此需要鏈表trx_locks串聯起一個事務相關的所有鎖信息。除瞭需要根據事務查詢到所有鎖信息,實際場景還要求系統必須能夠快速高效的檢測出某個行記錄是否已經上鎖。因此必須有一個全局變量支持對行記錄進行鎖信息的查詢。Innodb選擇瞭哈希表,其定義如下:

摘自lock0lock.h文件

/** 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.ic、ut0rnd.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) << 8) + n1)
            ^ UT_HASH_RANDOM_MASK
        ) 
        + n2
    );
}

這將意味著無法提供一個手段使得我們可以直接得知某一行是否上鎖。而是應該先通過其所在的頁得到space id、page number通過lock_rec_fold函數得出key值而後經過hash查詢得到lock_rec_t,而後根據heap_no掃描bit map,最終確定鎖信息。lock_rec_get_first函數實現瞭上述邏輯:

這裡返回的其實是lock_t對象,摘自lock0lock.cc文件

/**
 * 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位。

上述各種數據結構,對其整理關系如下圖所示:

註:

lock_sys_t中的slot顏色與lock_t顏色相同則表明lock_sys_t slot持有lock_t 指針信息,實在是沒法連線,不然圖很混亂

到此這篇關於Mysql鎖內部實現機制之C源碼解析的文章就介紹到這瞭,更多相關Mysql鎖內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: