《艾爾登法環》Steam 線上人數破 86 萬:逼近《CS:GO》、《失落方舟》
mysql 鎖
鎖型別
型別
-
表級鎖:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖衝突的概率最高,併發度最低
-
- 這些儲存引擎通過總是一次性同時獲取所有需要的鎖以及總是按相同的順序獲取表鎖來避免死鎖。
- 表級鎖更適合於以查詢為主,併發使用者少,只有少量按索引條件更新資料的應用,如Web 應用
-
行級鎖:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖衝突的概率最低,併發度也最高
-
- 最大程度的支援併發,同時也帶來了最大的鎖開銷
- 在 InnoDB 中,除單個 SQL 組成的事務外,鎖是逐步獲得的,這就決定了在 InnoDB 中發生死鎖是可能的
- 行級鎖只在儲存引擎層實現,而Mysql伺服器層沒有實現。 行級鎖更適合於有大量按索引條件併發更新少量不同資料,同時又有併發查詢的應用,如一些線上事務處理(OLTP)系統
-
頁面鎖:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,併發度一般
鎖粒度和相容
意向鎖
-
實際應用中InnoDB許多行級鎖與表級鎖共存
-
未來的某個時刻,事務可能要加共享/排它鎖了,先提前宣告一個意向(在索引樹上打一個標記,其他表鎖看到意向鎖必須等待)
意向鎖分類:
-
意向共享鎖(intention shared lock, IS),它預示著,事務有意向對錶中的某些行加共享S鎖
-
意向排它鎖(intention exclusive lock, IX),它預示著,事務有意向對錶中的某些行加排它X鎖
意向鎖獲取
- 事務要獲得某些行的S鎖,必須先獲得表的IS鎖
- 事務要獲得某些行的X鎖,必須先獲得表的IX鎖
意向鎖的相容
- InnoDB使用共享鎖,可以提高讀讀併發
- 為了保證資料強一致,InnoDB使用強互斥鎖,保證同一行記錄修改與刪除的序列性
- InnoDB使用插入意向鎖,可以提高插入併發
- 由於意向鎖僅僅表明意向,它其實是比較弱的鎖,意向鎖之間並不相互互斥,可以並行
意向共享鎖(IS) | 意向排他鎖(IX) | 共享鎖(S) | 排他鎖(X) | |
---|---|---|---|---|
意向共享鎖(IS) | 相容 | 相容 | 相容 | 不相容 |
意向排他鎖(IX) | 相容 | 相容 | 不相容 | 不相容 |
共享鎖(S) | 相容 | 不相容 | 相容 | 不相容 |
排他鎖(X) | 不相容 | 不相容 | 不相容 | 不相容 |
鎖衝突相容
Gap(間隙鎖) | Insert Intention(插入意向鎖) | Record(行鎖) | Next-Key | |
---|---|---|---|---|
Gap(間隙鎖) | 相容 | 相容 | 相容 | 相容 |
Insert Intention(插入意向鎖) | 衝突 | 相容 | 相容 | 衝突 |
Record(行鎖) | 相容 | 相容 | 衝突 | 衝突 |
Next-Key | 相容 | 相容 | 衝突 | 衝突 |
InnoDB 鎖實現方式
行鎖
- InnoDB 行鎖是通過給索引上的索引項加鎖來實現的,這一點 MySQL 與 Oracle 不同,後者是通過在資料塊中對相應資料行加鎖來實現的。InnoDB 這種行鎖實現特點意味著:只有通過索引條件檢索資料,InnoDB 才使用行級鎖,否則,InnoDB 將使用表鎖
- 不論是使用主鍵索引、唯一索引或普通索引,InnoDB 都會使用行鎖來對資料加鎖
- 只有執行計劃真正使用了索引,才能使用行鎖:即便在條件中使用了索引欄位,但是否使用索引來檢索資料是由 MySQL 通過判斷不同執行計劃的代價來決定的,如果 MySQL 認為全表掃描效率更高,比如對一些很小的表,它就不會使用索引,這種情況下 InnoDB 將使用表鎖,而不是行鎖。因此,在分析鎖衝突時
別忘了檢查 SQL 的執行計劃(可以通過 explain 檢查 SQL 的執行計劃),以確認是否真正使用了索引 - 由於 MySQL 的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然多個session是訪問不同行的記錄, 但是如果是使用相同的索引鍵, 是會出現鎖衝突的(後使用這些索引的session需要等待先使用索引的session釋放鎖後,才能獲取鎖)。 應用設計的時候要注意這一點
間隙鎖
-
間隙鎖的目的是為了防止幻讀,其主要通過兩個方面實現這個目的:防止間隙內有新資料被插入
-
innodb自動使用條件:
1.事務級別在RR級別下
2.檢索條件必須有索引(沒有索引的話,mysql會全表掃描,那樣會鎖定整張表所有的記錄,包括不存在的記錄,此時其他事務不能修改不能刪除不能新增) -
對記錄之間的間隙鎖定
next-key鎖
- next-key鎖的目的是解決可重複度
- 實現方案:
間隙鎖
+行鎖
隔離級別和鎖
rc更新
更新事務 | 更新事務 |
---|---|
聚集索引行鎖 | 等待 |
範圍(聚集索引)Gap鎖 | 範圍內等待,範圍外直接執行 |
無索引(所有記錄加鎖,不符合記錄解鎖,符合記錄鎖定,鎖定記錄過多會升級為表鎖) | 鎖定記錄等待,非鎖定記錄執行(鎖定記錄過多,升級為表鎖) |
rc 插入
更新事務 | 插入事務 |
---|---|
行鎖 | 無影響 |
範圍(聚集索引) | 無影響 |
無索引查詢(所有記錄加鎖,不符合記錄解鎖,符合記錄鎖定) | 無影響 |
rr更新
更新事務 | 更新事務 |
---|---|
聚集索引行鎖 | 等待 |
範圍(聚集索引)Next-key鎖 | 範圍內等待,範圍外直接執行 |
無索引(所有記錄加鎖,不符合記錄解鎖,符合記錄鎖定,鎖定記錄過多會升級為表鎖) | 鎖定記錄等待,非鎖定記錄執行(鎖定記錄過多,升級為表鎖) |
rr插入
更新事務 | 插入事務 |
---|---|
行鎖 | 無影響 |
範圍(聚集索引)Next-key鎖 | 範圍內等待,範圍外直接執行 |
無索引查詢(所有記錄加鎖,不符合記錄解鎖,符合記錄鎖定,鎖定記錄過多會升級為表鎖) | 鎖定記錄等待,非鎖定記錄執行(鎖定記錄過多,升級為表鎖) |
一致性非鎖定讀原理(MVCC)
多版本併發控制。MVCC是一種併發控制的方法。MVCC在MySQL InnoDB中的實現主要是為了提高資料庫併發效能,用更好的方式去處理讀-寫衝突,做到即使有讀寫衝突時,也能做到不加鎖,非阻塞併發讀
當前讀和快照讀
- 當前讀:讀取的是記錄的最新版本,讀取時還要保證其他併發事務不能修改當前記錄,會對讀取的記錄進行加鎖
- 快照讀:不加鎖的非阻塞讀;快照讀的前提是隔離級別不是序列級別,序列級別下的快照讀會退化成當前讀;之所以出現快照讀的情況,是基於提高併發效能的考慮,快照讀的實現是基於多版本併發控制,即MVCC,可以認為MVCC是行鎖的一個變種,但它在很多情況下,避免了加鎖操作,降低了開銷;既然是基於多版本,即快照讀可能讀到的並不一定是資料的最新版本,而有可能是之前的歷史版本
- MVCC就是為了實現讀-寫衝突不加鎖,而這個讀指的就是
快照讀
, 而非當前讀
,當前讀實際上是一種加鎖的操作,是悲觀鎖的實現
MVCC原理
依賴記錄中的 3個隱式欄位
,undo日誌
,Read View
隱式欄位
- DB_TRX_ID:6byte,最近修改(
修改/插入
)事務ID:記錄建立這條記錄/最後一次修改該記錄的事務ID - DB_ROLL_PTR:7byte,回滾指標,指向這條記錄的上一個版本(儲存於rollback segment裡)
- DB_ROW_ID:6byte,隱含的自增ID(隱藏主鍵),如果資料表沒有主鍵,InnoDB會自動以
DB_ROW_ID
產生一個聚簇索引
undo日誌
- insert undo log
代表事務在insert
新記錄時產生的undo log
, 只在事務回滾時需要,並且在事務提交後可以被立即丟棄 - update undo log
事務在進行update
或delete
時產生的undo log
; 不僅在事務回滾時需要,在快照讀時也需要;所以不能隨便刪除,只有在快速讀或事務回滾不涉及該日誌時,對應的日誌才會被purge
執行緒統一清除
undolog記錄流程
1. 開啟事務,修改該行(記錄)資料時,資料庫會先對該行加`排他鎖`
2. 在`undo log`中增加當前行的拷貝副本
3. 拷貝完畢後,修改該行`name`為標貝科技,並且修改隱藏欄位的事務ID為當前`事務1`的ID, 我們預設從`1`開始,之後遞增,回滾指標指向拷貝到`undo log`的副本記錄,既表示我的上一個版本就是它
4. 事務提交後,釋放鎖
執行事務
:
序號 | name | age | DB_TRX_ID(事務id) | DB_ROLL_PTR(回滾指標) | DB_ROW_ID(隱藏主鍵) |
---|---|---|---|---|---|
2 | 標貝科技 | 25 | 1 | 0x101212 | 1 |
undolog日誌
:
序號 | name | age | DB_TRX_ID(事務id) | DB_ROLL_PTR(回滾指標) | DB_ROW_ID(隱藏主鍵) |
---|---|---|---|---|---|
1 | 皮皮檀 | 25 | null | null | 1 |
1. 開啟事務,修改該行資料,資料庫先為該行加鎖
2. 把該行資料拷貝到`undo log`中,作為舊記錄,發現該行記錄已經有`undo log`了,那麼最新的舊資料作為連結串列的表頭,插在該行記錄的`undo log`最前面
3. 修改該行`age`為18歲,並且修改隱藏欄位的事務ID為當前`事務2`的ID, 那就是`2`,回滾指標指向剛剛拷貝到`undo log`的副本記錄
4. 事務提交,釋放鎖
執行事務
:
序號 | name | age | DB_TRX_ID(事務id) | DB_ROLL_PTR(回滾指標) | DB_ROW_ID(隱藏主鍵) |
---|---|---|---|---|---|
3 | 標貝科技 | 18 | 2 | 0x101266 | 1 |
undolog日誌
:
序號 | name | age | DB_TRX_ID(事務id) | DB_ROLL_PTR(回滾指標) | DB_ROW_ID(隱藏主鍵) |
---|---|---|---|---|---|
2 | 標貝科技 | 25 | 1 | 0x101212 | 1 |
1 | 皮皮檀 | 25 | null | null | 1 |
序號3、2、1通過回滾指標串聯起來
Read View
- 事務進行
快照讀
操作的時候生產的讀檢視
(Read View),在該事務執行的快照讀的那一刻,會生成資料庫系統當前的一個快照,記錄並維護系統當前活躍事務的ID(當每個事務開啟時,都會被分配一個ID, 這個ID是遞增的,所以最新的事務,ID值越大) - Read View
遵循一個可見性演算法,主要是將
要被修改的資料的最新記錄中的
DB_TRX_ID(即當前事務ID)取出來,與系統當前其他活躍事務的ID去對比(由Read View維護),如果
DB_TRX_ID跟Read View的屬性做了某些比較,不符合可見性,那就通過
DB_ROLL_PTR回滾指標去取出
Undo Log中的
DB_TRX_ID再比較,即遍歷連結串列的
DB_TRX_ID(從鏈首到鏈尾,即從最近的一次修改查起),直到找到滿足特定條件的
DB_TRX_ID, 那麼這個DB_TRX_ID所在的舊記錄就是當前事務能看見的最新
老版本
ReadView和事務
-
read uncommitted隔離級別事務:直接讀取記錄的最新版本
-
serializable隔離級別事務:使用加鎖的方式來訪問記錄
-
RC和RR隔離級別事務:需要用到版本鏈概念,核心問題是如何判斷版本鏈中哪個版本是當前事務可見的
-
readview中四個比較重要的概念:
-
- m_ids:表示在生成readview時,當前系統中活躍的讀寫事務id列表
- min_trx_id:表示在生成readview時,當前系統中活躍的讀寫事務中最小的事務id,也就是m_ids中最小的值
- max_trx_id:表示生成readview時,系統中應該分配給下一個事務的id值
- creator_trx_id:表示生成該readview的事務的事務id
-
有了readview,在訪問某條記錄時,按照以下步驟判斷記錄的某個版本是否可見
-
-
1、如果被訪問版本的trx_id,與readview中的creator_trx_id值相同,表明當前事務在訪問自己修改過的記錄,該版本可以被當前事務訪問
-
2、如果被訪問版本的trx_id,小於readview中的min_trx_id值,表明生成該版本的事務在當前事務生成readview前已經提交,該版本可以被當前事務訪問
-
3、如果被訪問版本的trx_id,大於或等於readview中的max_trx_id值,表明生成該版本的事務在當前事務生成readview後才開啟,該版本不可以被當前事務訪問
-
4、如果被訪問版本的trx_id,值在readview的min_trx_id和max_trx_id之間,就需要判斷trx_id屬性值是不是在m_ids列表中
-
- 如果在:說明建立readview時生成該版本的事務還是活躍的,該版本不可以被訪問
- 如果不在:說明建立readview時生成該版本的事務已經被提交,該版本可以被訪問
-
-
生成readview時機
-
- RC隔離級別:每次讀取資料前,都生成一個readview
- RR隔離級別:在第一次讀取資料前,生成一個readview
struct trx_t {
/* 事務ID */
trx_id_t id; /*!< transaction id */
/* 一致性讀的快照 */
ReadView* read_view; /*!< consistent read view used in the transaction, or NULL if not yet set */
// 省略一大堆屬性...
}
class ReadView {
/**
* 建立這個快照的事務ID
*/
trx_id_t m_creator_trx_id;
/**
* 生成這個快照時處於活躍狀態的事務ID的列表,
* 是個已經排好序的列表
*/
ids_t m_ids;
/**
* 高水位線:id大於等於 m_low_limit_id 的事務都不可見。
* 在生成快照時,它被賦值為“下一個待分配的事務ID”(會大於所有已分配的事務ID)。
*/
trx_id_t m_low_limit_id;
/**
* 低水位線:id小於m_up_limit_id的事務都不可見。
* 它是活躍事務ID列表的最小值,在生成快照時,小於m_up_limit_id的事務都已經提交(或者回滾)。
*/
trx_id_t m_up_limit_id;
// 判斷事務是否可見的方法
bool changes_visible(){}
// 關閉快照的方法
void close(){}
// ...
}
/* 判斷某個事務的修改對當前事務是否可見 */
bool changes_visible(){
/**
* 可見的情況:
* 1. 小於低水位線,即建立快照時,該事務已經提交(或回滾)
* 2. 事務ID是當前事務。
*/
if (id < m_up_limit_id || id == m_creator_trx_id) {
return(true);
}
if (id >= m_low_limit_id) { /* 高於水位線不可見,即建立快照時,該事務還沒有提交 */
return(false);
} else if (m_ids.empty()) { /* 建立快照時,沒有其它活躍的讀寫事務時,可見 */
return(true);
}
/**
* 執行到這一步,說明事務ID在低水位和高水位之間,即 id ∈ [m_up_limit_id, m_low_limit_id)
* 需要判斷是否屬於在活躍事務列表m_ids中,
* 如果在,說明建立快照時,該事務處於活躍狀態(未提交),修改對當前事務不可見。
*/
// 獲取活躍事務ID列表,並使用二分查詢判斷事務ID是否在 m_ids中
const ids_t::value_type* p = m_ids.data();
return(!std::binary_search(p, p + m_ids.size(), id));
}
bool lock_clust_rec_cons_read_sees()
{
// 獲取修改這個資料行的事務ID
trx_id_t trx_id = row_get_rec_trx_id(rec, index, offsets);
// 呼叫 changes_visible() 判斷是否可見,如果不可見則取查詢undolog
return(view->changes_visible(trx_id, index->table->name));
}
MVCC整體請求流程
針對記錄 X
的操作事務(RR或RC隔離級別下)
事務0 | 事務1 | 事務2 | 事務3 | 事務4 |
---|---|---|---|---|
事務結束 | 事務開始 | 事務開始 | 事務開始 | 事務開始 |
... | … | … | … | 事務2開始後修改且已提交 |
.... | 進行中 | 快照讀 | 進行中 | |
.... | … | … | … |
當前的m_low_limit_id
= 4+1=5 、 trx_id_t m_up_limit_id = 1,readView:[事務1,2,3]
m_low_limit_id = 4+1=5 //下一個事務的id
m_up_limit_id = 1 // 活躍的最小事務id
read_view = [事務1,事務2,事務3] //活躍事務
m_creator_trx_id = 2 // 當前事務
/**
* 事務2的可查詢情況(事務1,3不修改資料)
* RC隔離級別下
* 可以查到事務4提交的資料
* RR隔離級別下
* 可以查到事務0提交的資料
*
* 事務2的可查詢情況(事務1,3修改資料)
* RC隔離級別下
* 可以查到事務4的資料
* RR隔離級別下
* 可以查到事務0的資料
*/
參考文獻
[1]https://dev.mysql.com/doc/dev/mysql-server/latest/PAGE_PROTOCOL.html
[2]https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_packets.html#sect_protocol_basic_packets_packet
[3]https://www.jianshu.com/p/5e6b33d8945f
[4]https://cloud.tencent.com/developer/article/1768901
[5]https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_authentication_methods.html
[6]https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_command_phase.html
[7]https://dev.mysql.com/doc/internals/en/
[8]https://www.cnblogs.com/wyq178/p/11576065.html
[9]Mysql技術內幕:InnoDB儲存引擎 (第2版). 姜承堯
[10]MySQL運維內參:MySQL、Galera、Inception核心原理與最佳實踐. 周彥偉,王竹峰,強昌金
[11]https://dev.mysql.com/doc/refman/5.7/en/innodb-architecture.html
[12]https://dev.mysql.com/doc/refman/5.7/en/faqs-innodb-change-buffer.html