Mysql鎖機制介紹

前言

數據庫鎖定機制簡單來講就是數據庫爲了保證數據的一致性而使各類共享資源在被併發訪問訪問變得有序所設計的一種規則;對於任何一種數據庫來講都須要有相應的鎖定機制,Mysql也不例外。java

Mysql幾種鎖定機制類型

MySQL 各存儲引擎使用了三種類型(級別)的鎖定機制:行級鎖定,頁級鎖定和表級鎖定。mysql

1.行級鎖定

鎖定對象的顆粒度很小,只對當前行進行鎖定,因此發生鎖定資源爭用的機率也最小,可以給予應用程序儘量大的併發處理能力;弊端就是獲取鎖釋放鎖更加頻繁,系統消耗更大,同時行級鎖定也最容易發生死鎖;
行級鎖定的主要是Innodb存儲引擎和NDB Cluster存儲引擎;sql

2.頁級鎖定

鎖定顆粒度介於行級鎖定與表級鎖之間,每頁有多行數據,併發處理能力以及獲取鎖定所須要的資源開銷在二者之間;
頁級鎖定主要是BerkeleyDB 存儲引擎;數據庫

3.表級鎖定

一次會將整張表鎖定,該鎖定機制最大的特色是實現邏輯很是簡單,帶來的系統負面影響最小,並且能夠避免死鎖問題;弊端就是鎖定資源爭用的機率最高,併發處理能力最低;
使用表級鎖定的主要是MyISAM,Memory,CSV等一些非事務性存儲引擎。session

本文重點介紹Innodb存儲引擎使用的行級鎖定;併發

兩段鎖協議(2PL)

兩段鎖協議規定全部的事務應遵照的規則:
1.在對任何數據進行讀、寫操做以前,首先要申請並得到對該數據的封鎖;
2.在釋放一個封鎖以後,事務再也不申請和得到其它任何封鎖;性能

即事務的執行分爲兩個階段:
第一階段是得到封鎖的階段,稱爲擴展階段;第二階段是釋放封鎖的階段,稱爲收縮階段;測試

begin;
insert ...   加鎖1
update ...   加鎖2
commit;      事務提交時,釋放鎖1,鎖2

若是在加鎖2的時候,加鎖不成功,則進入等待狀態,直到加鎖成功才繼續執行;
若是有另一個事務獲取鎖的時候順序恰好相反,是有可能致使死鎖的;爲此有了一次性封鎖法,要求事務必須一次性將全部要使用的數據所有加鎖,不然就不能繼續執行;ui

定理:若全部事務均遵照兩段鎖協議,則這些事務的全部交叉調度都是可串行化的(串行化很重要,尤爲是在數據恢復和備份的時候);設計

行級鎖定(悲觀鎖)

1.共享鎖和排他鎖

Innodb的行級鎖定一樣分爲兩種類型:共享鎖和排他鎖;
共享鎖:當一個事務得到共享鎖以後,它只能夠進行讀操做,因此共享鎖也叫讀鎖,多個事務能夠同時得到某一行數據的共享鎖;
排他鎖:而當一個事務得到一行數據的排他鎖時,就能夠對該行數據進行讀和寫操做,因此排他鎖也叫寫鎖,排他鎖與共享鎖和其餘的排他鎖不兼容;

既然數據庫提供了共享鎖和排他鎖,那具體用在什麼地方:
1.1在數據庫操做中,爲了有效保證併發讀取數據的正確性,提出的事務隔離級別,隔離級別就使用了鎖機制;
1.2提供了相關的SQL,能夠方便的在程序中使用;

2.事務隔離級別和鎖的關係

數據庫隔離級別:未提交讀(Read uncommitted),已提交讀(Read committed),可重複讀(Repeatable read)和可串行化(Serializable);
未提交讀(Read uncommitted):可能讀取到其餘會話中未提交事務修改的數據,會出現髒讀(Dirty Read)
已提交讀(Read committed):只能讀取到已經提交的數據,會出現不可重複讀(NonRepeatable Read)
可重複讀(Repeatable read):InnoDB默認級別,不會出現不可重複讀(NonRepeatable Read),可是會出現幻讀(Phantom Read);
可串行化(Serializable):強制事務排序,使之不可能相互衝突,從而解決幻讀問題,使用表級共享鎖,讀寫相互都會阻塞;

經常使用的2種隔離級別是:已提交讀(Read committed)可重複讀(Repeatable read)

3.已提交讀

3.1準備測試表

CREATE TABLE `test_lock` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `type` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8
 
mysql> insert into test_lock values(null,'zhaohui',1);
mysql> insert into test_lock values(null,'zhaohui2',2);

3.2查看和設置隔離級別

mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set
 
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected
 
mysql> SELECT @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+

3.3模擬多個事務交叉執行

Session1執行查詢

mysql> begin;
Query OK, 0 rows affected
mysql> select * from test_lock where id=1;
+----+---------+------+
| id | name    | type |
+----+---------+------+
|  1 | zhaohui |    1 |
+----+---------+------+
1 row in set

Session2更新數據

mysql> begin;
Query OK, 0 rows affected
 
mysql> update test_lock set name='zhaohui_new' where id=1;
  
Query OK, 1 row affected
Rows matched: 1  Changed: 1  Warnings: 0
mysql> commit;
Query OK, 0 rows affected

Session1執行查詢

mysql> select * from test_lock where id=1;
+----+-------------+------+
| id | name        | type |
+----+-------------+------+
|  1 | zhaohui_new |    1 |
+----+-------------+------+
1 row in set
 
mysql> commit;
Query OK, 0 rows affected

Session1中出現了不可重複讀(NonRepeatable Read),也就是在查詢的時候沒有鎖住相關的數據,致使出現了不可重複讀,可是寫入、修改和刪除數據仍是加鎖了,以下所示:

Session1更新數據

mysql> begin;
Query OK, 0 rows affected
 
mysql> update test_lock set name='zhaohui_new2' where id=1;
Query OK, 1 row affected
Rows matched: 1  Changed: 1  Warnings: 0

Session2更新數據

mysql> begin;
Query OK, 0 rows affected
 
mysql> update test_lock set name='zhaohui_new3' where id=1;
1205 - Lock wait timeout exceeded; try restarting transaction

Session2更新在更新同一條數據的時候超時了,在更新數據的時候添加了排他鎖;

4.可重複讀

4.1查看和設置隔離級別

mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected
 
mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set

4.2模擬多個事務交叉執行

Session1執行查詢

mysql> begin;
Query OK, 0 rows affected
 
mysql> select * from test_lock where type=2;
+----+----------+------+
| id | name     | type |
+----+----------+------+
|  2 | zhaohui2 |    2 |
+----+----------+------+
1 row in set

Session2更新數據

mysql> begin;
Query OK, 0 rows affected
 
mysql> update test_lock set name='zhaohui2_new' where type=2;
Query OK, 1 row affected
Rows matched: 1  Changed: 1  Warnings: 0
 
mysql> commit;
Query OK, 0 rows affected

Session1執行查詢

mysql> select * from test_lock where type=2;
+----+----------+------+
| id | name     | type |
+----+----------+------+
|  2 | zhaohui2 |    2 |
+----+----------+------+
1 row in set

能夠發現2次查詢的數據結果是同樣的,實現了可重複讀(Repeatable read),再來看一下是否有幻讀(Phantom Read)的問題;

Session3插入數據

mysql> begin;
Query OK, 0 rows affected
 
mysql> insert into test_lock values(null,'zhaohui3',2);
Query OK, 1 row affected
 
mysql> commit;
Query OK, 0 rows affected

Session1執行查詢

mysql> select * from test_lock where type=2;
+----+----------+------+
| id | name     | type |
+----+----------+------+
|  2 | zhaohui2 |    2 |
+----+----------+------+
1 row in set

能夠發現可重複讀(Repeatable read)隔離級別下,也不會出現幻讀的現象;

分析一下緣由:如何經過悲觀鎖的方式去實現可重複讀和不出現幻讀的現象,對讀取的數據加共享鎖,對一樣的數據執行更新操做就只能等待,這樣就能夠保證可重複讀,可是對於不出現幻讀的現象沒法經過鎖定行數據來解決;
最終看到的現象是沒有幻讀的問題,同時若是對讀取的數據加共享鎖,更新相同數據應該會等待,上面的實例中並無出現等待,因此mysql內部應該還有其餘鎖機制,後面介紹;

5.悲觀鎖SQL使用

5.1共享鎖使用(lock in share mode)

Session1查詢數據

mysql> begin;
Query OK, 0 rows affected
 
mysql> select * from test_lock where type=2 lock in share mode;
+----+--------------+------+
| id | name         | type |
+----+--------------+------+
|  2 | zhaohui2_new |    2 |
|  3 | zhaohui3     |    2 |
+----+--------------+------+
2 rows in set

Session2查詢數據

mysql> begin;
Query OK, 0 rows affected
 
mysql> select * from test_lock where type=2 lock in share mode;
+----+--------------+------+
| id | name         | type |
+----+--------------+------+
|  2 | zhaohui2_new |    2 |
|  3 | zhaohui3     |    2 |
+----+--------------+------+
2 rows in set

Session3更新數據

mysql> begin;
Query OK, 0 rows affected
 
mysql> update test_lock set name='zhaohui3_new' where id=3;
1205 - Lock wait timeout exceeded; try restarting transaction

Session1和Session2使用了共享鎖,因此能夠存在多個,並不衝突,可是Session3更新操做須要加上排他鎖,和共享鎖不能同時存在;

5.2排他鎖使用(for update)

Session1查詢數據

mysql> begin;
Query OK, 0 rows affected
 
mysql> select * from test_lock where type=2 for update;
+----+--------------+------+
| id | name         | type |
+----+--------------+------+
|  2 | zhaohui2_new |    2 |
|  3 | zhaohui3     |    2 |
+----+--------------+------+
2 rows in set

Session2查詢數據

mysql> begin;
Query OK, 0 rows affected
 
mysql> select * from test_lock where type=2 for update;
Empty set

Session3更新數據

mysql> begin;
Query OK, 0 rows affected
 
mysql> update test_lock set name='zhaohui3_new' where id=3;
1205 - Lock wait timeout exceeded; try restarting transaction

排他鎖只能有一個同時存在,全部Session2和Session3都將等等超時;

多版本併發控制MVCC

從上面的例子中分析出,mysql內部使用了其餘的併發控制機制,這種機制就是多版本併發控制(MVCC);
爲何要引入此機制,首先經過悲觀鎖來處理讀請求是很耗性能的,其次數據庫的事務大都是隻讀的,讀請求是寫請求的不少倍,最後若是沒有併發控制機制,最壞的狀況也是讀請求讀到了已經寫入的數據,這對不少應用徹底是能夠接受的;
多版本併發控制(Multiversion Concurrency Control):每個寫操做都會建立一個新版本的數據,讀操做會從有限多個版本的數據中挑選一個最合適的結果直接返回;讀寫操做之間的衝突就再也不須要被關注,而管理和快速挑選數據的版本就成了MVCC須要解決的主要問題。

再來看一下可重複讀(Repeatable read)現象,經過MVCC機制讀操做只讀該事務開始前的數據庫的快照(snapshot), 這樣在讀操做不用阻塞寫操做,寫操做不用阻塞讀操做的同時,避免了髒讀和不可重複讀;

固然並非說悲觀鎖就沒有用了,在數據更新的時候數據庫默認仍是使用悲觀鎖的,因此MVCC是能夠整合起來一塊兒使用的(MVCC+2PL),用來解決讀-寫衝突的無鎖併發控制;
MVCC使用快照讀的方式,解決了不可重複讀和幻讀的問題,如上面的實例所示:select查詢的一直是快照信息,不須要添加任何鎖;
以上實例中使用的select方式把它稱爲快照讀(snapshot read),其實事務的隔離級別的讀還有另外一層含義:讀取數據庫當前版本數據–當前讀(current read)

當前讀和Gap鎖

區別普通的select查詢,當前讀對應的sql包括:

select ...for update,
select ...lock in share mode,
insert,update,delete;

以上sql自己會加悲觀鎖,因此不存在不可重複讀的問題,剩下的就是幻讀的問題;
Session1執行當前讀

mysql> select * from test_lock where type=2 for update;
+----+----------------+------+
| id | name           | type |
+----+----------------+------+
|  2 | zhaohui2_new   |    2 |
|  3 | zhaohui3_new_1 |    2 |
+----+----------------+------+
2 rows in set

Session2執行插入

mysql> begin;
Query OK, 0 rows affected
 
mysql> insert into test_lock values(null,'zhaohui_001',1);
1205 - Lock wait timeout exceeded; try restarting transaction

爲何明明鎖住的是type=2的數據,當插入type=1也會鎖等待,由於InnoDB對於行的查詢都是採用了Next-Key鎖,鎖定的不是單個值,而是一個範圍(GAP);
若是當前type類型包括:1,2,4,6,8,10鎖住type=2,那麼type=1,2,3會被鎖住,後面的不會,鎖住的是一個區間;這樣也就保證了當前讀也不會出現幻讀的現象;
注:type字段添加了索引,若是沒有添加索引,gap鎖會鎖住整張表;

樂觀鎖

樂觀鎖是一種思想,認爲事務間爭用沒有那麼多,和悲觀鎖是相對的,樂觀鎖在java的併發包中大量的使用;通常採用如下方式:使用版本號(version)機制來實現,版本號就是爲數據添加一個版本標誌,通常在表中添加一個version字段;當讀取數據的時候把version也取出來,而後version+1,更新數據庫的時候對比第一次取出來的version和數據庫裏面的version是否一致,若是一致則更新成功,不然失敗進入重試,具體使用大體以下:

begin;
select id,name,version from test_lock where id=1;
....
update test_lock set name='xxx',version=version+1 where id=1 and version=${version};
commit;

先查詢後更新,須要保證原子性,要麼使用悲觀鎖的方式,對整個事務加鎖;要麼使用樂觀鎖的方式,若是在讀多寫少的系統中,樂觀鎖性能更好;

總結

本文首先從Mysql的悲觀鎖出發,而後介紹了悲觀鎖和事務隔離級別之間的關係,並分析爲何沒有使用悲觀鎖來實現隔離級別;而後從問題出發分別介紹了MVCC和Gap鎖是如何解決了不可重複讀的問題和幻讀的問題;最後介紹了樂觀鎖常常被用在讀數據遠大於寫數據的系統中。

相關文章
相關標籤/搜索