MySQL的鎖和事務隔離級別

現在的互聯網,開發一個大型的多人APP,你必定離不開數據庫。而如何保證全部人可以高併發的進行讀寫一直是一個高難度的架構問題,先刨去高併發,保證一致性讀寫這個問題最經常使用的手段是事務,而實現一個事務的關鍵點在於鎖機制。mysql

今天咱們就來介紹下InnoDB存儲引擎如何在高併發下實現鎖機制來知足一致性讀寫的原理和實現。算法

數據庫的鎖機制是區別於文件系統的一個關鍵特性。用於管理對共享資源的併發訪問。InnoDB會在不少地方使用鎖機制,好比操做緩衝池中的數據表、LRU頁列表、數據行,爲了保證一致性和完整性,須要有鎖的機制。sql

對於不一樣數據庫,鎖機制的設計和實現徹底不一樣:數據庫

  • MyISAM引擎: 表鎖設計,併發讀沒有問題,併發寫性能差。
  • Microsoft SQL Server: 支持樂觀併發和悲觀併發,樂觀併發下支持行級鎖,維持鎖的開銷大,在行鎖數量超過閾值後會升級爲表鎖。
  • InnoDB引擎: 支持行鎖,提供一致性的非鎖定讀。行鎖沒有額外開銷,性能不會降低。
  • Oracle:和InnoDB引擎很是相似。

兩類鎖:lock和latch

數據庫中lock和latch均可以稱爲鎖,可是有很大的區別。安全

latch通常稱爲閂鎖,用於保證併發線程操做臨界資源的正確性,做用對象是內存數據結構,要求鎖定時間很是短,不會檢測死鎖。在InnoDB引擎中又分爲mutex(互斥量)和rwlock(讀寫鎖)。session

lock是用來鎖定數據庫中的對象,如表、頁、行,做用對象是事務,在commit/rollback後釋放,會檢測死鎖。分爲行鎖、表鎖、意向鎖。數據結構

咱們下面的鎖指的都是lock類鎖。架構

四種鎖類型

InnoDB支持四種鎖:併發

  • 共享鎖(S Lock):容許事務讀一行數據
  • 排他鎖(X Lock):容許事務刪除或更新一行數據
  • 意向共享鎖(Intention S Lock):事務想要得到一張表中某幾行的共享鎖
  • 意向排他鎖(Intention X Lock):事務想要得到一張表中某幾行的排他鎖

當事務T1獲取了行r的共享鎖,因爲讀取不會改變行數據,所以事務T2也能夠直接得到行r的共享鎖,此時稱爲鎖兼容(Lock Compatible)。高併發

而當事務T3想要獲取行r的排他鎖進行修改數據時,就須要等待T1/T2釋放行共享鎖,此時稱爲鎖不兼容。

S鎖和X鎖都是行鎖,而IS鎖和IX鎖都爲意向鎖,屬於表鎖。意向鎖的設計是爲了在一個事務中揭示下一行將被請求的鎖類型,即在表鎖的更細粒度進行鎖定。因爲InnoDB支持表鎖,所以意向鎖不會阻塞除全表掃描外的任何請求。

鎖的兼容性:

IS IX S X
IS 兼容 兼容 兼容 不兼容
IX 兼容 兼容 不兼容 不兼容
S 兼容 不兼容 兼容 不兼容
X 不兼容 不兼容 不兼容 不兼容

存儲事務和鎖信息的三張表

咱們能夠經過show engine innodb status命令在事務部分查看當前鎖請求的信息。

從InnoDB1.0開始,在INFORMATION_SCHEMA架構下添加了INNODB_TRX(transaction事務表)、INNODB_LOCKS(鎖表)、INNODB_LOCK_WAITS(鎖等待表),經過這三張表,可讓咱們實時監控當前事務並分析可能存在的表問題。

三個表的定義分別爲:

INNODB_TRX
trx_id InnoDB存儲引擎內部惟一的事務ID
trx_state 當前事務的狀態
trx_started 事務的開始時間
trx_requested_lock_id 等待事務的鎖IDC,當狀態不爲LOCK WAIT時爲NULL
trx_wait_started 事務等待開始的時間
trx_weight 事務的權重,反映一個事務修改和鎖定的行數。當須要回滾時,選擇該值最小的事務進行回滾
trx_mysql_thread_id MySQL的線程ID,show processlist顯示的結果
trx_query 事務運行的SQL語句
INNODB_LOCKS
lock_id 鎖ID
lock_trx_id 事務ID
lock_mode 鎖的模式
lock_type 鎖的類型,表鎖或行鎖
lock_table 要加鎖的表
lock_index 鎖住的索引
lock_space 鎖對象的space id
lock_page 事務鎖定頁的數量,表鎖時爲NULL
lock_rec 事務鎖定行的數量,表鎖時爲NULL
lock_data 事務鎖定記錄的主鍵值,表鎖時爲NULL
INNODB_LOCK_WAITS
requesting_trx_id 申請鎖資源的事務ID
requesting_lock_id 申請的鎖的ID
blocking_trx_id 阻塞的事務ID
blocking_lock_id 阻塞的鎖的ID

經過INNODB_TRX咱們能夠看到全部的事務,以及事務是否被阻塞,阻塞的鎖ID是什麼。
以後,經過INNODB_LOCKS查看全部的鎖信息。
以後,經過INNODB_LOCK_WAITS能夠查看到鎖的等待信息以及阻塞關係。

經過這三種表可以較爲清晰的查看事務和鎖的狀況,也能夠聯合查詢,在下面的一些場景下咱們會來展現這三個表的內容。

隔離級別

首先咱們來講下數據庫的四種事務隔離級別:

  • READ UNCOMMITTED(0): 瀏覽訪問級別,存在髒讀、不可重複讀、幻讀
  • READ COMMITTED(1): 遊標穩定級別,存在不可重複度、幻讀
  • REPEATABLE READ(2): 存在幻讀
  • SERIALIZABLE(3): 隔離級別,保證事務安全,但徹底串行,性能低

這四種事務隔離級別是指定的SQL標準,InnoDB默認的隔離級別是REAPEATABLE READ,但與其餘數據庫不一樣的時,它同時使用了Next-Key-Lock鎖的算法,可以避免幻讀的產生,所以可以徹底知足事務的隔離性要求,即達到SERIALIZABLE隔離級別。

隔離級別越低,事務請求的鎖越少或持鎖時間越短,所以大部分數據庫的默認隔離級別爲READ COMMITED。可是有相關的分析也指出,隔離級別的性能開銷幾乎同樣,所以用戶無須經過調整隔離級別來提升性能。

查看和修改事務隔離級別的命令:

mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
1 row in set (0.00 sec)

mysql> set session transaction isolation level SERIALIZABLE;
Query OK, 0 rows affected (0.00 sec)

示例中修改了本次會話的事務隔離級別,若是須要修改全局參數,能夠替換session爲global。若是想要永久修改,須要修改配置文件:

[mysqld]
transaction-isolation = READ-COMMITED

在SERIALIZABLE的事務隔離級別,InnoDB會對每一個SELECT語句後自動加上LOCK IN SHARE MODE,來對讀操做加上一個共享鎖,所以再也不支持一致性的非鎖定讀。

因爲InnoDB在REPEATABLE READ隔離級別就能夠達到SERIALIZABLE,所以通常不用使用最高隔離級別。

一致性非鎖定讀和多版本併發控制

一致性非鎖定讀(consistent nonlocking read)是指InnoDB經過行多版本控制(Multi Version Concurrency Control, MVCC)的方法來讀取當前執行時間數據庫中行的數據。

即若是讀取的行正在執行變動操做,這時讀取不會等待行鎖的釋放,而是會讀取行的一個快照數據。快照是指該行的一個歷史數據,經過undo操做來完成。這種方式極大提升了數據庫的併發性,這也是InnoDB的默認設置。

快照是當前行的一個歷史版本,但可能存在多個版本,行數據存在多個快照數據,這種技術成爲行多版本技術,由此帶來的併發控制,稱爲多版本併發控制(MVCC)。InnoDB在READ COMMITED 和 REPEATABLE READ隔離級別時,會使用非鎖定的一致性讀,可是在這兩種隔離級別使用的快找數據定義卻不一樣:

  • READ COMMITED: 老是讀取最新一份快照
  • REPEATABLE READ: 老是讀取事務開始時的行數據版本

咱們執行一個示例:

一致性非鎖定讀
時間 會話A 會話B
1 BEGIN
2 select * from z where a = 3;
3 BEGIN
4 update z set b=2 where a=3;
5 select * from z where a = 3;
6 COMMIT;
7 select * from z where a = 3;
8 COMMIT;

在這個例子中咱們能夠清晰的看到0、一、2三種隔離級別的區別:

#在事務開始前咱們能夠分別調整爲0、一、2三種隔離級別,來查看不一樣的輸出
mysql> set session transaction isolation level READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation   |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set (0.00 sec)

# A會話:T1事務
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from z where a = 3;
+---+------+
| a | b    |
+---+------+
| 3 |    1 |
+---+------+
1 row in set (0.00 sec)

# B會話:T2事務
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update z set b=2 where a=3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

# A會話:T1事務,若是此時隔離級別是READ-UNCOMMITTED,由於此刻事務2可能會回滾,因此出現了髒讀
mysql> select * from z where a=3;
+---+------+
| a | b    |
+---+------+
| 3 |    2 |
+---+------+
1 row in set (0.00 sec)

# A會話:T1事務,若是此時隔離級別是大於READ-UNCOMMITTED的更高級別
mysql> select * from z where a=3;
+---+------+
| a | b    |
+---+------+
| 3 |    1 |
+---+------+
1 row in set (0.00 sec)

# B會話:T2事務
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

# A會話:T1事務,若是此時隔離級別是READ-COMMITTED,由於數據和事務開始時讀取的出現了不一致,所以稱爲不可重複讀,可以讀到其餘事務的結果,違反了事務的隔離性
mysql> select * from z where a=3;
+---+------+
| a | b    |
+---+------+
| 3 |    2 |
+---+------+
1 row in set (0.00 sec)

# A會話:T1事務,若是此時隔離級別是大於READ-COMMITTED的更高級別
mysql> select * from z where a=3;
+---+------+
| a | b    |
+---+------+
| 3 |    1 |
+---+------+
1 row in set (0.00 sec)

# A會話:T1事務
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

一致性鎖定讀和SERIALIZABLE隔離

在默認的REPEATABLE READ隔離級別時,InnoDB使用的是一致性非鎖定讀。但有時咱們也須要顯示的指定使用一致性鎖定讀來保證讀取操做時對數據進行加鎖達到一致性。這要求數據庫支持鎖定讀加鎖語句:

  • select ... for update: 讀取時對行記錄加X鎖
  • select ... lock in share mode:讀取時對行記錄加一個S鎖

這兩種鎖必須在一個事務中,當事務提交後鎖也就釋放了,所以務必加上BEGIN, START TRANSACTION或者SET AUTOCOMMIT=0。

咱們在前面隔離級別時也說過SERIALIZABLE隔離級別會對讀操做自動加上LOCK IN SHARE MODE指令來加上一個共享鎖,所以再也不支持一致性的非鎖定讀。這也是隔離級別3的一大特性。

總結

因爲鎖的概念很是重要,這裏先講了鎖的概念、鎖的類型、鎖的信息查看、事務的隔離級別和區別,後面咱們會繼續說鎖的算法、鎖的三種問題和幻讀、死鎖和鎖升級。

喜歡的能夠先點贊,我會繼續第二篇。

參考資料

  1. 《MySQL技術內幕 InnoDB存儲引擎》 P6 鎖
相關文章
相關標籤/搜索