本文首發於 vivo 互聯網技術微信公衆號 https://mp.weixin.qq.com/s/JF...
做者:張碩
本文對 MySQL 數據庫中有關鎖、事務及併發控制的知識及其原理作了系統化的介紹和總結,但願幫助讀者能更加深入地理解 MySQL 中的鎖和事務,從而在業務系統開發過程當中能夠更好地優化與數據庫的交互。
目錄
1.MySQL 服務器邏輯架構
2.MySQL 鎖
3.事務
4.隔離級別
5.併發控制 與 MVCC
6.MySQL 死鎖問題html
每一個鏈接都會在 MySQL 服務端產生一個線程(內部經過線程池管理線程),好比一個 select 語句進入,MySQL 首先會在查詢緩存中查找是否緩存了這個 select 的結果集,若是沒有則繼續執行解析、優化、執行的過程;不然會之間從緩存中獲取結果集。mysql
2.一、Shared and Exclusive Locks (共享鎖與排他鎖)算法
它們都是標準的行級鎖。sql
共享鎖(S)共享鎖也稱爲讀鎖,讀鎖容許多個鏈接能夠同一時刻併發的讀取同一資源,互不干擾;數據庫
排他鎖(X)排他鎖也稱爲寫鎖,一個寫鎖會阻塞其餘的寫鎖或讀鎖,保證同一時刻只有一個鏈接能夠寫入數據,同時防止其餘用戶對這個數據的讀寫。緩存
注意:所謂共享鎖、排他鎖其實均是鎖機制自己的策略,經過這兩種策略對鎖作了區分。服務器
2.二、Intention Locks(意向鎖)微信
InnoDB 支持多粒度鎖(鎖粒度可分爲行鎖和表鎖),容許行鎖和表鎖共存。例如,一個語句,例如 LOCK TABLES…WRITE 接受指定表上的獨佔鎖。爲了實現多粒度級別的鎖定,InnoDB 使用了意圖鎖。session
意向鎖:表級別的鎖。先提早聲明一個意向,並獲取表級別的意向鎖(共享意向鎖 IS 或排他意向鎖 IX),若是獲取成功,則稍後將要或正在(才被容許),對該表的某些行加鎖(S或X)了。(除了 LOCK TABLES ... WRITE,會鎖住表中全部行,其餘場景意向鎖實際不鎖住任何行)架構
舉例來講:
SELECT ... LOCK IN SHARE MODE,要獲取IS鎖;An intention shared lock (IS)
SELECT ... FOR UPDATE ,要獲取IX鎖;An intention exclusive lock (IX) i
意向鎖協議在事務可以獲取表中的行上的共享鎖以前,它必須首先獲取表上的IS鎖或更強的鎖。 在事務可以獲取表中的行上的獨佔鎖以前,它必須首先獲取表上的IX鎖。
前文說了,意向鎖實現的背景是多粒度鎖的並存場景。以下兼容性的彙總:
意向鎖僅表意向,是一種較弱的鎖,意向鎖之間兼容並行(IS、IX 之間關係兼容並行)。 X與ISIX互斥;S與IX互斥。能夠體會到,意向鎖是比XS更弱的鎖,存在一種預判的意義!先獲取更弱的IXIS鎖,若是獲取失敗就沒必要要再花費跟大開銷獲取更強的XS鎖 ... ...
2.三、Record Locks (索引行鎖)
record lock 是一個在索引行記錄的鎖。好比,SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE,若是c1 上的索引被使用到。防止任何其餘事務變更 c1 = 10 的行。
record lock 老是會在索引行上加鎖。即便一個表並無設置任何索引,這種時候 innoDB 會建立一個隱式的彙集索引(primary Key),而後在這個彙集索引上加鎖。
當查詢字段沒有索引時,好比 update table set columnA="A" where columnB=「B".若是 columnB 字段不存在索引(或者不是組合索引前綴),這條語句會鎖住全部記錄也就是鎖表。若是語句的執行可以執行一個 columnB 字段的索引,那麼僅會鎖住知足 where 的行(RecordLock)。
鎖出現查看示例:
(使用 show engine innodb status 命令查看):
2.四、Gap locks(間隙鎖)
Gap Locks:鎖定索引記錄之間的間隙([2]),或者鎖定一個索引記錄以前的間隙([1]),或者鎖定一個索引記錄以後的間隙([3])。
示例:如圖[1]、[2]、[3]部分。通常做用於咱們的範圍篩選查詢> 、< 、between......
例如, SELECT userId FROM t1 WHERE userId BETWEEN 1 and 4 FOR UPDATE; 阻止其餘事務將值3插入到列 userId 中。由於該範圍內全部現有值之間的間隙都是鎖定的。
對於使用惟一索引來搜索惟一行的語句 select a from ,不產生間隙鎖定。(不包含組合惟一索引,也就是說 gapLock 不做用於單列惟一索引)
例如,若是id列有惟一的索引,下面的語句只對id值爲100的行使用索引記錄鎖,其餘會話是否在前一個間隙中插入行並不重要:
間隙能夠跨越單個索引值、多個索引值(如上圖2,3),甚至是空的。
間隙鎖是性能和併發性之間權衡的一種折衷,用於某些特定的事務隔離級別,如RC級別(RC級別:REPEATABLE READ,我司爲了減小死鎖,關閉了gap鎖,使用RR級別)。
在重疊的間隙中(或者說重疊的行記錄)中容許gap共存好比同一個 gap 中,容許一個事務持有 gap X-Lock(gap 寫鎖排他鎖),同時另外一個事務在這個 gap 中持有(gap 寫鎖排他鎖)
CREATE TABLE `new_table` ( `id` int(11) NOT NULL AUTO_INCREMENT, `a` int(11) DEFAULT NULL, `b` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_new_table_a` (`a`), KEY `idx_new_table_b` (`b`) ) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8 INSERT INTO `new_table` VALUES (1,1,'1'),(2,3,'2'),(3,5,'3'),(4,8,'4'),(5,11,'5'),(6,2,'6'),(7,2,'7'),(8,2,'8'),(9,4,'9'),(10,4,'10'); ######## 事務一 ######## START TRANSACTION; SELECT * FROM new_table WHERE a between 5 and 8 FOR UPDATE; ##暫不commit ######## 事務二 ######## SELECT * FROM new_table WHERE a = 4 FOR UPDATE; ##順利執行! 由於gap鎖能夠共存; ######## 事務三 ######## SELECT * FROM new_table WHERE b = 3 FOR UPDATE; ##獲取鎖超時,失敗。由於事務一的gap鎖定了 b=3的數據。
2.五、next-key lock
**next-key lock 是 record lock 與 gap lock 的組合。
好比 存在一個查詢匹配 b=3 的行(b上有個非惟一索引),那麼所謂 NextLock 就是:在b=3 的行加了 RecordLock 而且使用 GapLock 鎖定了 b=3 以前(「以前」:索引排序)的全部行記錄。**
MySQL 查詢時執行 行級鎖策略,會對掃描過程當中匹配的行進行加鎖(X 或 S),也就是加Record Lock,同時會對這個記錄以前的全部行加 GapLock 鎖。 假設一個索引包含值十、十一、13和20。該索引可能的NexKey Lock鎖定如下區間:
(negative infinity, 10] (10, 11] (11, 13] (13, 20] (20, positive infinity)
另外,值得一提的是 : innodb 中默認隔離級別(RR)下,next key Lock 自動開啓。(很好理解,由於 gap 做用於RR,若是是 RC,gapLock 不會生效,那麼 next key lock 天然也不會)
鎖出現查看示例:(使用 show engine innodb status 命令查看):
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t` trx id 10080 lock_mode X Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; 1: len 6; hex 00000000274f; asc 'O;; 2: len 7; hex b60000019d0110; asc ;;
2.六、Insert Intention Locks(插入意向鎖)
一個 insert intention lock 是一種發生在 insert 插入語句時的 gap lock 間隙鎖,鎖定插入行以前的全部行。
這個鎖以這樣一種方式代表插入的意圖,若是插入到同一索引間隙中的多個事務沒有插入到該間隙中的相同位置,則它們不須要等待對方。
假設存在值爲4和7的索引記錄。嘗試分別插入值爲5和6的獨立事務,在得到所插入行上的獨佔鎖以前,每一個事務使用 insert intention lock 鎖定4和7之間的間隙,但不會阻塞彼此,由於這些行不衝突。
示例:
mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB; mysql> INSERT INTO child (id) values (90),(102); ##事務一 mysql> START TRANSACTION; mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE; +-----+ | id | +-----+ | 102 | +-----+
##事務二 mysql> START TRANSACTION; mysql> INSERT INTO child (id) VALUES (101); ##失敗,已被鎖定 mysql> SHOW ENGINE INNODB STATUS RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child` trx id 8731 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 80000066; asc f;; 1: len 6; hex 000000002215; asc " ;; 2: len 7; hex 9000000172011c; asc r ;;...
2.七、 AUTO-INC Locks
AUTO-INC 鎖是一種特殊的表級鎖,產生於這樣的場景:事務插入(inserting into )到具備 AUTO_INCREMENT 列的表中。
在最簡單的狀況下,若是一個事務正在向表中插入值,那麼其餘任何事務必須等待向該表中插入它們本身的值,以便由第一個事務插入的行接收連續的主鍵值。
2.8 Predicate Locks for Spatial Indexes 空間索引的謂詞鎖
略
事務就是一組原子性的 sql,或者說一個獨立的工做單元。 事務就是說,要麼 MySQL 引擎會所有執行這一組sql語句,要麼所有都不執行(好比其中一條語句失敗的話)。
· 自動提交(AutoCommit,MySQL 默認)
show variables like "autocommit"; set autocommit=0; //0表示AutoCommit關閉 set autocommit=1; //1表示AutoCommit開啓
MySQL 默認採用 AutoCommit 模式,也就是每一個 sql 都是一個事務,並不須要顯示的執行事務。若是 autoCommit 關閉,那麼每一個 sql 都默認開啓一個事務,只有顯式的執行「commit」後這個事務纔會被提交。
· 顯示事務 (START TRANSACTION...COMMIT)
好比,tim 要給 bill 轉帳100塊錢:
1.檢查 tim 的帳戶餘額是否大於100塊;
2.tim 的帳戶減小100塊;
3.bill 的帳戶增長100塊;
這三個操做就是一個事務,必須打包執行,要麼所有成功, 要麼所有不執行,其中任何一個操做的失敗都會致使全部三個操做「不執行」——回滾。
CREATE DATABASE IF NOT EXISTS employees; USE employees; CREATE TABLE `employees`.`account` ( `id` BIGINT (11) NOT NULL AUTO_INCREMENT, `p_name` VARCHAR (4), `p_money` DECIMAL (10, 2) NOT NULL DEFAULT 0, PRIMARY KEY (`id`) ) ; INSERT INTO `employees`.`account` (`id`, `p_name`, `p_money`) VALUES ('1', 'tim', '200'); INSERT INTO `employees`.`account` (`id`, `p_name`, `p_money`) VALUES ('2', 'bill', '200'); START TRANSACTION; SELECT p_money FROM account WHERE p_name="tim";-- step1 UPDATE account SET p_money=p_money-100 WHERE p_name="tim";-- step2 UPDATE account SET p_money=p_money+100 WHERE p_name="bill";-- step3 COMMIT;
一個良好的事務系統,必須知足ACID特色:
3.一、事務的ACID:
· A:atomiciy 原子性:一個事務必須保證其中的操做要麼所有執行,要麼所有回滾,不可能存在只執行了一部分這種狀況出現。
· C:consistency 一致性:數據必須保證從一種一致性的狀態轉換爲另外一種一致性狀態。好比上一個事務中執行了第二步時系統崩潰了,數據也不會出現 bill 的帳戶少了100塊,可是 tim 的帳戶沒變的狀況。要麼維持原裝(所有回滾),要麼 bill 少了100塊同時 tim 多了100塊,只有這兩種一致性狀態的。
· I:isolation 隔離性:在一個事務未執行完畢時,一般會保證其餘 Session 沒法看到這個事務的執行結果。
· D:durability 持久性:事務一旦 commit,則數據就會保存下來,即便提交完以後系統崩潰,數據也不會丟失。
查看系統隔離級別:
select @@global.tx_isolation;查看當前會話隔離級別
select @@tx_isolation;設置當前會話隔離級別
SET session TRANSACTION ISOLATION LEVEL serializable;設置全局系統隔離級別
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
4.一、 READ UNCOMMITTED (未提交讀,可髒讀)
事務中的修改,即便沒有提交,對其餘會話也是可見的。能夠讀取未提交的數據——髒讀。髒讀會致使不少問題,通常不適用這個隔離級別。 實例:
-- ------------------------- read-uncommitted實例 ------------------------------ -- 設置全局系統隔離級別 SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- Session A START TRANSACTION; SELECT * FROM USER; UPDATE USER SET NAME="READ UNCOMMITTED"; -- commit; -- Session B SELECT * FROM USER; //SessionB Console 能夠看到Session A未提交的事物處理,在另外一個Session 中也看到了,這就是所謂的髒讀 id name 2 READ UNCOMMITTED 34 READ UNCOMMITTED
4.二、READ COMMITTED (提交讀或不可重複讀,幻讀)
通常數據庫都默認使用這個隔離級別(MySQL 不是), 這個隔離級別保證了一個事務若是沒有徹底成功(commit 執行完),事務中的操做對其餘會話是不可見的。
-- ------------------------- read-cmmitted實例 ------------------------------ -- 設置全局系統隔離級別 SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED; -- Session A START TRANSACTION; SELECT * FROM USER; UPDATE USER SET NAME="READ COMMITTED"; -- COMMIT; -- Session B SELECT * FROM USER; //Console OUTPUT: id name 2 READ UNCOMMITTED 34 READ UNCOMMITTED --------------------------------------------------- -- 當 Session A執行了commit,Session B獲得以下結果: id name 2 READ COMMITTED 34 READ COMMITTED
也就驗證了read committed 級別在事物未完成 commit 操做以前修改的數據對其餘 Session 不可見,執行了 commit 以後纔會對其餘 Session 可見。 咱們能夠看到 Session B 兩次查詢獲得了不一樣的數據。
read committed 隔離級別解決了髒讀的問題,可是會對其餘 Session 產生兩次不一致的讀取結果(由於另外一個 Session 執行了事務,一致性變化)。
4.三、 REPEATABLE READ (可重複讀)
一個事務中屢次執行統一讀 SQL,返回結果同樣。 這個隔離級別解決了髒讀的問題,幻讀問題。這裏指的是 innodb 的 rr 級別,innodb 中使用 next-key 鎖對"當前讀"進行加鎖,鎖住行以及可能產生幻讀的插入位置,阻止新的數據插入產生幻行。 下文中詳細分析。具體請參考 MySQL 手冊:
https://dev.mysql.com/doc/ref...
4.四、 SERIALIZABLE (可串行化)
最強的隔離級別,經過給事務中每次讀取的行加鎖,寫加寫鎖,保證不產生幻讀問題,可是會致使大量超時以及鎖爭用問題。
MVCC (multiple-version-concurrency-control)它是個行級鎖的變種, 在普通讀狀況下避免了加鎖操做,所以開銷更低。雖然實現不一樣,但一般都是實現非阻塞讀,對於寫操做只鎖定必要的行。
· 一致性讀 (就是讀取快照)select * from table ....
· 當前讀(就是讀取實際的持久化的數據)特殊的讀操做,插入/更新/刪除操做,屬於當前讀,處理的都是當前的數據,須要加鎖。 select from table where ? lock in share mode; select from table where ? for update; insert; update ; delete;
注意:select ...... from where...... (沒有額外加鎖後綴)使用MVCC,保證了讀快照(MySQL 稱爲 consistent read),所謂一致性讀或者讀快照就是讀取當前事務開始以前的數據快照,在這個事務開始以後的更新不會被讀到。詳細狀況下文 select 的詳述。
對於加鎖讀 SELECT with FOR UPDATE (排他鎖) or LOCK IN SHARE MODE (共享鎖)、 update、delete語句,要考慮是不是惟一索引的等值查詢。
INNODB 的 MVCC 一般是經過在每行數據後邊保存兩個隱藏的列來實現(實際上是三列,第三列是用於事務回滾,此處略去),一個保存了行的建立版本號,另外一個保存了行的更新版本號(上一次被更新數據的版本號) 這個版本號是每一個事務的版本號,遞增的。這樣保證了 innodb 對讀操做不須要加鎖也能保證正確讀取數據。
5.一、MVCC select無鎖操做 與 維護版本號
下邊在 MySQL 默認的 Repeatable Read 隔離級別下,具體看看 MVCC 操做:
· Select(快照讀,所謂讀快照就是讀取當前事務以前的數據。):
a.InnoDB 只 select 查找版本號早於當前版本號的數據行,這樣保證了讀取的數據要麼是在這個事務開始以前就已經 commit 了的(早於當前版本號),要麼是在這個事務自身中執行建立操做的數據(等於當前版本號)。
b.查找行的更新版本號要麼未定義,要麼大於當前的版本號(爲了保證事務能夠讀到老數據),這樣保證了事務讀取到在當前事務開始以後未被更新的數據。
注意: 這裏的 select 不能有 for update、lock in share 語句。 總之要只返回知足如下條件的行數據,達到了快照讀的效果:
(行建立版本號< =當前版本號 && (行更新版本號==null or 行更新版本號>當前版本號 ) )
· Insert InnoDB爲這個事務中新插入的行,保存當前事務版本號的行做爲行的行建立版本號。
· Delete InnoDB 爲每個刪除的行保存當前事務版本號,做爲行的刪除標記。
· Update 將存在兩條數據,保持當前版本號做爲更新後的數據的新增版本號,同時保存當前版本號做爲老數據行的更新版本號。
當前版本號—寫—>新數據行建立版本號 && 當前版本號—寫—>老數據更新版本號();
5.二、髒讀 vs 幻讀 vs 不可重複讀
髒讀:一事務未提交的中間狀態的更新數據 被其餘會話讀取到。
當一個事務正在訪問數據,而且對數據進行了修改, 而這種修改尚未 提交到數據庫中(commit 未執行), 這時,另外會話也訪問這個數據,由於這個數據是尚未提交, 那麼另一個會話讀到的這個數據是髒數據,依據髒數據所作的操做也多是不正確的。
不可重複讀:簡單來講就是在一個事務中讀取的數據可能產生變化,ReadCommitted 也稱爲不可重複讀。
在同一事務中,屢次讀取同一數據返回的結果有所不一樣。 換句話說就是,後續讀取能夠讀到另外一會話事務已提交的更新數據。 相反,「可重複讀」在同一事務中屢次讀取數據時,可以保證所讀數據同樣, 也就是,後續讀取不能讀到另外一會話事務已提交的更新數據。
幻讀:會話T1事務中執行一次查詢,而後會話T2新插入一行記錄,這行記錄剛好能夠知足T1所使用的查詢的條件。而後T1又使用相同 的查詢再次對錶進行檢索,可是此時卻看到了事務T2剛纔插入的新行。這個新行就稱爲「幻像」,由於對T1來講這一行就像忽然 出現的同樣。innoDB 的 RR 級別沒法作到徹底避免幻讀,下文詳細分析。
5.三、 如何保證 rr 級別絕對不產生幻讀?
在使用的 select ...where 語句中加入 for update (排他鎖) 或者 lock in share mode (共享鎖)語句來實現。其實就是鎖住了可能形成幻讀的數據,阻止數據的寫入操做。
實際上是由於數據的寫入操做(insert 、update)須要先獲取寫鎖,因爲可能產生幻讀的部分,已經獲取到了某種鎖,因此要在另一個會話中獲取寫鎖的前提是當前會話中釋放全部因加鎖語句產生的鎖。
5.四、 從另外一個角度看鎖:顯式鎖、隱式鎖
隱式鎖:咱們上文說的鎖都屬於不須要額外語句加鎖的隱式鎖。
顯示鎖:
SELECT ... LOCK IN SHARE MODE(加共享鎖);SELECT ... FOR UPDATE(加排他鎖);
詳情上文已經說過。
5.五、查看鎖狀況
經過以下 sql 能夠查看等待鎖的狀況
select * from information_schema.innodb_trx where trx_state="lock wait"; 或 show engine innodb status;
死鎖,就是產生了循環等待鏈條,我等待你的資源,你卻等待個人資源,咱們都相互等待,誰也不釋放本身佔有的資源,致使無線等待下去。 好比:
//Session A START TRANSACTION; UPDATE account SET p_money=p_money-100 WHERE p_name="tim"; UPDATE account SET p_money=p_money+100 WHERE p_name="bill"; COMMIT; //Thread B START TRANSACTION; UPDATE account SET p_money=p_money+100 WHERE p_name="bill"; UPDATE account SET p_money=p_money-100 WHERE p_name="tim"; COMMIT;
當線程A執行到第一條語句UPDATE account SET p_money=p_money-100 WHERE p_name="tim";鎖定了p_name="tim" 的行數據;而且試圖獲取p_name="bill" 的數據;
此時,剛好,線程B也執行到第一條語句:UPDATE account SET p_money=p_money+100 WHERE p_name="bill";鎖定了 p_name="bill" 的數據,同時試圖獲取p_name="tim" 的數據;
此時,兩個線程就進入了死鎖,誰也沒法獲取本身想要獲取的資源,進入無線等待中,直到超時!
innodb_lock_wait_timeout 等待鎖超時回滾事務:
直觀方法是在兩個事務相互等待時,當一個等待時間超過設置的某一閥值時,對其中一個事務進行回滾,另外一個事務就能繼續執行。
這種方法簡單有效,在i nnodb 中,參數innodb_lock_wait_timeout 用來設置超時時間。
wait-for graph 算法來主動進行死鎖檢測:innodb 還提供了 wait-for graph算法來主動進行死鎖檢測,每當加鎖請求沒法當即知足須要並進入等待時,wait-for graph 算法都會被觸發。
6.一、如何儘量避免死鎖
以固定的順序訪問表和行。好比兩個更新數據的事務,事務A 更新數據的順序 爲1,2;事務B更新數據的順序爲2,1。這樣更可能會形成死鎖;
大事務拆小。大事務更傾向於死鎖,若是業務容許,將大事務拆小;
在同一個事務中,儘量作到一次鎖定所須要的全部資源,減小死鎖機率;
下降隔離級別。若是業務容許,將隔離級別調低也是較好的選擇,好比將隔離級別從RR調整爲RC,能夠避免掉不少由於gap鎖形成的死鎖。(我司 MySQL 規範作法);
爲表添加合理的索引。能夠看到若是不走索引將會爲表的每一行記錄添加上鎖,死鎖的機率大大增大。
延伸閱讀:
· MySQL官網參考文檔:https://dev.mysql.com/doc/ref...
· 更多內容敬請關注 vivo互聯網技術微信公衆號。
版權聲明:轉載文章請先與微信號 labs2020 聯繫。