MySQL事務與鎖
#
鎖的基本概念
鎖是計算機協調多個進程或線程並發訪問某一資源的機制。
相對其他數據庫而言,MySQL的鎖機制比較簡單,其最顯著的特點是不同的存儲引擎支持不同的鎖機制。比如,MyISAM存儲引擎采用的是表級鎖(table-level locking);BDB存儲引擎采用的是頁面鎖(page-level locking),但也支持表級鎖(已過時);InnoDB存儲引擎既支持行級鎖(row-level locking),也支持表級鎖,但默認情況下是采用行級鎖。
MySQL這3種鎖的特性可大致歸納如下。
開銷、加鎖速度、死鎖、粒度、並發性能
- 表級鎖:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖沖突的概率最高,並發度最低。
- 行級鎖:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低,並發度也最高。
- 頁面鎖:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,並發度一般。
表級鎖:一次性插入和更新較多數據時,當很多操作都是讀表時可以選擇。但當select語句時間過長或者update和delete語句短而且次數多時,不適用,會各種鎖沖突。
行級鎖:在很多線程請求不同記錄時減少沖突鎖。事務回滾時減少改變數據。使長時間對單獨的一行記錄加鎖成為可能。比頁級鎖和表級鎖消耗更多的內存。當需要頻繁對大部分數據做 GROUP BY 操作或者需要頻繁掃描整個表時不適合。
MyISAM表鎖
MyISAM存儲引擎只支持表鎖,不支持事務安全。
查詢表級鎖爭用情況
mysql> show status like 'table%';
MySQL表級鎖的鎖模式
MySQL的表級鎖有兩種模式:表共享讀鎖(Table Read Lock)和表獨占寫鎖(Table Write Lock)。
對MyISAM表的讀操作,不會阻塞其他用戶對同一表的讀請求,但會阻塞對同一表的寫請求;對 MyISAM表的寫操作,則會阻塞其他用戶對同一表的讀和寫操作;MyISAM表的讀操作與寫操作之間,以及寫操作之間是串行的!
MyISAM在執行查詢語句(SELECT)前,會自動給涉及的所有表加讀鎖,在執行更新操作(UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,這個過程並不需要用戶干預,因此,用戶一般不需要直接用LOCK TABLE命令給MyISAM表顯式加鎖。
在用LOCK TABLES給表顯式加表鎖時,必須同時取得所有涉及到表的鎖,並且MySQL不支持鎖升級。也就是說,在執行LOCK TABLES后,只能訪問顯式加鎖的這些表,不能訪問未加鎖的表;同時,如果加的是讀鎖,那么只能執行查詢操作,而不能執行更新操作。其實,在自動加鎖的情況下也基本如此,MyISAM總是一次獲得SQL語句所需要的全部鎖。這也正是MyISAM表不會出現死鎖(Deadlock Free)的原因。
當使用LOCK TABLES時,不僅需要一次鎖定用到的所有表,別名也要鎖定,否則也會出錯!
比如鎖定actor表,他有兩個別名:
mysql> lock table actor as a read,actor as b read;
並發插入(Concurrent Inserts)
上文提到過MyISAM表的讀和寫是串行的,但這是就總體而言的。在一定條件下,MyISAM表也支持查詢和插入操作的並發進行。
MyISAM存儲引擎有一個系統變量concurrent_insert,專門用以控制其並發插入的行為,其值分別可以為0、1或2。
- 當concurrent_insert設置為0時,不允許並發插入。
- 當concurrent_insert設置為1時,如果MyISAM表中沒有空洞(即表的中間沒有被刪除的行),MyISAM允許在一個進程讀表的同時,另一個進程從表尾插入記錄。這也是MySQL的默認設置。
- 當concurrent_insert設置為2時,無論MyISAM表中有沒有空洞,都允許在表尾並發插入記錄。
默認情況下:
session_1獲得了一個表的READ LOCAL鎖,該線程可以對表進行查詢操作,但不能對表進行更新操作;其他的線程(session_2),雖然不能對表進行刪除和更新操作,但卻可以對該表進行並發插入操作,等session_1解鎖后再執行更新刪除操作
MyISAM的鎖調度
前面講過,MyISAM存儲引擎的讀鎖和寫鎖是互斥的,讀寫操作是串行的。那么,一個進程請求某個 MyISAM表的讀鎖,同時另一個進程也請求同一表的寫鎖,MySQL如何處理呢?答案是寫進程先獲得鎖。不僅如此,即使讀請求先到鎖等待隊列,寫請求后到,寫鎖也會插到讀鎖請求之前!這是因為MySQL認為寫請求一般比讀請求要重要。這也正是MyISAM表不太適合於有大量更新操作和查詢操作應用的原因,因為,大量的更新操作會造成查詢操作很難獲得讀鎖,從而可能永遠阻塞。這種情況有時可能會變得非常糟糕!幸好我們可以通過一些優先級設置來調節MyISAM 的調度行為,對不同的應用設定讀優先或者寫優先。
InnoDB鎖問題
InnoDB與MyISAM的最大不同有兩點:一是支持事務(TRANSACTION);二是采用了行級鎖。行級鎖與表級鎖本來就有許多不同之處,另外,事務的引入也帶來了一些新問題。下面我們先介紹一點背景知識,然后詳細討論InnoDB的鎖問題。
事務
事務處理可以用來維護數據庫的完整性,保證成批的 SQL 語句要么全部執行,要么全部不執行。
事務用來管理 insert,update,delete 語句
一般來說,事務是必須滿足4個條件(ACID): Atomicity(原子性)、Consistency(一致性)、Isolation(隔離性)、Durability(持久性)
1、原子性:一組事務,要么全部成功;要么撤回。
2、一致性 :滿足模式鎖指定的約束,比如銀行轉賬前后總金額應該不變。事務結束時,所有的內部數據結構(如B樹索引)也都必須是正確的。
3、隔離性:事務獨立運行。一個事務所做的修改在最終提交之前,對其它事務是不可見的。事務的100%隔離,需要犧牲速度。
4、持久性:軟、硬件崩潰后,InnoDB數據表驅動會利用日志文件重構修改,或者通過數據庫備份和恢復來保證。
注意幾點
- 在默認情況下,MySQL每執行一條SQL語句,都是一個單獨的事務。
- 在 MySQL 命令行的默認設置下,事務都是自動提交的,即執行 SQL 語句后就會馬上執行 COMMIT 操作。即對於獨立的每條sql語句,mysql會自動提交或者回滾。
- 如果要執行多條SQL語句組成的事務,可以顯式地使用命令 BEGIN 或 START TRANSACTION開啟事務,使用commit或rollback結束事務。(或者執行命令 SET AUTOCOMMIT=0,用來禁止當前會話使用自動提交,但是這種方式不推薦。)
- 在InnoDB的事務中,對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及數據行加排他鎖(X);對於普通SELECT語句,InnoDB不會加任何鎖(通過多版本並發控制實現,同時有效的解決了幻讀的問題)。
- 折返點:比如設置折返點:
SAVEPOINT adqoo_1
,然后利用ROLLBACK TO SAVEPOINT adqoo_1
實現發生在折返點 adqoo_1 之前的事務被提交,之后的被忽略。 - 樂觀鎖,即通過version來實現當並發沖突發生時,沖突的SQL語句會執行失敗,然后重試或者放棄或者交給用戶處理,但不能保證事務的ACID特性。(注意:
update seckill set number = number -1 where seckill_id = 1000 lock in share mode;
是錯誤的,lock in share mode和for update只能用在select語句中) - InnoDB采用的是兩階段鎖定協議。在事務執行過程中,隨時都可以執行鎖定,鎖只有在執行COMMIT或者ROLLBACK時才會釋放,並且所有的鎖是在同一時刻被釋放。所以在一個事務中,推薦最后執行需要獨占(獲得讀鎖)的行,盡量減少行鎖持有的時間。
- InnoDB目前處理死鎖的方法是:將持有最少行級排它鎖的事務回滾。如果是因為死鎖引起的回滾,可以考慮在應用程序中重新執行。
- 在事務中,如果要更新記錄,應該直接申請足夠級別的鎖,即排他鎖,而不應先申請共享鎖,更新時再申請排他鎖,因為當用戶申請排他鎖時,其他事務可能又已經獲得了相同記錄的共享鎖,從而造成鎖沖突,甚至死鎖。
並發事務處理帶來的問題
相對於串行處理來說,並發事務處理能大大增加數據庫資源的利用率,提高數據庫系統的事務吞吐量,從而可以支持更多的用戶。但並發事務處理也會帶來一些問題,主要包括以下幾種情況。
- 更新丟失(Lost Update):當兩個或多個事務選擇同一行,然后基於最初選定的值更新該行時,由於每個事務都不知道其他事務的存在,就會發生丟失更新問題--最后的更新覆蓋了由其他事務所做的更新。例如,兩個編輯人員制作了同一文檔的電子副本。每個編輯人員獨立地更改其副本,然后保存更改后的副本,這樣就覆蓋了原始文檔。最后保存其更改副本的編輯人員覆蓋另一個編輯人員所做的更改。如果在一個編輯人員完成並提交事務之前,另一個編輯人員不能訪問同一文件,則可避免此問題。“更新丟失”通常是應該完全避免的。但防止更新丟失,並不能單靠數據庫事務控制器來解決,需要應用程序對要更新的數據加必要的鎖來解決,因此,防止更新丟失應該是應用的責任。
- 臟讀(Dirty Reads):一個事務正在對一條記錄做修改,在這個事務完成並提交前,這條記錄的數據就處於不一致狀態;這時,另一個事務也來讀取同一條記錄,如果不加控制,第二個事務讀取了這些“臟”數據,並據此做進一步的處理,就會產生未提交的數據依賴關系。這種現象被形象地叫做"臟讀"。
- 不可重復讀(Non-Repeatable Reads):一個事務讀取某些數據,在它結束讀取之前,另一個事務可能完成了對數據行的更改。當第一個事務試圖再次執行同一個查詢,服務器就會返回不同的結果。
- 幻讀(Phantom Reads):一個事務按相同的查詢條件重新讀取以前檢索過的數據,卻發現其他事務插入了滿足其查詢條件的新數據,這種現象就稱為“幻讀”。
“臟讀”、“不可重復讀”和“幻讀”,其實都是數據庫讀一致性問題,必須由數據庫提供一定的事務隔離機制來解決。
數據庫實現事務隔離的方式,基本上可分為以下兩種。
- 一種是在讀取數據前,對其加鎖,阻止其他事務對數據進行修改。
- 另一種是不用加任何鎖,通過一定機制生成一個數據請求時間點的一致性數據快照(Snapshot),並用這個快照來提供一定級別(語句級或事務級)的一致性讀取。從用戶的角度來看,好像是數據庫可以提供同一數據的多個版本,因此,這種技術叫做數據多版本並發控制(MultiVersion Concurrency Control,簡稱MVCC或MCC),也經常稱為多版本數據庫。
數據庫的事務隔離越嚴格,並發副作用越小,但付出的代價也就越大,因為事務隔離實質上就是使事務在一定程度上 “串行化”進行,這顯然與“並發”是矛盾的。同時,不同的應用對讀一致性和事務隔離程度的要求也是不同的,比如許多應用對“不可重復讀”和“幻讀”並不敏感,可能更關心數據並發訪問的能力。
為了解決“隔離”與“並發”的矛盾,ISO/ANSI SQL92定義了4個事務隔離級別
4種事務隔離級別
隔離級別就是對對事務並發控制的等級
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL
READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE
1、不帶SESSION、GLOBAL的SET命令
只對下一個事務有效
2、SET SESSION
為當前會話設置隔離模式
3、SET GLOBAL
為以后新建的所有MYSQL連接設置隔離模式(當前連接不包括在內)
-
read uncommitted不提交的讀:
即臟讀,一個事務修改了一行,另一個事務也可以讀到該行。
如果第一個事務執行了回滾,那么第二個事務讀取的就是從來沒有正式出現過的值。 -
read committed提交的讀
即不可重復讀,試圖通過只讀取提交的值的方式來解決臟讀的問題,但是這又引起了不可重復讀取的問題。
事務1在兩次查詢的過程中,事務2對該表進行了插入、刪除操作,從而事務1第二次查詢的結果發生了變化。(未加鎖) -
repeatable read(默認)
即可重復讀,在一個事務對數據行執行讀取或寫入操作時鎖定了這些數據行。
但是這種方式又引發了幻讀的問題。
因為只能鎖定讀取或寫入的行,不能阻止另一個事務插入數據,后期執行同樣的查詢會產生更多的結果。
- serializable可串行化
事務被強制為依次執行。這是 SQL 標准建議的默認行為。
大多數的數據庫系統的默認事務隔離級別都是:Read committed
而MySQL的默認事務隔離級別是:Repeatable Read
事務鎖定模式
**獲取InnoDB行鎖爭用情況 **
mysql> show status like 'innodb_row_lock%';
如果發現鎖爭用比較嚴重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比較高,還可以通過設置InnoDB Monitors來進一步觀察發生鎖沖突的表、數據行等,並分析鎖爭用的原因。
具體方法如下:
mysql> CREATE TABLE innodb_monitor(a INT) ENGINE=INNODB;
然后就可以用下面的語句來進行查看:
mysql> Show innodb status\G;
監視器可以通過發出下列語句來停止查看:
mysql> DROP TABLE innodb_monitor;
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上的鎖。這保證了其他事務在T釋放A上的鎖之前不能再讀取和修改A。
另外,為了允許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖。
- 意向共享鎖(IS):事務打算給數據行加行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖。
- 意向排他鎖(IX):事務打算給數據行加行排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的IX鎖。
注意:
意向鎖僅僅用於表鎖和行鎖的共存使用。如果我們的操作僅僅涉及行鎖,那么意向鎖不會對我們的操作產生任何影響。在任一操作給表A的一行記錄加鎖前,首先要給該表加意向鎖,如果獲得了意向鎖,然后才會加行鎖,並在加行鎖時判斷是否沖突。如果現在有一個操作要獲得表A的表鎖,由於意向鎖的存在,表鎖獲取會失敗(如果沒有意向鎖的存在,加表鎖之前可能要遍歷整個聚簇索引,判斷是否有行鎖存在,如果沒有行鎖才能加表鎖)。
同理,如果某一操作已經獲得了表A的表鎖,那么另一操作獲得行鎖之前,首先會檢查是否可以獲得意向鎖,並在獲得意向鎖失敗后,等待表鎖操作的完成。也就是說:1.意向鎖是表級鎖,但是卻表示事務正在讀或寫某一行記錄;2.意向鎖之間不會沖突, 因為意向鎖僅僅代表要對某行記錄進行操作,在加行鎖時,會判斷是否沖突;3.意向鎖是InnoDB自動加的,不需用戶干預。
下面列出上述鎖模式的兼容情況:
| IS | IX | S | X |
---|---|---|---|
IS | + | + | + |
IX | + | + | – |
S | + | – | + |
X | – | – | – |
其中:+表示兼容;–表示不兼容。如果一個事務請求的鎖模式與當前的鎖兼容,InnoDB就將請求的鎖授予該事務;反之,如果兩者不兼容,該事務就要等待鎖釋放。
事務可以通過以下語句顯式的給記錄集加共享鎖或排他鎖:
共享鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
排他鎖(X):SELECT * FROM table_name WHERE ... FOR UPDATE。
InnoDB行鎖實現方式
InnoDB行鎖是通過給索引上的索引項加鎖來實現的,這一點MySQL與Oracle不同,后者是通過在數據塊中對相應數據行加鎖來實現的。InnoDB這種行鎖實現特點意味着:只有通過索引條件檢索數據,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖!
當訪問不同行的記錄,但是如果是使用相同的索引鍵,是會出現鎖沖突的。
create table tab_with_index(id int,name varchar(10)) engine=innodb;
alter table tab_with_index add index id(id);
alter table tab_with_index drop index name;
insert into tab_with_index values(1,'1');
insert into tab_with_index values(1,'4');
此時若
select * from tab_with_index where id = 1 and name = '1' for update;
會對兩行數據都加排它鎖。
當表有多個索引的時候,不同的事務可以使用不同的索引鎖定不同的行,另外,不論是使用主鍵索引、唯一索引或普通索引,InnoDB都會使用行鎖來對數據加鎖。
間隙鎖(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也會使用間隙鎖!
事務回滾的實現
MySQL:是SQL語句級的,在執行事務中的SQL語句前,需要先在日志緩沖寫日志,記錄該事務的日志序列號和執行的SQL語句。當事務提交時,必須將存儲引擎的日志緩沖寫入磁盤(通過innodb_flush_log_at_trx_commit來控制)。如果回滾,不是物理恢復,是邏輯恢復,因為它是通過執行相反的dml語句來實現的。而且不會回收因為insert和upate而新增加的page頁的。即insert變成delete,update變成相反的update。
Oracle是基於數據庫文件塊的。
從上面兩點可知,MySQL的恢復機制要求:在一個事務未提交前,其他並發事務不能插入滿足其鎖定條件的任何記錄(比如上面的>100都會鎖定的例子,只要不插入>100的數,就不會幻讀),也就是不允許出現幻讀,這已經超過了ISO/ANSI SQL92“可重復讀”隔離級別的要求,實際上是要求事務要串行化。
多版本並發控制MVCC
MVCC (Multiversion Concurrency Control),即多版本並發控制技術,它使得大部分支持行鎖的事務引擎,不再單純的使用行鎖來進行數據庫的並發控制,取而代之的是,把數據庫的行鎖與行的多個版本結合起來,只需要很小的開銷,就可以實現一致性非鎖定讀,從而大大提高數據庫系統的並發性能。
為了實現MVCC,InnoDB對每一行都加上了兩個隱藏的列,其中一列存儲行被創建的”時間”,另外一列存儲行被刪除的”時間”。當然InnoDB存儲的並不是絕對的時間,而是系統版本號,即記錄創建版本號和刪除版本號。每當一個事務開始的時候,InnoDB都會給這個事務分配一個遞增的版本號,事務開始時的系統版本號會作為事務的版本號。下面在repeatable read隔離級別下,說明MVCC的具體操作:
- SELECT
對於select語句,只有同時滿足了下面兩個條件的行,才能被返回:
- 創建版本號小於或者等於當前事務版本號 ,就是說記錄創建是在事務中(等於的情況)或者事務啟動之前。
- 行的刪除版本號要么沒有被定義,要么大於當前事務的版本號:行的刪除版本號如果沒有被定義,說明該行沒有被刪除過;如果刪除版本號大於當前事務的版本號,說明該行是被該事務后面啟動的事務刪除的,由於是repeatable read隔離級別,后開始的事務對數據的影響不應該被先開始的事務看見,所以該行可能被返回。
- INSERT
在插入操作時,記錄的創建版本號改為當前事務版本號。 - DELETE
在刪除操作時,記錄的刪除版本號改為當前事務版本號,相當於標記為刪除,而不是實際刪除。 - UPDATE
在更新操作的時候,采用的是先標記舊的那行記錄為已刪除,並且刪除版本號改為當前事務版本號,然后插入一行新的記錄。
上述策略的結果就是,在讀取數據的時候,InnoDB幾乎不用獲得任何鎖,每個查詢都通過版本檢查,只獲得自己需要的數據版本,從而大大提高了系統的並發度。
這種策略的缺點是,每行記錄都需要額外的存儲空間,更多的行檢查工作和一些額外的維護工作。
另外,只有read-committed和 repeatable-read 兩種事務隔離級別才能使用MVCC,read-uncommited由於是讀到未提交的,所以不存在版本的問題。而serializable 則會對所有讀取的行加鎖。
在某些情況下,用戶需要顯式地對數據庫讀取操作進行加鎖以保證數據邏輯的一致性。事務可以通過以下語句顯式的給記錄集加共享鎖或排他鎖:
共享鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
排他鎖(X):SELECT * FROM table_name WHERE ... FOR UPDATE。
Optimize Table
是mysql中一個可以回收更多的空間、減少“碎片”(defragment)的命令。當表上的數據行被刪除時,所占據的磁盤空間並沒有立即被回收,使用了OPTIMIZE TABLE命令后這些空間將被回收,並且對磁盤上的數據行進行重排(注意:是磁盤上,而非數據庫)。
多數時間並不需要運行OPTIMIZE TABLE,只需在批量刪除數據行之后,或定期(每周一次或每月一次)進行一次數據表優化操作即可,只對那些特定的表運行。
關於死鎖
MyISAM表鎖不會出現死鎖,這是因為MyISAM總是一次獲得所需的全部鎖,要么全部滿足,要么等待,因此不會出現死鎖。但在InnoDB中,除單個SQL組成的事務外,鎖是逐步獲得的,這就決定了在InnoDB中發生死鎖是可能的。
兩個事務都需要獲得對方持有的排他鎖才能繼續完成事務,這種循環鎖等待就是典型的死鎖。
InnoDB目前處理死鎖的方法是:將持有最少行級排它鎖的事務回滾。如果是因為死鎖引起的回滾,可以考慮在應用程序中重新執行。但在涉及外部鎖,或涉及表鎖的情況下,InnoDB並不能完全自動檢測到死鎖,這需要通過設置鎖等待超時參數 innodb_lock_wait_timeout來解決。需要說明的是,這個參數並不是只用來解決死鎖問題,在並發訪問比較高的情況下,如果大量事務因無法立即獲得所需的鎖而掛起,會占用大量計算機資源,造成嚴重性能問題,甚至拖跨數據庫。我們通過設置合適的鎖等待超時閾值,可以避免這種情況發生。
通常來說,死鎖都是應用設計的問題,通過調整業務流程、數據庫對象設計、事務大小,以及訪問數據庫的SQL語句,絕大部分死鎖都可以避免。介紹幾種避免死鎖的常用方法。
(1)在應用中,如果不同的程序會並發存取多個表,應盡量約定以相同的順序來訪問表,這樣可以大大降低產生死鎖的機會。
(2)在程序以批量方式處理數據的時候,如果事先對數據排序,保證每個線程按固定的順序來處理記錄,也可以大大降低出現死鎖的可能。比如兩個會話讀取前十個用戶的信息,每次讀取一個,那么我們可以規定他們從第一個用戶開始讀,而不是倒序,這樣不會死鎖。
(3)在事務中,如果要更新記錄,應該直接申請足夠級別的鎖,即排他鎖,而不應先申請共享鎖,更新時再申請排他鎖,因為當用戶申請排他鎖時,其他事務可能又已經獲得了相同記錄的共享鎖,從而造成鎖沖突,甚至死鎖。
(4) 選擇合理的事務大小,小事務發生鎖沖突的幾率也更小;
如果出現死鎖,可以用SHOW INNODB STATUS命令來確定最后一個死鎖產生的原因。返回結果中包括死鎖相關事務的詳細信息,如引發死鎖的SQL語句,事務已經獲得的鎖,正在等待什么鎖,以及被回滾的事務等。