鎖是計算機協調多個進程或者多個線程之間併發訪問同一資源的機制。在數據庫系統中,除了傳統的計算機資源(CPU、RAM、I/O)的爭用之外,數據也是一種供許多用戶共享的資源。如何保證數據併發訪問的一致性和有效性是全部數據庫系統須要考慮的問題。鎖衝突也是影響數據庫併發訪問性能的一個重要因素。從這個角度來說,鎖對於數據庫尤爲重要,也更加複雜。
對於mysql來講,不一樣的引擎鎖的實現方式不同,因此須要根據不一樣數據庫引擎來進行討論。mysql
MyISAM 引擎的表鎖有2種模式,讀鎖和寫鎖。sql
MyISAM在執行查詢語句(SELECT)前,會自動給涉及的全部表加讀鎖,在執行更新操做(UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,這個過程並不須要用戶干預,所以用戶通常不須要直接用LOCK TABLE命令給MyISAM表顯式加鎖。數據庫
MyISAM存儲引擎的讀和寫鎖是互斥,讀操做是串行的。那麼,一個進程請求某個MyISAM表的讀鎖,同時另外一個進程也請求同一表的寫鎖,MySQL如何處理呢?答案是寫進程先得到鎖。不只如此,即便讀進程先請求先到鎖等待隊列,寫請求後到,寫鎖也會插到讀請求以前!這是由於MySQL認爲寫請求通常比讀請求重要。這也正是MyISAM表不太適合於有大量更新操做和查詢操做應用的緣由,由於,大量的更新操做會形成查詢操做很難得到讀鎖,從而可能永遠阻塞。這種狀況有時可能會變得很是糟糕!幸虧咱們能夠經過一些設置來調節MyISAM的調度行爲。經過指定啓動參數low-priority-updates,使MyISAM引擎默認給予讀請求以優先的權利。session
InnoDB與MyISAM的最大不一樣有兩點:一是支持事務(TRANSACTION);二是採用了行級鎖。行級鎖和表級鎖原本就有許多不一樣之處,另外,事務的引入也帶來了一些新問題。數據結構
事務是由一組SQL語句組成的邏輯處理單元,事務具備4屬性,一般稱爲事務的ACID屬性。併發
相對於串行處理來講,併發事務處理能大大增長數據庫資源的利用率,提升數據庫系統的事務吞吐量,從而能夠支持能夠支持更多的用戶。但併發事務處理也會帶來一些問題,主要包括如下幾種狀況。性能
在併發事務處理帶來的問題中,「更新丟失」一般應該是徹底避免的。但防止更新丟失,並不能單靠數據庫事務控制器來解決,須要應用程序對要更新的數據加必要的鎖來解決,所以,防止更新丟失應該是應用的責任。優化
「髒讀」、「不可重複讀」和「幻讀」,其實都是數據庫讀一致性問題,必須由數據庫提供必定的事務隔離機制來解決。數據庫實現事務隔離的方式,基本能夠分爲如下兩種。線程
一種是在讀取數據前,對其加鎖,阻止其餘事務對數據進行修改。設計
另外一種是不用加任何鎖,經過必定機制生成一個數據請求時間點的一致性數據快照(Snapshot),並用這個快照來提供必定級別(語句級或事務級)的一致性讀取。從用戶的角度,好像是數據庫能夠提供同一數據的多個版本,所以,這種技術叫作數據多版本併發控制(MultiVersion Concurrency Control,簡稱MVCC或MCC),也常常稱爲多版本數據庫。
數據庫的事務隔離級別越嚴格,併發反作用越小,但付出的代價也就越大,由於事務隔離實質上就是使事務在必定程度上「串行化」進行,這顯然與「併發」是矛盾的,同時,不一樣的應用對讀一致性和事務隔離程度的要求也是不一樣的,好比許多應用對「不可重複讀」和「幻讀」並不敏感,可能更關心數據併發訪問的能力。
爲了解決「隔離」與「併發」的矛盾,ISO/ANSI SQL92定義了4個事務隔離級別,每一個級別的隔離程度不一樣,容許出現的反作用也不一樣,應用能夠根據本身業務邏輯要求,經過選擇不一樣的隔離級別來平衡"隔離"與"併發"的矛盾
隔離級別 | 讀數據一致性 | 髒讀 | 不可重複讀 | 幻讀 |
---|---|---|---|---|
未提交讀(Read uncommitted) | 最低級別,只能保證不讀取物理上損壞的數據 | 是 | 是 | 是 |
已提交度(Read committed) | 語句級 | 否 | 是 | 是 |
可重複讀(Repeatable read) | 事務級 | 否 | 否 | 是 |
可序列化(Serializable) | 最高級別,事務級 | 否 | 否 | 否 |
InnoDB鎖包括了共享鎖和排他鎖,同時爲了容許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖。
當前鎖模式/是否兼容 | X | IX | S | IS |
---|---|---|---|---|
X(排他鎖) | 衝突 | 衝突 | 衝突 | 衝突 |
S(共享鎖) | 衝突 | 衝突 | 兼容 | 兼容 |
IX(意向排他鎖) | 衝突 | 兼容 | 衝突 | 兼容 |
IS(意向共享鎖) | 衝突 | 兼容 | 兼容 | 兼容 |
鍵值在條件範圍內但並不存在的記錄,叫作「間隙(GAP)」,InnoDB也會對這個「間隙」加鎖,這種鎖機制就是所謂的間隙鎖 (Next-Key鎖)。舉例來講,假若有以下數據表:
Id | Name | Age |
---|---|---|
1 | p1 | 10 |
4 | p2 | 18 |
8 | p3 | 12 |
這時候執行以下的sql語句:
select * from demo where id>1 for update;
這時候在數據庫中會對id=4,8的記錄添加行鎖。同時會添加以下間隙鎖:(2,4], (4,8],(8, +suprenum],這種狀況下若是要在數據庫中insert id在間隙鎖之間的記錄是會被阻塞的。
當咱們用範圍條件而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的 索引項加鎖。
舉例來講有表demo:
Id | Name | Age |
---|---|---|
1 | p1 | 10 |
2 | p2 | 18 |
3 | p3 | 12 |
開啓第一個命令執行窗口執行for update操做:
mysql> set autocommit = 0; Query OK, 0 rows affected (0.01 秒) mysql> select * from demo where id>2 for update; +----+------+------+ | id | name | age | +----+------+------+ | 3 | p3 | 12 | | 4 | p4 | 11 | +----+------+------+ 2 行於數據集 (0.02 秒) mysql>
這是一個範圍條件的檢索,InnoDB不只會對符合條件的empid值爲3,4的記錄加鎖,也會對id大於3(這些記錄並不存在)的「間隙」加鎖。
開啓第二個命令窗口執行插入數據操做:
mysql> set autocommit = 0; Query OK, 0 rows affected (0.01 秒) mysql> insert into demo(name,age) values('p5', 22); Lock wait timeout exceeded; try restarting transaction
能夠看到這條sql執行會被阻塞,由於大於3的不存在的記錄也被加上了間隙鎖了,因此在執行插入時就執行不下去了,由於新增記錄的id是大於3的。
除了範圍查詢會使用間隙鎖以外,對於等值查詢而不存在的記錄也會使用間隙鎖,一樣打開第一個命令窗口,去更新一條不存在的記錄。
假如當前數據庫記錄以下:
Id | Name | Age |
---|---|---|
1 | p1 | 10 |
2 | p2 | 18 |
6 | p3 | 12 |
開啓第一個命令執行窗口執行for update操做:
mysql> set autocommit = 0; Query OK, 0 rows affected (0.01 秒) mysql> update demo set name='pp' where id=3; Query OK, 0 rows affected (0.00 秒)
這時候去另外一個命令窗口新增一條記錄,能夠看到這條記錄一直阻塞到超時也沒有執行,這是由於若是一個等值的記錄不存在的狀況下,mysql會使用間隙鎖鎖住它臨近的前面和後面2條記錄之間的全部記錄。對於id=3的記錄來講不存在,因此他會鎖住id(2-6]的全部記錄。
mysql> set autocommit = 0; Query OK, 0 rows affected (0.01 秒) mysql> insert into demo(id,name, age) values(4,'p5', 88); Lock wait timeout exceeded; try restarting transaction
InnoDB使用間隙鎖的目的,一方面是爲了防止幻讀,以知足相關隔離級別的要求,對於上面的例子,要是不使 用間隙鎖,若是其餘事務插入了empid大於100的任何記錄,那麼本事務若是再次執行上述語句,就會發生幻讀;另一方面,是爲了知足其恢復和複製的須要。有關其恢復和複製對鎖機制的影響,以及不一樣隔離級別下InnoDB使用間隙鎖的狀況,在後續的章節中會作進一步介紹。
很顯然,在使用範圍條件檢索並鎖定記錄時,InnoDB這種加鎖機制會阻塞符合條件範圍內鍵值的併發插入,這每每會形成嚴重的鎖等待。所以,在實際應用開發中,尤爲是併發插入比較多的應用,咱們要儘可能優化業務邏輯,儘可能使用相等條件來訪問更新數據,避免使用範圍條件,對於刪除。
在MVCC併發控制中,讀操做能夠分爲2類。快照讀和當前讀,其中快照讀讀取的是可見版本,不加鎖;而當前讀讀取的是最新版本,而且當前讀返回的記錄都會加鎖,保證其餘併發事務不能修改該記錄。那麼MVCC中那些語句會加鎖,那些不加鎖呢?
快照讀:簡單的select屬於快照讀,不須要加鎖。
當前讀:特殊的select操做(select for update),insert/update/delete屬於當前讀,都須要加鎖。
對於InnoDB表,在絕大部分狀況下都應該使用行級鎖,由於事務和行鎖每每是咱們之因此選擇InnoDB表的理由。但在個另特殊事務中,也能夠考慮使用表級鎖。
固然,應用中這兩種事務不能太多,不然,就應該考慮使用MyISAM表。
在InnoDB下 ,使用表鎖要注意如下兩點。
(1)使用LOCK TALBES雖然能夠給InnoDB加表級鎖,但必須說明的是,表鎖不是由InnoDB存儲引擎層管理的,而是由其上一層MySQL Server負責的,僅當autocommit=0、innodb_table_lock=1(默認設置)時,InnoDB層才能知道MySQL加的表鎖,MySQL Server才能感知InnoDB加的行鎖,這種狀況下,InnoDB才能自動識別涉及表級鎖的死鎖;不然,InnoDB將沒法自動檢測並處理這種死鎖。
(2)在用LOCAK TABLES對InnoDB鎖時要注意,要將AUTOCOMMIT設爲0,不然MySQL不會給表加鎖;事務結束前,不要用UNLOCAK TABLES釋放表鎖,由於UNLOCK TABLES會隱含地提交事務;COMMIT或ROLLBACK產不能釋放用LOCAK TABLES加的表級鎖,必須用UNLOCK TABLES釋放表鎖,正確的方式見以下語句。
InnoDB 行鎖是經過給索引上的索引項加鎖來實現的,只有經過索引條件檢索數據,InnoDB 才使用行級鎖,不然,InnoDB將使用表鎖。
因爲 MySQL 行鎖是針對索引加的鎖,不是針對記錄加的鎖,因此雖然是訪問不一樣行的記錄,可是若是是使用相同的索引鍵,是會出現鎖衝突的,設計的時候要注意這一點
當表有多個索引的時候,不一樣的事務可使用不一樣的索引鎖定不一樣的行,另外,不管是使用主鍵索引、惟一索引或普通索引,InnoDB都會使用行鎖來對數據加鎖
即使在條件中使用了索引字段,可是否使用索引來檢索數據是由 MySQL 經過判斷不一樣執行計劃的代價來決定的,若是 MySQL 認爲全表掃描效率更高,好比對一些很小的表,它就不會使用索引,這種狀況下 InnoDB 將使用表鎖,而不是行鎖。所以,在分析鎖衝突時,別忘了檢查 SQL 的執行計劃,以確認是否真正使用了索引
檢索值的數據類型與索引字段不一樣,雖然 MySQL 可以進行數據類型轉換,但卻不會使用索引,從而致使 InnoDB 使用表鎖。經過用explain 檢查兩條SQL的執行計劃
所謂死鎖是2個或者2個以上的併發進程在執行過程當中因爭奪資源而相互等待的現象,若無外力做用它們都將沒法繼續推動下去,此時稱系統處於死鎖狀態或系統產生了死鎖。這些永遠相互等待的進程稱爲死鎖進程。表級鎖不會產生死鎖,因此解決死鎖主要仍是針對於InnoDB。
死鎖的關鍵在於:兩個(或以上)的Session加鎖的順序不一致。
那麼對應的解決死鎖問題的關鍵就是:讓不一樣的session加鎖有次序
這種狀況很好理解,事務A和事務B操做兩張表,但出現循環等待鎖狀況。
事務A | 事務B |
---|---|
begin | begin |
delete from t1 where id=1 | |
update t2 set name='test' where id=2 | |
update t2 set name='test' where id=2 | |
delete from t1 where id=1 |
這種狀況比較常見,以前遇到兩個job在執行數據批量更新時,jobA處理的的id列表爲[1,2,3,4],而job處理的id列表爲[8,9,10,4,2],這樣就形成了死鎖。
事務A | 事務B |
---|---|
begin | begin |
update t2 set name='test' where id=1 | |
update t2 set name='test' where id=2 | |
update t2 set name='test' where id=2 | |
update t2 set name='test' where id=1 |
這種狀況比較隱晦,事務A在執行時,除了在二級索引加鎖外,還會在聚簇索引上加鎖,在聚簇索引上加鎖的順序是[1,4,2,3,5],而事務B執行時,只在聚簇索引上加鎖,加鎖順序是[2,3,4,5],這樣就形成了死鎖的可能性。
事務A | 事務B |
---|---|
update msg set message='訂單' where token>'abc' | delete from msg where id>1 |
innodb在RR級別下,以下的狀況也會產生死鎖,比較隱晦。不清楚的同窗能夠自行根據上節的gap鎖原理分析下。
事務A | 事務B |
---|---|
begin | begin |
update msg set message='訂單' where token='asd' | |
update msg set message='訂單' where token='aaa' | |
insert into msg values(null, 'aad', 'hello'); | |
commit | insert into msg values(null, 'bsa', 'hello'); |
commit |
(1)共享讀鎖(S)之間是兼容的,但共享讀鎖(S)和排他寫鎖(X)之間,以及排他寫鎖之間(X)是互斥的,也就是說讀和寫是串行的。
(2)在必定條件下,MyISAM容許查詢和插入併發執行,咱們能夠利用這一點來解決應用中對同一表和插入的鎖爭用問題。
(3)MyISAM默認的鎖調度機制是寫優先,這並不必定適合全部應用,用戶能夠經過設置LOW_PRIPORITY_UPDATES參數,或在INSERT、UPDATE、DELETE語句中指定LOW_PRIORITY選項來調節讀寫鎖的爭用。
(4)因爲表鎖的鎖定粒度大,讀寫之間又是串行的,所以,若是更新操做較多,MyISAM表可能會出現嚴重的鎖等待,能夠考慮採用InnoDB表來減小鎖衝突。
(1)InnoDB的行銷是基於索引實現的,若是不經過索引訪問數據,InnoDB會使用表鎖。
(2)InnoDB間隙鎖機制,以及InnoDB使用間隙鎖的緣由。
(3)在不一樣的隔離級別下,InnoDB的鎖機制和一致性讀策略不一樣。
(4)MySQL的恢復和複製對InnoDB鎖機制和一致性讀策略也有較大影響。
(5)鎖衝突甚至死鎖很難徹底避免。
在瞭解InnoDB的鎖特性後,用戶能夠經過設計和SQL調整等措施減小鎖衝突和死鎖,包括:
間隙鎖是針對insert而致使阻塞的鎖,實際開發中應該儘可能避免間隙鎖的發生,對於範圍查詢以及等值查詢的修改,儘可能先去查詢數據是否存在,存在再去執行更新和刪除操做,不然可能會產生死鎖。
鎖是解決併發的機制,事務是對一組sql處理單元的統稱,ACID是事務的4個屬性,隔離級別是對事務之間隔離程度的一種描述。