MySQL知識梳理圖,一圖看完整篇文章: mysql
MySQL系列文章:算法
定義bash
對比session
限制條件併發
行鎖和表鎖,在不一樣引擎還有所區別,MyISAM只有表鎖,沒有行鎖,不支持事務。 InnoDB 有行鎖和表鎖,支持事務。高併發
InnoDB 存儲引擎實現了兩種標準的行鎖,就是共享鎖,也稱叫S鎖,容許事務讀一行數據。排他鎖,也稱叫X鎖,容許事務刪除或更新一行數據。post
特性
加鎖方式
select語句 在查詢語句中,能夠經過在SQL語句中主動加鎖。
共享鎖:
select * from table where 索引限制 lock in share mode
記住,行鎖查詢是須要具有索引條件。好比執行: select * from user where id=1 lock in share mode. 其中 id 是主鍵。
排他鎖: select * from table where 索引限制 for update
好比執行: select name from user where id=1 for update.其中 id 是主鍵
insert or update or delete 語句。 InnoDB中對修改數據相關類SQL中,會自動給涉及到的數據加上排他鎖。
如何釋放鎖
查看當前鎖的狀態 能夠經過SQL語句 : show engine innodb status\G;
查看。
一致性的非鎖定讀是指InnoDB存儲引擎經過行多版本控制的方式來讀取當前執行時間數據庫中行的數據。 若是讀取的行的時候有正在執行的 Delete 或者 Update 操做,這時讀取操做不會等待行上鎖的釋放,而是InnoDB引擎會去讀取行的一個快照數據。
圖片來自於《MySQL技術內幕第2版》
能夠得知一致性非鎖定讀機制大大提高了數據庫的併發性,這也是InnoDB默認的讀取方式,即讀取不會佔用和等待表上的鎖。但不一樣事務隔離級別下,讀取的方式不一樣,對快照的定義也不一樣,一個行記錄可能有多個快照數據,通常稱這種技術爲行多版本技術,由此帶來的併發控制,稱之爲多版本併發控制(MVCC)
事務隔離級別 READ-COMMITTED vs REPEATABLE-READ
REPEATABLE-READ 是InnoDB默認的事務隔離級別,REPEATABLE-READ 對於快照數據,非一致性讀老是讀取事務開始時的行數據版本。
READ-COMMITTED 事務隔離級別下,對於快照數據,非一致性讀老是讀取被鎖定行的最新一份快照數據.
咱們來舉例看看,開啓2個終端,能夠經過下面命令開始事務會話:
`start transaction;` or `begin;` or `set autocommit=0`
複製代碼
經過 select @@tx_isolation\G;
能夠查看事務隔離級別,先來看看 REPEATABLE-READ 的狀況, 在SessionA 和 SessionB中,總共執行了6步,先執行1和2,都能查到id=2的內容,而後再SessionA中執行update操做,將id=2改成3,若是不執行commit操做,不管是REPEATABLE-READ or READ-COMMITTED ,都是能查到id=2的內容,但若是commit以後,REPEATABLE-READ仍是能夠繼續查看id=2的內容,演示數據以下 1-2-3-4-5-6 順序。
Session A:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation\G;
*************************** 1. row ***************************
@@tx_isolation: REPEATABLE-READ
mysql> select * from user where id=2; # 1
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 2 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.01 sec)
mysql> update user set id=3 where id=2; # 3
Query OK, 1 row affected (0.03 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit; # 5
Query OK, 0 rows affected (0.00 sec)
複製代碼
Session B:
mysql> select * from user where id=2; # 2
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 2 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.00 sec)
mysql> select * from user where id=2; # 4
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 2 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.00 sec)
mysql> select * from user where id=2; # 6
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 2 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.00 sec)
複製代碼
再來事務隔離級別爲READ-COMMITTED的狀況:
能夠經過命令 set session transaction isolation level read committed;
修改會話級的事務隔離級別。
以下面順序 1-2-3-4-5-6,能夠看出READ-COMMITTED下,SessionA commit以後,SessionB就更改了。
Session A
mysql> select @@tx_isolation\G;
*************************** 1. row ***************************
@@tx_isolation: READ-COMMITTED
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where id=4; # 1
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 4 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.01 sec)
mysql> update user set id=3 where id=4; # 3
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit; # 5
Query OK, 0 rows affected (0.00 sec)
複製代碼
Session B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation\G;
*************************** 1. row ***************************
@@tx_isolation: READ-COMMITTED
mysql> select * from user where id=4; # 2
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 4 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.00 sec)
mysql> select * from user where id=4; # 4
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 4 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.00 sec)
mysql> select * from user where id=4; # 6
Empty set (0.00 sec)
複製代碼
默認狀況下,InnoDB是一致性非鎖定讀,若是有些業務場景須要顯式的對數據庫讀取操做進行加鎖以保證數據邏輯的一致性。這就須要進行加鎖了,加鎖方式上面描述共享鎖和排他鎖的時候已經提到過,這裏再也不重複。
select ... for update
和 select ... lock in share mode
下面演示一下: 順序是 1-2-3-4,加鎖的前提是必須在一個事務中,因此開始一個事務,而後進行加共享鎖,若是未進行commit, SessionB執行update操做則會等待,等待的時候默認是50s,能夠查看相關mysql配置,若是再超時以前,SessionA執行了commit操做,則SessionB會立刻執行成功。
Session A:
ysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where id=3 lock in share mode; # 1
+----+--------+--------+------------+-----+
| id | gender | name | birthday | age |
+----+--------+--------+------------+-----+
| 3 | boy | xiao12 | 1995-08-03 | 20 |
+----+--------+--------+------------+-----+
1 row in set (0.00 sec)
mysql> commit; # 3
Query OK, 0 rows affected (0.00 sec)
複製代碼
Session B:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update user set id=4 where id=3; # 2
# 等待
ysql> update user set id=4 where id=3; # 4
Query OK, 1 row affected (18.18 sec)
Rows matched: 1 Changed: 1 Warnings: 0
複製代碼
舉例,索引有10,11,13,20這四個值。
select * from user where id=3
則只會鎖定id=3這一行,即降級爲Record Rock算法。 若是是輔助索引,則狀況會有所不一樣,舉例解釋一下,這裏有點繞。
CREATE TABLE z (a INT, b INT, PRIMARY KEY(a), KEY(b));
INSERT INTO z SELECT 1,1;
INSERT INTO z SELECT 3,1;
INSERT INTO z SELECT 5,3;
INSERT INTO z SELECT 7,6;
INSERT INTO z SELECT 10,8;
複製代碼
執行上面語句,會建立一個z表,同時數據庫裏有以下數據
mysql> select * from z;
+----+------+
| a | b |
+----+------+
| 1 | 1 |
| 3 | 1 |
| 5 | 3 |
| 7 | 6 |
| 10 | 8 |
+----+------+
5 rows in set (0.00 sec)
複製代碼
如今開啓一個會話A和B。
Session A:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from z where b=3 for update; # 1 給輔助索引b=3加上X鎖
+---+------+
| a | b |
+---+------+
| 5 | 3 |
+---+------+
複製代碼
給輔助索引b=3加上X鎖以後,因爲使用的Next-Key Lock算法,而且有涉及到a=5的主鍵索引,會首先對 a=5 進行Record Lock鎖定,而後對b=3進行Next-Key Lock鎖定,即鎖定(1, 3]。須要特別注意的是,InnoDB還會對輔助索引的下一個鍵(6)加上Gap Lock鎖,即鎖定(3, 6)。
因此若是再SessionB中執行下面語句會是等待嗎?
Session B:
select * from z where a=5 lock in share mode; #2
insert into z select 4, 2; #3
insert into z select 6, 5; #4
insert into z select 8, 6; #5
複製代碼
執行2,發現須要等待,緣由是a=5索引已經被加上了X鎖。 執行3,主鍵寫入4沒有問題,但輔助索引2是在鎖定的範圍(1,3)中。 執行4,主鍵寫入6沒有問題,但輔助索引5是在鎖定的範圍(3,6)中。 執行5,主鍵8和輔助索引6均沒有問題,能夠寫入。
說了這麼多,接下來講一下如何關閉Gap Lock。
因此將隔離級別設置爲READ-COMMITTED要謹慎。關閉GapLock以後,除了外鍵約束和惟一性檢查還須要GapLoc,其他狀況僅使用RecordLock進行鎖定。這樣設置會破壞事務的隔離性。下面來講一下這個問題。
先來認識一個名詞: Phantom Problem,幻像問題。Innodb存儲引擎採用Next-Key Lock算法就是爲了不Phantom Problem。
Phantom Problem是指同一個事務下,連續執行兩次一樣的SQL語句可能致使不一樣的結果,第二次的SQL語句可能返回以前不存在的行。
舉例分別在SessionA 和 SessionB中按順序1-2-3-4執行。
Session A:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation\G;
*************************** 1. row ***************************
@@tx_isolation: READ-COMMITTED
mysql> select * from z where a>2 for update; # 1
+----+------+
| a | b |
+----+------+
| 3 | 1 |
| 5 | 3 |
| 7 | 6 |
| 10 | 8 |
+----+------+
4 rows in set (0.00 sec)
mysql> select * from z where a>2 for update; # 4
+----+------+
| a | b |
+----+------+
| 3 | 1 |
| 4 | 0 |
| 5 | 3 |
| 7 | 6 |
| 10 | 8 |
+----+------+
5 rows in set (0.00 sec)
複製代碼
Session B:
mysql> insert into z select 4, 0; # 2
Query OK, 1 row affected (0.01 sec)
Records: 1 Duplicates: 0 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec) # 3
複製代碼
結果發現SessionA中,事務還沒結束,執行1和4返回的結果不同,這樣就是違法了事務的隔離性。
若是使用事務隔離級別爲: REPEATABLE-READ。會使用Next-Key Lock算法,則上面執行1則會鎖定(2,+∞),從而第2步會等待,進而避免了Phantom Problem問題。
說了這麼多,總結幾點InnoDB默認下的幾種狀況:
鎖機制雖然能夠實現事務的隔離性要求,使得事務能夠併發的工做,不過也會帶來幾個潛在的問題。
髒讀是指不一樣事務下, 當前事務能夠讀到另外事務未提交的數據。這個通常生產環境不多遇到,且只會發生在事務隔離級別爲READ-UNCOMMITTED的狀況下,這種事務隔離設置不多見。具體演示,感興趣的能夠試一下。
不可重複讀是指一個事務內屢次讀取同一數據集合,獲得數據結果不同。與髒讀的區別是,髒讀讀取到未提交的數據,而不可重複讀讀取到了已經提交的數據,可是違反了數據庫事務一致性的要求,當前事務未結束,先後兩次相同查詢獲得了不同的結果。
這種狀況上面已經有演示過,當事務隔離級別是READ-COMMITTED,則會發生這種狀況。
丟失更新就是一個事務的更新操做會被另一個事務的更新操做所覆蓋,從而致使數據的不一致。 好比:
上面舉例理論上在MySQL的事務隔離級別,都不會發生丟失更新,由於對行進行更新操做,都會對行繼續加鎖,因此第2步並不會執行成功,而是會阻塞,等待事務T1提交。
但丟失更新在生產環境是會發生的,出如今下面的狀況:
致使這個問題,並非由於數據庫自己的問題,而是在多用戶系統環境下,高併發讀取信息都有可能會產生這個問題。好比容易發生在帳單金額方面的場景。 要避免此類丟失更新發送,則須要事務在這種狀況下的操做變成串行化,而不是並行操做。須要再1)中用戶讀取的記錄加上一個排他鎖(X鎖),這樣2)則讀的時候須要等待1)3)事務結束才能夠讀到。從而避免了丟失更新的問題。
接下來,來看看死鎖問題。 死鎖是指兩個或兩個以上的事務在執行過程當中,因爭奪鎖資源而形成的一種互相等待的現象。
通常比較簡單的解決死鎖的問題是超時,當兩個事務互相等待時,當一個等待時間超過設置的閥值時,則該事務進行回滾,另外一個等待的事務則繼續進行。能夠經過innodb_lock_wait_timeout來設置超時的時間。
除了超時機制,數據庫普通採用等待圖(wait-for graph)的方式來進行死鎖檢測,Innodb採用的是這種方式來進行死鎖檢測。
wait-for graph須要2個信息:
舉例:
圖中有t1,t2,t3,t4 4個事務,事務t1須要等待t2中row1的資源,則wait-for graph有節點t1指向t2。事務t2又須要等待t1,t4的資源,事務t3須要等待t1,t4,t2的資源,從而構成如下wait-for graph
能夠看見t1和t2之間造成迴路,從而存在死鎖。
死鎖實際舉例:
A | B |
---|---|
select * from user where id=2 for update; | begin |
select * from user where id=8 for update; | |
select * from user where id=8 for update; (等待) | |
select * from user where id=2 for update; \ ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |
如上表,SessionA 先給id=2行加上X鎖,SessionB則給id=8加上X鎖,SessionA也準備想給id=8加上X鎖,從而處於等待中,須要等待SessionB是否id=8的鎖,SessionB在未是否id=8的鎖以前,又想給id=2加上X鎖,從而使SessionA和SessionB互相等待,出現死鎖。
上面講了那麼一大堆鎖相關的知識,接下來來看看了解這些知識有什麼用。 常見的一個場景,秒殺系統。雙11或者電商搶購的時候,常常是多用戶搶購一個商品,庫存確定是頗有限的,如何控制庫存不讓出現超買超賣,以防止形成沒必要要的損失。
仔細想一想,其實跟上面鎖知識中描述的丟失更新相似,假設庫存只剩下一個,若是查詢的時候不加任何鎖,也不開啓事務。同時a、b、c三個用戶讀到了這一個庫存,而後程序也均經過了,a、b、c用戶付款後,依次更新數據庫的庫存,這時候發現庫存出現負值,形成商家的損失。
如何避免了?
若是顯式的給查詢的時候加上S鎖(共享鎖),有用嗎?顯然根據上面的鎖知識得知,仍是會出現,由於共享鎖跟共享鎖是兼容了,能夠都讀取,只是不能寫入。這樣a、b、c仍是會都讀到最後一個庫存。
因此只能使用排他鎖了(X鎖)。
總結以下:
若是不開啓事務,讀取結束後就會是否鎖,因此必定要先開啓事務。
固然這樣加鎖,高併發的狀況,實際生產環境不會這麼作,大量的數據庫讀寫對性能和DB都有很大的壓力。實際過程當中,均會引入緩存、隊列等來協助實現秒殺系統。這只是單純從數據庫層面進行分析。
這一篇文章就到這裏,下一篇繼續對MySQL事務繼續分析瞭解。
更多精彩文章,請關注公衆號『天澄技術雜談』