InnoDB存儲引擎支持行級鎖,支持事務處理,事務是有一組SQL語句組成的邏輯處理單元,他的ACID特性以下:mysql
併發事務能提升數據庫資源的利用率,提升了數據庫的事務吞吐量,但併發事務也存在一些問題,主要包括:sql
數據庫併發中的「更新丟失」一般應該是徹底避免的,但防止更新丟失數據,並不能單靠數據庫事務控制來解決,須要應用程序對要更新的數據加必要的鎖來解決,而以上出現的數據庫問題都必要由數據庫提供必定的事務隔離機制來解決。爲了不數據庫事務併發帶來的問題,在標準SQL規範中定義了4個事務的隔離級別,不一樣的隔離級別對事務處理不同。數據庫
隔離級別 | 讀數據一致性 | 髒讀 | 不可重複讀 | 幻讀 |
---|---|---|---|---|
未提交讀 (Read uncommitted) |
最低級別,只能保證不讀取物理上損壞的數據 | 是 | 是 | 是 |
已提交讀 (Read committed) |
語句級 | 否 | 是 | 是 |
可重複讀 (Repeatable read) |
事務級 | 否 | 否 | 是 |
可序列化 (Serializable) |
最高級別,事務級 | 否 | 否 | 否 |
InnoDB存儲引擎實現了4中行鎖,分別時共享鎖(S)、排他鎖(X)、意向共享鎖(IS)、意向排他鎖(IX)。session
意向鎖不會和行級的S和X鎖衝突,只會和表級的S和X鎖衝突
意向鎖是爲了不遍歷所有行鎖併發
考慮這個例子:
事務A鎖住了表中的一行,讓這一行只能讀,不能寫。
以後,事務B申請整個表的寫鎖。rest
若是事務B申請成功,那麼理論上它就能修改表中的任意一行,這與A持有的行鎖是衝突的。code
數據庫須要避免這種衝突,就是說要讓B的申請被阻塞,直到A釋放了行鎖。
數據庫要怎麼判斷這個衝突呢?
step1:判斷表是否已被其餘事務用表鎖鎖表
step2:判斷表中的每一行是否已被行鎖鎖住。
注意step2,這樣的判斷方法效率實在不高,由於須要遍歷整個表。索引
因而就有了意向鎖。
在乎向鎖存在的狀況下,事務A必須先申請表的意向共享鎖,成功後再申請一行的行鎖。
在乎向鎖存在的狀況下,上面的判斷能夠改爲
step1:不變
step2:發現表上有意向共享鎖,說明表中有些行被共享行鎖鎖住了,所以,事務B申請表的寫鎖會被阻塞。事務
1.1經過索引檢索數據,上共享鎖,行鎖 SessionA SessionB mysql> set autocommit=0; mysql> set autocommit=0; Query OK, 0 rows affected (0.02 sec) Query OK, 0 rows affected (0.02 sec) mysql> select * from test; mysql> select * from test; -------------------------------------------------------------------------------- +----+-------+-------+-------+ +----+-------+-------+-------+ | id | name | money | level | | id | name | money | level | +----+-------+-------+-------+ +----+-------+-------+-------+ | 1 | tom | 100 | 1 | | 1 | tom | 100 | 1 | | 2 | jack | 200 | 2 | | 2 | jack | 200 | 2 | | 3 | lucas | 300 | 3 | | 3 | lucas | 300 | 3 | +----+-------+-------+-------+ +----+-------+-------+-------+ 3 rows in set (0.00 sec) 3 rows in set (0.00 sec) -------------------------------------------------------------------------------- 對主鍵索引上共享鎖,其餘事務也能獲取到共享鎖 mysql> select * from test where id=1 lock in share mode; +----+------+-------+-------+ | id | name | money | level | +----+------+-------+-------+ | 1 | tom | 100 | 1 | +----+------+-------+-------+ 1 row in set (0.01 sec) -------------------------------------------------------------------------------- 事務B也能繼續加共享鎖 mysql> select * from test where id=1 lock in share mode; +----+------+-------+-------+ | id | name | money | level | +----+------+-------+-------+ | 1 | tom | 100 | 1 | +----+------+-------+-------+ 1 row in set (0.01 sec) 但沒法更新,由於事務A也加了共享鎖 mysql> update test set level=11 where id=1; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction MORE: 沒法加排它鎖 select *from test where id=1 for update; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 能夠更新未加鎖的,好比 mysql> update test set level=11 where id=2; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 -------------------------------------------------------------------------------- 事務A也沒法更新,由於事務B加了共享鎖 mysql> update test set level=11 where id=1; ERROR 1205 (HY000): Lock wait timeout excee ded; try restarting transaction -------------------------------------------------------------------------------- 任意一個釋放共享鎖,則獨佔共享鎖的事務能夠更新 mysql> commit; Query OK, 0 rows affected (0.00 sec) -------------------------------------------------------------------------------- 事務B釋放鎖,事務A獨佔,能夠更新了 mysql> update test set level=11 where id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
1.2經過索引檢索數據,上排他鎖,行鎖 SessionA SessionB mysql> set autocommit=0; mysql> set autocommit=0; Query OK, 0 rows affected (0.02 sec) Query OK, 0 rows affected (0.02 sec) mysql> select * from test; mysql> select * from test; -------------------------------------------------------------------------------- +----+-------+-------+-------+ +----+-------+-------+-------+ | id | name | money | level | | id | name | money | level | +----+-------+-------+-------+ +----+-------+-------+-------+ | 1 | tom | 100 | 1 | | 1 | tom | 100 | 1 | | 2 | jack | 200 | 2 | | 2 | jack | 200 | 2 | | 3 | lucas | 300 | 3 | | 3 | lucas | 300 | 3 | +----+-------+-------+-------+ +----+-------+-------+-------+ 3 rows in set (0.00 sec) 3 rows in set (0.00 sec) -------------------------------------------------------------------------------- 對主鍵索引上排他鎖 mysql> select *from test where id=1 for update; +----+------+-------+-------+ | id | name | money | level | +----+------+-------+-------+ | 1 | tom | 100 | 1 | +----+------+-------+-------+ 1 row in set (0.01 sec) -------------------------------------------------------------------------------- 事務B則不能繼續上排它鎖,會發生等待 mysql> select *from test where id=1 for update; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction MORE: 也不能更新,由於更新也是上排它鎖 mysql> update test set level=2 where id=1; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 也不能上共享鎖 mysql> select * from test where level=1 lock in share mode; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction -------------------------------------------------------------------------------- 事務A能夠更新 mysql> update test set level=11 where id=1; Query OK, 1 row affected (0.08 sec) Rows matched: 1 Changed: 1 Warnings: 0 -------------------------------------------------------------------------------- 釋放排它鎖 mysql> commit; Query OK, 0 rows affected (0.00 sec) -------------------------------------------------------------------------------- 事務A釋放鎖,事務B就能夠加排它鎖了 mysql> select * from test where id=1 for update; +----+------+-------+-------+ | id | name | money | level | +----+------+-------+-------+ | 1 | tom | 100 | 1 | +----+------+-------+-------+ 1 row in set (0.00 sec)
對於update,insert,delete
語句會自動加排它鎖ci
1.3經過索引更新數據,也是上排他鎖,行鎖 SessionA SessionB mysql> set autocommit=0; mysql> set autocommit=0; Query OK, 0 rows affected (0.02 sec) Query OK, 0 rows affected (0.02 sec) mysql> select * from test; mysql> select * from test; -------------------------------------------------------------------------------- +----+-------+-------+-------+ +----+-------+-------+-------+ | id | name | money | level | | id | name | money | level | +----+-------+-------+-------+ +----+-------+-------+-------+ | 1 | tom | 100 | 1 | | 1 | tom | 100 | 1 | | 2 | jack | 200 | 2 | | 2 | jack | 200 | 2 | | 3 | lucas | 300 | 3 | | 3 | lucas | 300 | 3 | +----+-------+-------+-------+ +----+-------+-------+-------+ 3 rows in set (0.00 sec) 3 rows in set (0.00 sec) -------------------------------------------------------------------------------- 更新id=1的行,就給該行上了排它鎖,其餘事務 沒法更新該行 mysql> update test set level=11 where id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 -------------------------------------------------------------------------------- 事務B則不能更新id=1的行,會發生等待 mysql> update test set level=21 where id=1; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction MORE: 也不能上排它鎖 mysql> select *from test where id=1 for update; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 也不能上共享鎖 mysql> select * from test where level=1 lock in share mode; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction -------------------------------------------------------------------------------- 釋放排它鎖 mysql> commit; Query OK, 0 rows affected (0.00 sec) -------------------------------------------------------------------------------- 事務A釋放鎖,事務B就能夠加排它鎖了 mysql> select * from test where id=1 for update; +----+------+-------+-------+ | id | name | money | level | +----+------+-------+-------+ | 1 | tom | 100 | 11| +----+------+-------+-------+ 1 row in set (0.00 sec)
2.1髒讀
//2.1髒讀 SessionA SessionB mysql> set autocommit=0; mysql> set autocommit=0; Query OK, 0 rows affected (0.02 sec) Query OK, 0 rows affected (0.02 sec) set session transaction isolation set session transaction isolation level read uncommitted; level read uncommitted; Query OK, 0 rows affected (0.00 sec) Query OK, 0 rows affected (0.00 sec) mysql> select * from test; mysql> select * from test; -------------------------------------------------------------------------------- +----+-------+-------+-------+ +----+-------+-------+-------+ | id | name | money | level | | id | name | money | level | +----+-------+-------+-------+ +----+-------+-------+-------+ | 1 | tom | 100 | 1 | | 1 | tom | 100 | 1 | | 2 | jack | 200 | 2 | | 2 | jack | 200 | 2 | | 3 | lucas | 300 | 3 | | 3 | lucas | 300 | 3 | +----+-------+-------+-------+ +----+-------+-------+-------+ 3 rows in set (0.00 sec) 3 rows in set (0.00 sec) -------------------------------------------------------------------------------- mysql> update test set level=100 where id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 -------------------------------------------------------------------------------- //髒讀 mysql> select *from test where id=1; +----+------+-------+-------+ | id | name | money | level | +----+------+-------+-------+ | 1 | tom | 100 | 100 | +----+------+-------+-------+ 1 row in set (0.00 sec) -------------------------------------------------------------------------------- rollback; Query OK, 0 rows affected (0.01 sec) mysql> select *from test where id=1; +----+------+-------+-------+ | id | name | money | level | +----+------+-------+-------+ | 1 | tom | 100 | 1 | +----+------+-------+-------+ 1 row in set (0.00 sec)
2.2不可重複讀
//2.2不可重複讀 SessionA SessionB mysql> set autocommit=0; mysql> set autocommit=0; Query OK, 0 rows affected (0.02 sec) Query OK, 0 rows affected (0.02 sec) set session transaction isolation set session transaction isolation level read uncommitted; level read uncommitted; Query OK, 0 rows affected (0.00 sec) Query OK, 0 rows affected (0.00 sec) mysql> select * from test; mysql> select * from test; -------------------------------------------------------------------------------- +----+-------+-------+-------+ +----+-------+-------+-------+ | id | name | money | level | | id | name | money | level | +----+-------+-------+-------+ +----+-------+-------+-------+ | 1 | tom | 100 | 1 | | 1 | tom | 100 | 1 | | 2 | jack | 200 | 2 | | 2 | jack | 200 | 2 | | 3 | lucas | 300 | 3 | | 3 | lucas | 300 | 3 | +----+-------+-------+-------+ +----+-------+-------+-------+ 3 rows in set (0.00 sec) 3 rows in set (0.00 sec) -------------------------------------------------------------------------------- mysql> update test set level=100 where id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 -------------------------------------------------------------------------------- mysql> select *from test where id=1; +----+------+-------+-------+ | id | name | money | level | +----+------+-------+-------+ | 1 | tom | 100 | 100 | +----+------+-------+-------+ 1 row in set (0.00 sec) -------------------------------------------------------------------------------- mysql> update test set level=1000 where id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 -------------------------------------------------------------------------------- //不可重複讀 //讀三次,第一次是level是1,第二次是100,第三次是1000 mysql> select *from test where id=1; +----+------+-------+-------+ | id | name | money | level | +----+------+-------+-------+ | 1 | tom | 100 | 1000| +----+------+-------+-------+ 1 row in set (0.00 sec)
2.3幻讀
//2.3幻讀 SessionA SessionB mysql> set autocommit=0; mysql> set autocommit=0; Query OK, 0 rows affected (0.02 sec) Query OK, 0 rows affected (0.02 sec) set session transaction isolation set session transaction isolation level read uncommitted; level read uncommitted; Query OK, 0 rows affected (0.00 sec) Query OK, 0 rows affected (0.00 sec) mysql> select * from test; mysql> select * from test; -------------------------------------------------------------------------------- +----+-------+-------+-------+ +----+-------+-------+-------+ | id | name | money | level | | id | name | money | level | +----+-------+-------+-------+ +----+-------+-------+-------+ | 1 | tom | 100 | 1 | | 1 | tom | 100 | 1 | | 2 | jack | 200 | 2 | | 2 | jack | 200 | 2 | | 3 | lucas | 300 | 3 | | 3 | lucas | 300 | 3 | +----+-------+-------+-------+ +----+-------+-------+-------+ 3 rows in set (0.00 sec) 3 rows in set (0.00 sec) -------------------------------------------------------------------------------- mysql> update test set level=100 where id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 -------------------------------------------------------------------------------- mysql> select *from test where id=1; +----+------+-------+-------+ | id | name | money | level | +----+------+-------+-------+ | 1 | tom | 100 | 100 | +----+------+-------+-------+ 1 row in set (0.00 sec) -------------------------------------------------------------------------------- mysql> insert into test (name, money,level) VALUES ('tim',250,4); Query OK, 1 row affected (0.01 sec) -------------------------------------------------------------------------------- //幻讀 //讀兩次,第二次多了tim的數據 //若是是rr級別,須要使用當前讀select * from test lock in share mode;不然由於MVCC的緣故,是讀不到tim的 mysql> select * from test; +----+-------+-------+-------+ | id | name | money | level | +----+-------+-------+-------+ | 1 | tom | 100 | 1 | | 2 | jack | 200 | 2 | | 3 | lucas | 300 | 3 | | 4 | tim | 250 | 4 | +----+-------+-------+-------+ 4 row in set (0.00 sec)
MVCC使RR級別下,事務當前讀,來避免了讀狀況下的幻讀問題,但若是寫更新時候呢?在範圍更新的同時,往範圍內插入新數據,怎麼辦? 因而就有了間隙鎖,在更新某個區間數據時,將會鎖定這個區間的全部記錄。例如update XXX where id between 1 and 100, 就會鎖住id從1到100之間的全部的記錄。值得注意的是,在這個區間中假設某條記錄並不存在,該條記錄也會被鎖住,這時,若是另外一個事務往這個區間添加數據,就必須等待上一個事務釋放鎖資源。 使用間隙鎖有兩個目的,一是防止幻讀;二是知足其恢復和賦值的需求。
//間隙鎖(Net-Key鎖) 範圍間隙鎖,左開右閉區間 SessionA SessionB mysql> set autocommit=0; mysql> set autocommit=0; Query OK, 0 rows affected (0.02 sec) Query OK, 0 rows affected (0.02 sec) mysql> select * from test; mysql> select * from test; -------------------------------------------------------------------------------- +----+-------+-------+-------+ +----+-------+-------+-------+ | id | name | money | level | | id | name | money | level | +----+-------+-------+-------+ +----+-------+-------+-------+ | 1 | tom | 100 | 1 | | 1 | tom | 100 | 1 | | 2 | jack | 200 | 2 | | 2 | jack | 200 | 2 | | 3 | lucas | 300 | 3 | | 3 | lucas | 300 | 3 | +----+-------+-------+-------+ +----+-------+-------+-------+ 3 rows in set (0.00 sec) 3 rows in set (0.00 sec) -------------------------------------------------------------------------------- mysql> update test set level=0 where money between 0 and 200; Query OK, 2 rows affected (0.02 sec) Rows matched: 2 Changed: 2 Warnings: 0 理論上應該鎖定[0,300)這個區間 -------------------------------------------------------------------------------- 插入money=0等待 mysql> insert into test (name, money,level) VALUES ('tim',0,0); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 插入money=90等待 mysql> insert into test (name, money,level) VALUES ('tim',90,0); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 插入money=100等待 mysql> insert into test (name, money,level) VALUES ('tim',100,0); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 插入money=299等待 mysql> insert into test (name, money,level) VALUES ('tim',299,0); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 插入money=300 ok mysql> insert into test (name, money,level) VALUES ('tim',300,0); Query OK, 1 row affected (0.00 sec)
上小節是指定update某個區間,那若是說是隻update一個值呢?還會有間隙鎖麼?
//間隙鎖(Net-Key鎖) 單個間隙鎖,左開右閉區間 SessionA SessionB mysql> set autocommit=0; mysql> set autocommit=0; Query OK, 0 rows affected (0.02 sec) Query OK, 0 rows affected (0.02 sec) mysql> select * from test; mysql> select * from test; -------------------------------------------------------------------------------- +----+-------+-------+-------+ +----+-------+-------+-------+ | id | name | money | level | | id | name | money | level | +----+-------+-------+-------+ +----+-------+-------+-------+ | 1 | tom | 100 | 1 | | 1 | tom | 100 | 1 | | 2 | jack | 200 | 2 | | 2 | jack | 200 | 2 | | 3 | lucas | 300 | 3 | | 3 | lucas | 300 | 3 | +----+-------+-------+-------+ +----+-------+-------+-------+ 3 rows in set (0.00 sec) 3 rows in set (0.00 sec) -------------------------------------------------------------------------------- mysql> update test set level=0 where money = 200; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 理論上應該鎖定[0,300)這個區間 -------------------------------------------------------------------------------- 插入money=0 ok mysql> insert into test (name, money,level) VALUES ('tim',0,0); Query OK, 1 row affected (0.00 sec) 插入money=90 ok mysql> insert into test (name, money,level) VALUES ('tim',90,0); Query OK, 1 row affected (0.00 sec) 插入money=100等待 mysql> insert into test (name, money,level) VALUES ('tim',100,0); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 插入money=150等待 mysql> insert into test (name, money,level) VALUES ('tim',150,0); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 插入money=200等待 mysql> insert into test (name, money,level) VALUES ('tim',200,0); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 插入money=240等待 mysql> insert into test (name, money,level) VALUES ('tim',240,0); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 插入money=300 ok mysql> insert into test (name, money,level) VALUES ('tim',300,0); Query OK, 1 row affected (0.00 sec)
當不指定區間時,隱式的區間爲索引B+數先後兩個節點的值所肯定的區間,也是左開右閉,對於上述例子,就是[0,300)這個區間。