鎖定是電腦協調多個行程或執行緒並發存取某一資源的機制。在資料庫中,除傳統的 運算資源(如CPU、RAM、I/O等)的爭用以外,資料也是許多使用者共享的資源。如何確保資料並發存取的一致性、有效性是所有資料庫必須解決的一 個問題,鎖定衝突也是影響資料庫並發存取效能的重要因素。從這個角度來說,鎖對資料庫而言顯得尤其重要,也更加複雜。本章我們著重討論MySQL鎖定機制 的特點,常見的鎖定問題,以及解決MySQL鎖定問題的一些方法或建議。
Mysql用到了很多這種鎖機制,像是行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。這些鎖統稱為悲觀鎖(Pessimistic Lock)。
InnoDB鎖定
InnoDB與MyISAM的最大差異有兩點:
一是支援事務(TRANSACTION);
二是採用了行級鎖定。行級鎖與表級鎖本來就有許多不同之處,另外,事務的引入也帶來了一些新問題。
1、事務(Transaction)及其ACID屬性
事務是由一組SQL語句組成的邏輯處理單元,事務具有4屬性,通常稱為事務的ACID屬性。
1、原子性(Actomicity):事務是一個原子操作單元,其對資料的修改,要麼全都執行,要麼全都不執行。
2、一致性(Consistent):在交易開始和完成時,資料都必須保持一致狀態。這意味著所有相關的資料規則都必須應用於事務的修改,以操持完整性;事務結束時,所有的內部資料結構(如B樹索引或雙向鍊錶)也都必須是正確的。
3、隔離性(Isolation):資料庫系統提供一定的隔離機制,確保交易在不受外部並發操作影響的「獨立」環境中執行。這意味著事務處理過程中的中間狀態對外部是不可見的,反之亦然。
4、持久性(Durable):事務完成之後,它對於資料的修改是永久性的,即使出現系統故障也能夠維持。
2、並發事務帶來的問題
相對於序列處理來說,並發事務處理能大幅增加資料庫資源的使用率,提高資料庫系統的交易吞吐量,從而可以支援更多的用戶。但並發事務處理也會帶來一些問題,主要包括以下幾種情況。
1、更新遺失(Lost Update):當兩個或多個交易選擇同一行,然後基於最初選定的值更新該行時,由於每個事務都不知道其他事務的存在,就會發生遺失更新問題——最後的更新覆蓋了其他事務所所做的更新。例如,兩位編輯人員製作了同一文件的電子副本。每個編輯人員獨立地更改其副本,然後保存更改後的副本,這樣就覆蓋了原始文件。最後儲存其更改副本的編輯人員會覆蓋另一個編輯人員所做的修改。如果在一個編輯人員完成並提交事務之前,另一個編輯人員無法存取相同文件,則可避免此問題。
2、髒讀(Dirty Reads):一個交易正在對一筆記錄做修改,在這個事務並提交前,這條記錄的資料就處於不一致狀態;這時,另一個事務也來讀取同一筆記錄,如果不加控制,第二個事務讀取了這些「髒」的數據,並據此做進一步的處理,就會產生未提交的數據依賴關係。這種現像被形像地叫做「髒讀」。
3、不可重複讀取(Non-Repeatable Reads):一個交易在讀取某些資料已經發生了改變、或某些記錄已經被刪除了!這種現象叫做「不可重複讀」。
4、幻讀(Phantom Reads):一個事務以相同的查詢條件重新讀取先前檢索過的數據,卻發現其他事務插入了滿足其查詢條件的新數據,這種現象就稱為「幻讀」。
3、交易隔離等級
在並發事務處理所帶來的問題中,「更新遺失」通常應該是完全避免的。但防止更新遺失,並不能單靠資料庫事務控制器來解決,需要應用程式對要更新的資料加必要的鎖定來解決,因此,防止更新遺失應該是應用程式的責任。
“髒讀”、“不可重複讀”和“幻讀”,其實都是資料庫讀取一致性問題,必須由資料庫提供一定的事務隔離機制來解決。資料庫實現事務隔離的方式,基本可以分為以下兩種。
1、一種是在讀取資料前,對其加鎖,阻止其他交易對資料進行修改。
2、另一種是不用加任何鎖,透過一定機制產生一個資料請求時間點的一致性資料快照(Snapshot),並用這個快照來提供某一層級(語句級或交易級)的一致性讀取。從使用者的角度,好像是資料庫可以提供相同資料的多個版本,因此,這種技術叫做資料多版本並發控制(MultiVersion Concurrency Control,簡稱MVCC或MCC),也常稱為多版本資料庫。
在MVCC並發控制中,讀取操作可以分成兩類:快照讀取 (snapshot read)與目前讀取 (current read)。快照讀,讀取的是記錄的可見版本 (有可能是歷史版本),不用加鎖。目前讀取,讀取的是記錄的最新版本,並且,目前讀取返回的記錄,都會加上鎖,保證其他事務不會再並發修改這條記錄。
在一個支援MVCC並發控制的系統中,哪些讀取操作是快照讀取?哪些操作又是目前讀取呢?以MySQL InnoDB為例:
快照讀:簡單的select操作,屬於快照讀,不加鎖。 (當然,也有例外)
select * from table where ?;
目前讀:特殊的讀取操作,插入/更新/刪除操作,屬於目前讀,需要加鎖。
下面語句都屬於目前讀取,讀取記錄的最新版本。並且,讀取之後,還需要確保其他並發事務不能修改目前記錄,對讀取記錄加鎖。其中,除了第一條語句,對讀取記錄加S鎖 (共享鎖)外,其他的操作,都加的是X鎖 (排它鎖)。
資料庫的事務隔離越嚴格,並發副作用越小,但付出的代價也就越大,因為事務隔離實質上就是使事務在一定程度上「串行化」進行,這顯然與「並發」是矛盾的。同時,不同的應用程式對讀取一致性和事務隔離程度的要求也是不同的,例如許多應用程式對「不可重複讀取」和「幻讀」並不敏感 感,可能更關心資料並發存取的能力。
為了解決「隔離」與「並發」的矛盾,ISO/ANSI SQL92定義了4個事務隔離級別,每個級別的隔離程度不同,允許出現的副作用也不同,應用可以根據自己的業務邏輯要求,透過選擇不同的隔離等級來平衡「隔離」與「並發」的矛盾。下表很好地概括了這4個隔離等級的特性。
取得InonoD行鎖定爭用情況
可以透過檢查InnoDB_row_lock狀態變數來分析系統上的行鎖定的爭奪情況:
mysql> show status like 'innodb_row_lock%';
如果發現鎖定爭用比較嚴重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比較高,也可以透過設定InnoDB Monitors來進一步觀察發生鎖定衝突的表、資料行等,並分析鎖爭用的原因。
InnoDB的行鎖定模式及加鎖方法
#InnoDB實作了下列兩種類型的行鎖定。
共享鎖定(s):又稱讀鎖。允許一個事務去讀一行,阻止其他事務獲得相同資料集的排他鎖。若事務T對資料物件A加上S鎖,則事務T可以讀A但不能修改A,其他事務只能再對A加S鎖,而不能加X鎖,直到T釋放A上的S鎖。這保證了其他事務可以讀A,但在T釋放A上的S鎖之前不能對A做任何修改。
排他鎖(X):又稱寫鎖。允許取得排他鎖的事務更新數據,阻止其他事務取得相同的數據集共享讀鎖和排他寫鎖。若事務T對資料物件A加上X鎖,事務T可以讀A也可以修改A,其他事務不能再對A加任何鎖,直到T釋放A上的鎖。
對於共享鎖定大家可能很好理解,就是多個事務只能讀資料不能改資料。
对于排他锁大家的理解可能就有些差别,我当初就犯了一个错误,以为排他锁锁住一行数据后,其他事务就不能读取和修改该行数据,其实不是这样的。排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁。mysql InnoDB引擎默认的修改数据语句:update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。
另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
意向共享锁(IS):事务打算给数据行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。意向排他锁(IX):事务打算给数据行加排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
InnoDB行锁模式兼容性列表:
如果一个事务请求的锁模式与当前的锁兼容,InnoDB就请求的锁授予该事务;反之,如果两者两者不兼容,该事务就要等待锁释放。
意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁。
事务可以通过以下语句显式给记录集加共享锁或排他锁:
1、共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
2、排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE。
用SELECT ... IN SHARE MODE获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作。但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT… FOR UPDATE方式获得排他锁。
InnoDB行锁实现方式
InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。下面通过一些实际例子来加以说明。
(1)在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。
mysql> create table tab_no_index(id int,name varchar(10)) engine=innodb; Query OK, 0 rows affected (0.15 sec) mysql> insert into tab_no_index values(1,'1'),(2,'2'),(3,'3'),(4,'4');Query OK, 4 rows affected (0.00 sec) Records: 4 Duplicates: 0 Warnings: 0
在上面的例子中,看起来session_1只给一行加了排他锁,但session_2在请求其他行的排他锁时,却出现了锁等待!原因就是在没有索引的情况下,InnoDB只能使用表锁。当我们给其增加一个索引后,InnoDB就只锁定了符合条件的行,如下例所示:
创建tab_with_index表,id字段有普通索引:
mysql> create table tab_with_index(id int,name varchar(10)) engine=innodb; mysql> alter table tab_with_index add index id(id);
(2)由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。应用设计的时候要注意这一点。
在下面的例子中,表tab_with_index的id字段有索引,name字段没有索引:
mysql> alter table tab_with_index drop index name; 1 Query OK, 4 rows affected (0.22 sec) Records: 4 Duplicates: 0 Warnings: 0 mysql> insert into tab_with_index values(1,'4'); 1 Query OK, 1 row affected (0.00 sec) mysql> select * from tab_with_index where id = 1;
InnoDB存储引擎使用相同索引键的阻塞例子 :
(3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
在下面的例子中,表tab_with_index的id字段有主键索引,name字段有普通索引:
mysql> alter table tab_with_index add index name(name); 1Query OK, 5 rows affected (0.23 sec) Records: 5 Duplicates: 0 Warnings: 0
InnoDB存储引擎的表使用不同索引的阻塞例子 :
(4)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决 定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突 时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。
比如,在tab_with_index表里的name字段有索引,但是name字段是varchar类型的,检索值的数据类型与索引字段不同,虽然MySQL能够进行数据类型转换,但却不会使用索引,从而导致InnoDB使用表锁。通过用explain检查两条SQL的执行计划,我们可以清楚地看到了这一点。
mysql> explain select * from tab_with_index where name = 1 \G mysql> explain select * from tab_with_index where name = '1' \G
间隙锁(Next-Key锁)
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,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这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。
还要特别说明的是,InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁!下面这个例子假设emp表中只有101条记录,其empid的值分别是1,2,……,100,101。
InnoDB存储引擎的间隙锁阻塞例子
小结
本文重点介绍了MySQL中MyISAM表级锁和InnoDB行级锁的实现特点,并讨论了两种存储引擎经常遇到的锁问题和解决办法。
對於MyISAM的表鎖,主要討論了以下幾點:
(1)共享讀鎖(S)之間是相容的,但共享讀鎖(S)與排他寫鎖(X)之間,以及排他寫鎖(X)之間是互斥的,也就是說讀和寫是串列的。
(2)在一定條件下,MyISAM允許查詢和插入並發執行,我們可以利用這一點來解決應用中對相同表查詢和插入的鎖爭用問題。
(3)MyISAM預設的鎖定調度機制是寫優先,這並不一定適合所有應用,使用者可以透過設定LOW_PRIORITY_UPDATES參數,或在INSERT、UPDATE、DELETE語句中指定LOW_PRIORITY選項來調節讀寫鎖定的爭用。
(4)由於表鎖的鎖定粒度大,讀寫之間又是串行的,因此,如果更新操作較多,MyISAM表可能會出現嚴重的鎖等待,可以考慮採用InnoDB表來減少鎖衝突。
對於InnoDB表,本文主要討論了以下幾項內容:
(1)InnoDB的行鎖定是基於索引實現的,如果不透過索引存取數據,InnoDB會使用表鎖定。
(2)介紹了InnoDB間隙鎖定(Next-key)機制,以及InnoDB使用間隙鎖定的原因。
在不同的隔離等級下,InnoDB的鎖定機制和一致性讀取策略不同。
在了解InnoDB鎖定特性後,使用者可以透過設計和SQL調整等措施減少鎖定衝突和死鎖,包括:
盡量使用較低的隔離等級;精心設計索引,並盡量使用索引存取數據,使加鎖更精確,從而減少鎖衝突的機會;選擇合理的事務大小,小事務發生鎖衝突的幾率也更小;給記錄集明確加鎖時,最好一次性請求足夠等級的鎖。例如要修改資料的話,最好直接申請排他鎖,而不是先申請共享鎖,修改時再請求排他鎖,這樣容易產生死鎖;不同的程序訪問一組表時,應盡量約定以相同的順序訪問各表,對一個表而言,盡可能以固定的順序存取表中的行。這樣可以大幅減少死鎖的機會;盡量用相等條件存取數據,這樣可以避免間隙鎖對並發插入的影響; 不要申請超過實際需要的鎖級別;除非必須,查詢時不要顯示加鎖;對於一些特定的事務,可以使用表鎖來提高處理速度或減少死鎖的可能。
想了解更多相關內容請造訪PHP中文網:mysql影片教學
#以上是關於mysql鎖機制原理的詳細講解(二)的詳細內容。更多資訊請關注PHP中文網其他相關文章!