Mysql鎖內(nèi)部實(shí)現(xiàn)機(jī)制之C源碼解析
概述
雖然現(xiàn)在關(guān)系型數(shù)據(jù)庫越來越相似,但其背后的實(shí)現(xiàn)機(jī)制可能大相徑庭。實(shí)際使用方面,因?yàn)镾QL語法規(guī)范的存在使得我們熟悉多種關(guān)系型數(shù)據(jù)庫并非難事,但是有多少種數(shù)據(jù)庫可能就有多少種鎖的實(shí)現(xiàn)方法。
Microsoft Sql Server2005之前只提供頁鎖,直到2005版本才開始支持樂觀并發(fā)、悲觀并發(fā),樂觀模式下允許實(shí)現(xiàn)行級別鎖,在Sql Server的設(shè)計(jì)中鎖是一種稀缺資源,鎖的數(shù)量越多,開銷就越大,為了避免因?yàn)殒i的數(shù)量快速攀升導(dǎo)致性能斷崖式下跌,其支持一種稱為鎖升級的機(jī)制,一旦行鎖升級為頁鎖,并發(fā)性能就又回到原點(diǎn)。
事實(shí)上,即使在同一個數(shù)據(jù)庫,不同的執(zhí)行引擎對鎖這一功能的詮釋依然是百家爭鳴。對于MyISAM而言僅僅支持表鎖,并發(fā)讀取尚可,并發(fā)修改可就捉襟見肘了。Innodb則和Oracle非常相似,提供非鎖定一致性讀取、行鎖支持,與Sql Server明顯不同的是隨著鎖總數(shù)的上升,Innodb僅僅只需要付出一點(diǎn)點(diǎn)代價。
行鎖結(jié)構(gòu)
Innodb支持行鎖,且對于鎖的描述并不會存在特別大的開銷。因此不需要鎖升級這一機(jī)制作為大量鎖導(dǎo)致性能下降之后的搶救措施。
摘自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;
};不難看出雖然并發(fā)控制可以細(xì)化到行級別,但是鎖以頁的粒度組織管理。Innodb的設(shè)計(jì)中通過space id、page number兩個必要條件就可以確定唯一一個數(shù)據(jù)頁,n_bits表示描述該頁行鎖信息需要多少bit位。
同一數(shù)據(jù)頁中每條記錄都分配唯一的連續(xù)的遞增序號:heap_no,若要知道某一行記錄是否上鎖,則只需要判斷位圖heap_no位置的數(shù)字是否為一即可。由于lock bitmap根據(jù)數(shù)據(jù)頁的記錄數(shù)量進(jìn)行內(nèi)存空間分配的,因此沒有顯式定義,且該頁記錄可能還會繼續(xù)增加,因此預(yù)留了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
假設(shè)space id = 20,page number = 100的數(shù)據(jù)頁目前有160條記錄,heap_no為2、3、4的記錄已經(jīng)被鎖,則對應(yīng)的lock_rec_t結(jié)構(gòu)與數(shù)據(jù)頁應(yīng)該被這樣刻畫:

注:
- 內(nèi)存中的lock bitmap應(yīng)該是線性分布的,圖中所示二維結(jié)構(gòu)是為了方便描述
- bitmap與lock_rec_t結(jié)構(gòu)是一塊連續(xù)內(nèi)存,圖中引用關(guān)系也是繪圖需要
可以看到該頁對應(yīng)的bitmap第二三四位置全部置一,描述一個數(shù)據(jù)頁行鎖所消耗內(nèi)存從感官上相當(dāng)有限,那具體占用多少呢?我們可以計(jì)算一下:
160 / 8 + 8 + 1 = 29byte。
- 160條記錄對應(yīng)160bit
- +8是因?yàn)樾枰A(yù)留出64bit
- +1是因?yàn)樵创a中還預(yù)留了1字節(jié)
這里還額外+1,應(yīng)該是為了避免因?yàn)檎龑?dǎo)致的結(jié)果數(shù)值偏小的問題。假如是161條記錄如果不+1則計(jì)算出來的20byte不夠描述所有記錄的鎖信息(不動用預(yù)留位)。
摘自lock0priv.h文件:
/* lock_rec_create函數(shù)代碼片段 */
n_bits = page_dir_get_n_heap(page) + LOCK_PAGE_BITMAP_MARGIN;
n_bytes = 1 + n_bits / 8;
/* 注意這里是分配的連續(xù)內(nèi)存 */
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);
}表鎖結(jié)構(gòu)
Innodb還支持表鎖,表鎖可分為兩大類:意向鎖,自增鎖其數(shù)據(jù)結(jié)構(gòu)定義如下:
摘自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>事務(wù)中鎖的描述
上述lock_rec_t、lock_table_t結(jié)構(gòu)只是單獨(dú)的定義,鎖產(chǎn)生于事務(wù)之中,因此每個事務(wù)對應(yīng)的行鎖、表鎖會有一個相應(yīng)的鎖的結(jié)構(gòu),其定義如下:
摘自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是根據(jù)每個事務(wù)每個頁(或表)來定義的,但是一個事務(wù)往往涉及到多個頁,因此需要鏈表trx_locks串聯(lián)起一個事務(wù)相關(guān)的所有鎖信息。除了需要根據(jù)事務(wù)查詢到所有鎖信息,實(shí)際場景還要求系統(tǒng)必須能夠快速高效的檢測出某個行記錄是否已經(jīng)上鎖。因此必須有一個全局變量支持對行記錄進(jìn)行鎖信息的查詢。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;
};
函數(shù)lock_sys_create在database start之際負(fù)責(zé)初始化lock_sys_t結(jié)構(gòu)。rec_hash的hash slot數(shù)量由srv_lock_table_size變量決定。rec_hash哈希表的key值通過頁的space id,page number計(jì)算得出。
摘自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
);
}這將意味著無法提供一個手段使得我們可以直接得知某一行是否上鎖。而是應(yīng)該先通過其所在的頁得到space id、page number通過lock_rec_fold函數(shù)得出key值而后經(jīng)過hash查詢得到lock_rec_t,而后根據(jù)heap_no掃描bit map,最終確定鎖信息。lock_rec_get_first函數(shù)實(shí)現(xiàn)了上述邏輯:
這里返回的其實(shí)是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);
}鎖維護(hù)以頁的粒度,不是一個最高效直接的方式,明顯的時間換空間,這種設(shè)計(jì)使得鎖的開銷很小。某一事務(wù)對任一行上鎖的開銷都是一樣的,鎖數(shù)量的上升也不會帶來額外的內(nèi)存消耗。
每個事務(wù)都對應(yīng)一個trx_t的內(nèi)存對象,其中保存著該事務(wù)鎖信息鏈表和正在等待的鎖信息。因此存在如下兩種途徑對鎖進(jìn)行查詢:
- 根據(jù)事務(wù): 通過trx_t對象的trx_locks鏈表,再通過lock_t對象中的trx_locks遍歷可得某事務(wù)持有、等待的所有鎖信息。
- 根據(jù)記錄: 根據(jù)記錄所在的頁,通過space id、page number在lock_sys_t結(jié)構(gòu)中定位到lock_t對象,掃描bitmap找到heap_no對應(yīng)的bit位。
上述各種數(shù)據(jù)結(jié)構(gòu),對其整理關(guān)系如下圖所示:

注:
lock_sys_t中的slot顏色與lock_t顏色相同則表明lock_sys_t slot持有l(wèi)ock_t 指針信息,實(shí)在是沒法連線,不然圖很混亂
到此這篇關(guān)于Mysql鎖內(nèi)部實(shí)現(xiàn)機(jī)制之C源碼解析的文章就介紹到這了,更多相關(guān)Mysql鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
mysql搭建主從復(fù)制的實(shí)現(xiàn)步驟
在MySQL集群中,主庫更新會同步到從庫,但從庫更新不同步到主庫,主從復(fù)制能分?jǐn)倝毫?本文就來介紹一下mysql搭建主從復(fù)制的實(shí)現(xiàn)步驟,感興趣的可以了解一下2024-11-11
MySQL數(shù)據(jù)庫基于sysbench實(shí)現(xiàn)OLTP基準(zhǔn)測試
這篇文章主要介紹了MySQL數(shù)據(jù)庫基于sysbench實(shí)現(xiàn)OLTP基準(zhǔn)測試,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-11-11
MySQL Where 條件語句介紹和運(yùn)算符小結(jié)
這篇文章主要介紹了MySQL Where 條件語句介紹和運(yùn)算符小結(jié),本文同時還給出了一些用法示例,需要的朋友可以參考下2014-11-11
Explain命令在優(yōu)化查詢中的實(shí)際應(yīng)用
在MySQL中,EXPLAIN命令是一種非常重要的查詢優(yōu)化工具,它可以幫助我們分析SQL查詢語句的執(zhí)行計(jì)劃,以及如何優(yōu)化它們。本文介紹了Explain命令在優(yōu)化查詢中的實(shí)際應(yīng)用,感興趣的小伙伴可以參考閱讀2023-04-04

