數據庫鎖定機制簡單來講就是數據庫爲了保證數據的一致性而使各類共享資源在被併發訪問變得有序所設計的一種規則;對於任何一種數據庫來講都須要有相應的鎖定機制,Mysql也不例外。mysql
通常來講,事務是必須知足4個條件(ACID):原子性(Atomicity,或稱不可分割性)、一致性(Consistency)、隔離性(Isolation,又稱獨立性)、持久性(Durability)。sql
原子性:一個事務(transaction)中的全部操做,要麼所有完成,要麼所有不完成,不會結束在中間某個環節。事務在執行過程當中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務歷來沒有執行過同樣。數據庫
一致性:在事務開始以前和事務結束之後,數據庫的完整性沒有被破壞。這表示寫入的資料必須徹底符合全部的預設規則,這包含資料的精確度、串聯性以及後續數據庫能夠自發性地完成預約的工做。session
隔離性:數據庫容許多個併發事務同時對其數據進行讀寫和修改的能力,隔離性能夠防止多個事務併發執行時因爲交叉執行而致使數據的不一致。事務隔離分爲不一樣級別,包括讀未提交(Read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和串行化(Serializable)。架構
持久性:事務處理結束後,對數據的修改就是永久的,即使系統故障也不會丟失。併發
用 BEGIN, ROLLBACK, COMMIT來實現
1)、BEGIN 開始一個事務
2)、ROLLBACK 事務回滾
3)、COMMIT 事務確認性能
直接用 SET 來改變 MySQL 的自動提交模式:
1)、SET AUTOCOMMIT=0 禁止自動提交
2)、SET AUTOCOMMIT=1 開啓自動提交測試
mysql事務隔離級別分爲以下4種:ui
(1)讀不提交(Read Uncommited,RU)spa
這種隔離級別下,事務間徹底不隔離,會產生髒讀,能夠讀取未提交的記錄,實際狀況下不會使用。
(2)讀提交(Read commited,RC)
僅能讀取到已提交的記錄,這種隔離級別下,會存在幻讀現象,所謂幻讀是指在同一個事務中,屢次執行同一個查詢,返回的記錄不徹底相同的現象。幻讀產生的根本緣由是,在RC隔離級別下,每條語句都會讀取已提交事務的更新,若兩次查詢之間有其餘事務提交,則會致使兩次查詢結果不一致。雖然如此,讀提交隔離級別在生產環境中使用很普遍。
(3)可重複讀(Repeatable Read, RR)
可重複讀隔離級別解決了不可重複讀的問題,但依然沒有解決幻讀的問題。那麼不可重複讀與幻讀有什麼區別呢?不可重複讀重點在修改,即讀取過的數據,兩次讀的值不同;而幻讀則側重於記錄數目變化【插入和刪除】。通常教科書上告訴咱們只有到串行化隔離級別才解決幻讀問題,但mysql的innodb比較特殊,RR即解決了幻讀問題,主要經過GAP鎖實現。另外,不是全部的數據庫都實現了該隔離級別,後面會簡單介紹下mysql是如何實現可重複讀隔離級別的。
(4)串行化(Serializable)
在串行化隔離模式下,消除了髒讀,幻象,但事務併發度急劇降低,事務的隔離級別與事務的併發度成反比,隔離級別越高,事務的併發度越低。實際生產環境下,dba會在併發和知足業務需求之間做權衡,選擇合適的隔離級別。
MySQL 各存儲引擎使用了三種類型(級別)的鎖定機制:行級鎖定,頁級鎖定和表級鎖定。
鎖定對象的顆粒度很小,只對當前行進行鎖定,因此發生鎖定資源爭用的機率也最小,可以給予應用程序儘量大的併發處理能力;弊端就是獲取鎖釋放鎖更加頻繁,系統消耗更大,同時行級鎖定也最容易發生死鎖;
行級鎖定的主要是Innodb存儲引擎和NDB Cluster存儲引擎;
一次會將整張表鎖定,該鎖定機制最大的特色是實現邏輯很是簡單,帶來的系統負面影響最小,並且能夠避免死鎖問題;弊端就是鎖定資源爭用的機率最高,併發處理能力最低;
使用表級鎖定的主要是MyISAM,Memory,CSV等一些非事務性存儲引擎。
鎖定顆粒度介於行級鎖定與表級鎖之間,每頁有多行數據,併發處理能力以及獲取鎖定所須要的資源開銷在二者之間;
頁級鎖定主要是BerkeleyDB 存儲引擎;
兩段鎖協議規定全部的事務應遵照的規則:
1.在對任何數據進行讀、寫操做以前,首先要申請並得到對該數據的封鎖;
2.在釋放一個封鎖以後,事務再也不申請和得到其它任何封鎖;
即事務的執行分爲兩個階段:
第一階段是得到封鎖的階段,稱爲擴展階段;第二階段是釋放封鎖的階段,稱爲收縮階段;
1begin;insert ... 加鎖1 update ... 加鎖2 commit; 事務提交時,釋放鎖1,鎖2
若是在加鎖2的時候,加鎖不成功,則進入等待狀態,直到加鎖成功才繼續執行;
若是有另一個事務獲取鎖的時候順序恰好相反,是有可能致使死鎖的;爲此有了一次性封鎖法,要求事務必須一次性將全部要使用的數據所有加鎖,不然就不能繼續執行;
Innodb 實現了兩種類型的行鎖:
共享鎖(S):容許一個事務去讀一行,阻止其餘事務得到相同數據集的排他鎖。
排他鎖(X):容許得到排他鎖的事務更新數據,阻止其餘事務取得相同數據集的共享讀鎖和排他寫鎖。
另外,爲了容許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖。
意向共享鎖(IS):事務打算給數據行加行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖。
意向排他鎖(IX):事務打算給數據行加行排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的IX鎖。
若是一個事務請求的鎖模式與當前的鎖兼容,innodb 就將請求的鎖授予該事務;反之,若是二者不兼容,該事務就要等待鎖釋放。
意向鎖是 innodb 自動加的,不須要用戶干預。對於 update、delete 和 insert 語句,innodb 會自動給涉及數據集加排它鎖(X);對於普通 select 語句,innodb 不會加任何鎖。
事務能夠經過如下語句顯式給記錄集加共享鎖或排它鎖。
共享鎖(S):select * from table_name where … lock in share mode.
排它鎖(X): select * from table_name where … for update.
用 select… in share mode 得到共享鎖,主要用在須要數據依存關係時來確認某行記錄是否存在,並確保沒有人對這個記錄進行 update 或者 delete 操做。可是若是當前事務也須要對該記錄進行更新操做,則有可能形成死鎖,對於鎖定行記錄後須要進行更新操做的應用,應該使用 select… for update 方式得到排他鎖。
數據庫隔離級別:未提交讀(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);mysql的默認隔離級別是可重複讀。
InnoDB行鎖是經過給索引上的索引項加鎖來實現的,這一點MySQL與Oracle不一樣,後者是經過在數據塊中對相應數據行加鎖來實現的。InnoDB這種行鎖實現特色意味着:只有經過索引條件檢索數據,InnoDB才使用行級鎖,不然,InnoDB將使用表鎖!在實際應用中,要特別注意InnoDB行鎖的這一特性,否則的話,可能致使大量的鎖衝突,從而影響併發性能。
因爲MySQL的行鎖是針對索引加的鎖,不是針對記錄加的鎖,因此雖然是訪問不一樣行的記錄,可是若是是使用相同的索引鍵,是會出現鎖衝突的
當表有多個索引的時候,不一樣的事務可使用不一樣的索引鎖定不一樣的行,另外,不管是使用主鍵索引、惟一索引或普通索引,InnoDB都會使用行鎖來對數據加鎖。若是不一樣的索引碰巧都落到了同一個行上,那麼一樣會阻塞。
即使在條件中使用了索引字段,可是否使用索引來檢索數據是由MySQL經過判斷不一樣執行計劃的代價來決定的,若是MySQL認爲全表掃描效率更高,好比對一些很小的表,它就不會使用索引,這種狀況下InnoDB將使用表鎖,而不是行鎖。所以,在分析鎖衝突時,別忘了檢查SQL的執行計劃,以確認是否真正使用了索引。
當咱們用範圍條件而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的索引項加鎖;對於鍵值在條件範圍內但並不存在的記錄,叫作「間隙(GAP)」,InnoDB也會對這個「間隙」加鎖,這種鎖機制就是所謂的間隙鎖(Next-Key鎖)。
舉例來講,假如emp表中只有101條記錄,其empid的值分別是 1,2,…,100,101,下面的SQL:
Select * from emp where empid > 100 for update;
是一個範圍條件的檢索,InnoDB不只會對符合條件的empid值爲101的記錄加鎖,也會對empid大於101(這些記錄並不存在)的「間隙」加鎖。
InnoDB使用間隙鎖的目的,一方面是爲了防止幻讀,以知足相關隔離級別的要求,對於上面的例子,要是不使用間隙鎖,若是其餘事務插入了empid大於100的任何記錄,那麼本事務若是再次執行上述語句,就會發生幻讀;另一方面,是爲了知足其恢復和複製的須要
還要特別說明的是,InnoDB除了經過範圍條件加鎖時使用間隙鎖外,若是使用相等條件請求給一個不存在的記錄加鎖,InnoDB也會使用間隙鎖!
MySQL的恢復機制是經過BINLOG記錄來執行IUD操做來同步Slave的,這就要求:在一個事務未提交前,其餘併發事務不能插入知足其鎖定條件的任何記錄,也就是不容許出現幻讀,這已經超過了ISO/ANSI SQL92「可重複讀」隔離級別的要求,其實是要求事務要串行化。這也是許多狀況下,InnoDB要用到間隙鎖的緣由,好比在用範圍條件更新記錄時,不管在Read Commited或是Repeatable Read隔離級別下,InnoDB都要使用間隙鎖,但這並非隔離級別要求的。
INSERT…SELECT…和 CREATE TABLE…SELECT…語句,可能會阻止對源表的併發更新,形成對源表鎖的等待。若是查詢比較複雜的話,會形成嚴重的性能問題,咱們在應用中應儘可能避免使用。實際上,MySQL將這種SQL叫做不肯定(non-deterministic)的SQL,不推薦使用。
接下來咱們測試下mysql的已提交讀,咱們先看下mysql的隔離級別:
1mysql> SELECT @@tx_isolation; 2+----------------+ 3| @@tx_isolation | 4+----------------+ 5| REPEATABLE-READ | 6+----------------+
設置隔離級別爲已提交讀
1set session transaction isolation level read committed; 2Query OK, 0 rows affected (0.02 sec) 3 4mysql> SELECT @@tx_isolation; 5+----------------+ 6| @@tx_isolation | 7+----------------+ 8| READ-COMMITTED | 9+----------------+ 101 row in set (0.01 sec)
準備測試數據:
1CREATE TABLE `test_lock` ( 2 `id` int(11) NOT NULL AUTO_INCREMENT, 3 `name` varchar(255) NOT NULL, 4 `type` int(11) DEFAULT NULL, 5 PRIMARY KEY (`id`) 6) ENGINE=InnoDB DEFAULT CHARSET=utf8 7 8mysql> insert into test_lock values(null,'zhaohui',1); 9mysql> insert into test_lock values(null,'zhaohui2',2);
模擬多個事務交叉執行:
Session1執行查詢
1mysql> begin 2 -> ; 3Query OK, 0 rows affected (0.02 sec) 4 5mysql> select * from test_lock where id=1; 6+----+---------+------+ 7| id | name | type | 8+----+---------+------+ 9| 1 | zhaohui | 1 | 10+----+---------+------+ 111 row in set (0.05 sec)
Session2更新數據
1 begin; 2Query OK, 0 rows affected (0.08 sec) 3 4mysql> update test_lock set name='zhaohui_new' where id=1; 5Query OK, 1 row affected (0.02 sec) 6Rows matched: 1 Changed: 1 Warnings: 0 7 8mysql> commit; 9Query OK, 0 rows affected (0.03 sec) 10
Session1執行查詢
1select * from test_lock where id=1; 2+----+-------------+------+ 3| id | name | type | 4+----+-------------+------+ 5| 1 | zhaohui_new | 1 | 6+----+-------------+------+ 71 row in set (0.04 sec)
Session1中出現了不可重複讀(NonRepeatable Read),也就是在查詢的時候沒有鎖住相關的數據,致使出現了不可重複讀,可是寫入、修改和刪除數據仍是加鎖了,以下所示:
Session1更新數據
1mysql> begin; 2Query OK, 0 rows affected (0.01 sec) 3 4mysql> update test_lock set name='zhaohui_new2' where id=1; 5Query OK, 1 row affected (0.01 sec) 6Rows matched: 1 Changed: 1 Warnings: 0
Session2更新數據
1mysql> begin 2 -> ; 3Query OK, 0 rows affected (0.05 sec) 4 5mysql> update test_lock set name='zhaohui_new3' where id=1; 6ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
Session2更新在更新同一條數據的時候超時了,在更新數據的時候添加了排他鎖;
查看和設置隔離級別
1mysql> set session transaction isolation level repeatable read; 2Query OK, 0 rows affected (0.07 sec) 3 4mysql> SELECT @@tx_isolation; 5+-----------------+ 6| @@tx_isolation | 7+-----------------+ 8| REPEATABLE-READ | 9+-----------------+ 101 row in set (0.05 sec)
模擬多個事務交叉執行:
Session1執行查詢
1mysql> begin; 2Query OK, 0 rows affected (0.03 sec) 3 4mysql> select * from test_lock where type=2; 5+----+----------+------+ 6| id | name | type | 7+----+----------+------+ 8| 2 | zhaohui2 | 2 | 9+----+----------+------+ 101 row in set (0.02 sec)
Session2更新數據
1mysql> begin; 2Query OK, 0 rows affected (0.05 sec) 3 4mysql> update test_lock set name='zhaohui2_new' where type=2; 5Query OK, 1 row affected (0.03 sec) 6Rows matched: 1 Changed: 1 Warnings: 0 7 8mysql> commit; 9Query OK, 0 rows affected (0.06 sec)
Session1執行查詢
1mysql> select * from test_lock where type=2; 2+----+----------+------+ 3| id | name | type | 4+----+----------+------+ 5| 2 | zhaohui2 | 2 | 6+----+----------+------+ 71 row in set (0.03 sec)
能夠發現2次查詢的數據結果是同樣的,實現了可重複讀(Repeatable read),再來看一下是否有幻讀的問題;
Session3插入數據
1mysql> begin; 2Query OK, 0 rows affected (0.01 sec) 3 4mysql> insert into test_lock(name,type) values('zhaohui3',2); 5Query OK, 1 row affected (0.02 sec) 6 7mysql> commit; 8Query OK, 0 rows affected (0.02 sec)
Session1執行查詢
1mysql> select * from test_lock where type=2; 2+----+----------+------+ 3| id | name | type | 4+----+----------+------+ 5| 2 | zhaohui2 | 2 | 6+----+----------+------+ 71 row in set (0.01 sec)
這裏發現一個奇怪的現象,按理說應該會出現兩條記錄,可是mysql在重複讀的隔離級別下沒有出現幻讀的狀況;
咱們知道innodb默認是行鎖級別,如何經過悲觀鎖的方式去實現可重複讀和不出現幻讀的現象,對讀取的數據加共享鎖,對一樣的數據執行更新操做就只能等待,這樣就能夠保證可重複讀,可是對於不出現幻讀的現象沒法經過鎖定行數據來解決;
最終看到的現象是沒有幻讀的問題,同時若是對讀取的數據加共享鎖,更新相同數據應該會等待,上面的實例中並無出現等待,因此mysql內部應該還有其餘鎖機制--MVCC機制;
Session1查詢數據
1mysql> begin; 2Query OK, 0 rows affected (0.03 sec) 3 4mysql> select * from test_lock where type=2 lock in share mode; 5+----+--------------+------+ 6| id | name | type | 7+----+--------------+------+ 8| 2 | zhaohui2_new | 2 | 9| 3 | zhaohui3 | 2 | 10+----+--------------+------+ 112 rows in set (0.15 sec)
Session2查詢數據
1mysql> begin; 2Query OK, 0 rows affected (0.01 sec) 3 4mysql> select * from test_lock where type=2 lock in share mode; 5+----+--------------+------+ 6| id | name | type | 7+----+--------------+------+ 8| 2 | zhaohui2_new | 2 | 9| 3 | zhaohui3 | 2 | 10+----+--------------+------+ 112 rows in set (0.05 sec)
Session3 更新數據
1mysql> begin; 2Query OK, 0 rows affected (0.02 sec) 3 4mysql> update test_lock set name='zhaohui3_new' where id=3; 5ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
Session1和Session2使用了共享鎖,因此能夠存在多個,並不衝突,可是Session3更新操做須要加上排他鎖,和共享鎖不能同時存在;
Session1查詢數據
1mysql> begin; 2Query OK, 0 rows affected (0.07 sec) 3 4mysql> select * from test_lock where type=2 for update; 5+----+--------------+------+ 6| id | name | type | 7+----+--------------+------+ 8| 2 | zhaohui2_new | 2 | 9| 3 | zhaohui3 | 2 | 10+----+--------------+------+ 112 rows in set (15.02 sec)
Session2查詢數據
1mysql>begin; 2Query OK, 0 rows affected (0.07 sec) 3 4mysql> select * from test_lock where type=2 for update; 5ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
排他鎖只能有一個同時存在,若是session1獲取了鎖,那麼session2將會等待超時。
MVCC (Multiversion Concurrency Control),即多版本併發控制技術,它使得大部分支持行鎖的事務引擎,再也不單純的使用行鎖來進行數據庫的併發控制,取而代之的是把數據庫的行鎖與行的多個版本結合起來,只須要很小的開銷,就能夠實現非鎖定讀,從而大大提升數據庫系統的併發性能
MVCC是經過保存數據在某個時間點的快照來實現的. 不一樣存儲引擎的MVCC. 不一樣存儲引擎的MVCC實現是不一樣的,典型的有樂觀併發控制和悲觀併發控制.
InnoDB的MVCC,是經過在每行記錄後面保存兩個隱藏的列來實現的,這兩個列,分別保存了這個行的建立時間,一個保存的是行的刪除時間。這裏存儲的並非實際的時間值,而是系統版本號(能夠理解爲事務的ID),沒開始一個新的事務,系統版本號就會自動遞增,事務開始時刻的系統版本號會做爲事務的ID.
innodb存儲的最基本row中包含一些額外的存儲信息 DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE BIT
6字節的DATA_TRX_ID 標記了最新更新這條行記錄的transaction id,每處理一個事務,其值自動+1
7字節的DATA_ROLL_PTR 指向當前記錄項的rollback segment的undo log記錄,找以前版本的數據就是經過這個指針
6字節的DB_ROW_ID,當由innodb自動產生彙集索引時,彙集索引包括這個DB_ROW_ID的值,不然彙集索引中不包括這個值.,這個用於索引當中
DELETE BIT位用於標識該記錄是否被刪除,這裏的不是真正的刪除數據,而是標誌出來的刪除。真正意義的刪除是在commit的時候
具體的執行過程
begin->用排他鎖鎖定該行->記錄redo log->記錄undo log->修改當前行的值,寫事務編號,回滾指針指向undo log中的修改前的行
上述過程確切地說是描述了UPDATE的事務過程,其實undo log分insert和update undo log,由於insert時,原始的數據並不存在,因此回滾時把insert undo log丟棄便可,而update undo log則必須遵照上述過程
下面分別以select、delete、 insert、 update語句來講明:
Innodb檢查每行數據,確保他們符合兩個標準:
一、InnoDB只查找版本早於當前事務版本的數據行(也就是數據行的版本必須小於等於事務的版本),這確保當前事務讀取的行都是事務以前已經存在的,或者是由當前事務建立或修改的行
二、行的刪除操做的版本必定是未定義的或者大於當前事務的版本號,肯定了當前事務開始以前,行沒有被刪除
符合了以上兩點則返回查詢結果。
InnoDB爲每一個新增行記錄當前系統版本號做爲建立ID。
InnoDB爲每一個刪除行的記錄當前系統版本號做爲行的刪除ID。
InnoDB複製了一行。這個新行的版本號使用了系統版本號。它也把系統版本號做爲了刪除行的版本。
這裏簡單作下總結:
insert操做時 「建立時間」=DB_ROW_ID,這時,「刪除時間 」是未定義的;
update時,複製新增行的「建立時間」=DB_ROW_ID,刪除時間未定義,舊數據行「建立時間」不變,刪除時間=該事務的DB_ROW_ID;
delete操做,相應數據行的「建立時間」不變,刪除時間=該事務的DB_ROW_ID;
select操做對二者都不修改,只讀相應的數據
本公衆號團隊成員由餓了麼、阿里、螞蟻金服等同事組成,關注架構師之巔,能夠了解最前沿的技術。