(二) MYSQL 事務與鎖


(一)背景:

相對其他數據庫而言,MySQL 的鎖機制比較簡單,其最顯著的特點是不同的存儲引擎支持不同的鎖機制。比如,MyISAM和MEMORY存儲引擎采用的是表級鎖(table-level locking);BDB存儲引擎采用的是頁面鎖(page-levellocking),但也支持表級鎖;InnoDB存儲引擎既支持行級鎖(row-levellocking),也支持表級鎖,但默認情況下是采用行級鎖。

MySQL這3種鎖的特性可大致歸納如下。

表級鎖:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖沖突的概率最高,並發度最低。

行級鎖:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低,並發度也最高。

頁面鎖:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,並發度一般。

(二)MyISAM表鎖

2.1 MyISAM表鎖模式:

MySQL的表級鎖有兩種模式:表共享讀鎖(Table Read Lock)和表獨占寫鎖(Table Write Lock)。鎖模式的兼容性如下表所示:

 

加讀鎖之后,其它線程只能讀,不能更新;加寫鎖之后,其它線程既不能讀也不能更新

MyISAM 表的讀操作,不會阻塞其他用戶對同一表的讀請求,但會阻塞對同一表的寫請求;對 MyISAM 表的寫操作,則會阻塞其他用戶對同一表的讀和寫操作;MyISAM表的讀操作與寫操作之間,以及寫操作之間是串行的!當一個線程獲得對一個表的寫鎖后,只有持有鎖的線程可以對表進行更新操作(UPDATE、DELETE、INSERT等)。其他線程的讀、寫操作都會等待,直到鎖被釋放為止。

 2.2 如何給MyISAM加表鎖:

MyISAM存儲引擎的讀鎖和寫鎖是互斥的,讀寫操作是串行的。MyISAM默認執行查詢操作時加讀鎖,更新操作時加寫鎖。在更新操作比較頻繁時,查詢操作就需要等待,甚至可能永遠阻塞。此外MYSQL默認為寫請求比讀請求優先級要高,當同時有一個讀進程和一個寫進程時,會優先處理寫進程,這就是MyISAM表不太適合於大量更新操作和查詢操作的原因

MyISAM在執行查詢語句(SELECT)前,會自動給涉及的所有表加讀鎖,在執行更新操作(UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,這個過程並不需要用戶干預,因此,用戶一般不需要直接用LOCK TABLE命令給MyISAM表顯式加鎖。顯示加鎖和解鎖:Lock tables tableName; Unlock tables;

【當前session該某表加鎖后,這個session只能訪問這個加鎖的表,而不能訪問其它的表。如果加的是讀鎖,那么其它會話是可以訪問這個被加鎖的表】

在用LOCK TABLES給表顯式加表鎖時,必須同時取得所有涉及表的鎖,並且MySQL不支持鎖升級。也就是說,在執行LOCK TABLES后,只能訪問顯式加鎖的這些表,不能訪問未加鎖的表;同時,如果加的是讀鎖,那么只能執行查詢操作,而不能執行更新操作。在自動加鎖的情況下也是如此,MyISAM總是一次獲得SQL語句所需要的全部鎖。這也正是MyISAM表不會出現死鎖(Deadlock Free)的原因。

2.3 並發插入:

就總體而言MyISAM表的讀和寫是串行的,在一定條件下,MyISAM表也支持查詢和插入操作的並發進行。

MyISAM 存儲引擎有一個系統變量 concurrent_insert,專門用以控制其並發插入的行為,其值分別可以為0、1或2。

1 當concurrent_insert設置為0時,不允許並發插入。

2 當concurrent_insert設置為1時,如果MyISAM表中沒有空洞(即表的中間沒有被刪除的行),MyISAM允許在一個進程讀表的同時,另一個進程從表尾插入記錄。這也是MySQL的默認設置。對於表空洞,可以過定期在系統空閑時段執行 OPTIMIZE TABLE 語句來整理空間碎片,收回因刪除記錄而產生的中間空洞。

3 當concurrent_insert設置為2時,無論MyISAM表中有沒有空洞,都允許在表尾並發插入記錄。

2.4 MyISAM的鎖調度:

針對於MyISAM表讀寫操作的並發時,讀操作在默認情況下的劣勢,可以通過一些設置來調節MyISAM的調度行為,從而適當解決讀嚴重等待的問題。

1 通過指定啟動參數low-priority-updates,使MyISAM引擎默認給予讀請求以優先的權利。

2 通過執行命令SET LOW_PRIORITY_UPDATES=1,使該連接發出的更新請求優先級降低。

3 通過指定INSERT、UPDATE、DELETE語句的LOW_PRIORITY屬性,降低該語句的優先級。

4 給系統參數max_write_lock_count設置一個合適的值,當一個表的讀鎖達到這個值后,MySQL就暫時將寫請求的優先級降低,給讀進程一定獲得鎖的機會。

(三)InnoDB鎖

InnoDB 與 MyISAM 的最大不同有兩點:一是支持事務(TRANSACTION);二是采用了行級鎖。行級鎖與表級鎖本來就有許多不同之處,另外,事務的引入也帶來了一些新問題。

3.1 背景知識:

1 事務(Transaction)及其ACID屬性:

事務是由一組 SQL 語句組成的邏輯處理單元,事務具有以下 4 個屬性,通常簡稱為事務的ACID屬性。

原子性(Atomicity):事務是一個原子操作單元,其對數據的修改,要么全都執行,要么全都不執行。【要么pass, 要么all in

一致性(Consistent):在事務開始和完成時,數據都必須保持一致狀態。這意味着所有相關的數據規則都必須應用於事務的修改,以保持數據的完整性;事務結束時,所有的內部數據結構(如B樹索引或雙向鏈表)也都必須是正確的。

隔離性(Isolation):數據庫系統提供一定的隔離機制,保證事務在不受外部並發操作影響的“獨立”環境執行。這意味着事務處理過程中的中間狀態對外部是不可見的,反之亦然。[誰也不知道誰中間干了些啥]

持久性(Durable):事務完成之后,它對於數據的修改是永久性的,即使出現系統故障也能夠保持。【一經修改,永久保存

2 並發事務處理引發的問題:

更新丟失(Lost Update):當兩個或多個事務選擇同一行,然后基於最初選定的值更新該行時,由於每個事務都不知道其他事務的存在,就會發生丟失更新問題—最后的更新覆蓋了由其他事務所做的更新。例如,兩個編輯人員制作了同一文檔的電子副本。每個編輯人員獨立地更改其副本,然后保存更改后的副本,這樣就覆蓋了原始文檔。最后保存其更改副本的編輯人員覆蓋另一個編輯人員所做的更改。如果在一個編輯人員完成並提交事務之前,另一個編輯人員不能訪問同一文件,則可避免此問題。

丟失更新的問題可通過LBCC(稱Lock Based Concurrency Control)的方式進行控制,就是一個事務去讀取一條數據的時候,就上鎖,不允許其他事務來操作。MySQL加鎖之后就是當前讀。假如當前事務只是加共享鎖,那么其他事務就不能有排他鎖,也就是不能修改數據;而假如當前事務需要加排他鎖,那么其他事務就不能持有任何鎖。總而言之,能加鎖成功,就確保了除了當前事務之外,其他事務不會對當前數據產生影響,所以自然而然的,當前事務讀取到的數據就只能是最新的

臟讀(Dirty Reads):一個事務正在對一條記錄做修改,在這個事務完成並提交前,這條記錄的數據就處於不一致狀態;這時,另一個事務也來讀取同一條記錄,如果不加控制,第二個事務讀取了這些“臟”數據,並據此做進一步的處理,就會產生未提交的數據依賴關系。這種現象被形象地叫做“臟讀”。【讀取了被修改但是沒有提交的數據

RC隔離級別+MVCC可以解決臟讀的問題,后續將在MVCC章節介紹

不可重復讀(Non-Repeatable Reads):一個事務在讀取某些數據后的某個時間,再次讀取以前讀過的數據,卻發現其讀出的數據已經發生了改變或某些記錄已經被刪除了!這種現象就叫做“不可重復讀”。【同一個記錄前后讀取結果不一致,其原因是其它事務修改了這條記錄

RR隔離級別+MVCC可以解決不可重復讀的問題,后續將在MVCC章節介紹

幻讀(Phantom Reads):一個事務按相同的查詢條件重新讀取以前檢索過的數據,卻發現其他事務插入了滿足其查詢條件的新數據,這種現象就稱為“幻讀”。【同一個查詢條件,查詢結果不一樣,其原因是插入了相同查詢條件的記錄

3 事務隔離級別:

“臟讀”、“不可重復讀”和“幻讀”,其實都是數據庫讀一致性問題,必須由數據庫提供一定的事務隔離機制來解決。數據庫實現事務隔離的方式,基本上可分為以下兩種。一種是在讀取數據前,對其加鎖,阻止其他事務對數據進行修改。

另一種是不用加任何鎖,通過一定機制生成一個數據請求時間點的一致性數據快照(Snapshot),並用這個快照來提供一定級別(語句級或事務級)的一致性讀取。從用戶的角度來看,好像是數據庫可以提供同一數據的多個版本,因此,這種技術叫做數據多版本並發控制(MultiVersion Concurrency Control,簡稱MVCC或MCC),也經常稱為多版本數據庫。

數據庫的事務隔離越嚴格,並發副作用越小,但付出的代價也就越大,因為事務隔離實質上就是使事務在一定程度上“串行化”進行,這顯然與“並發”是矛盾的。

隔離級別對比:

3.2 InnoDB鎖介紹

3.2.1 InnoDB的行鎖模式及加鎖方法:

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 SHAREMODE。排他鎖(X):SELECT * FROM table_name WHERE ... FOR UPDATE。用SELECT ... IN SHARE MODE獲得共享鎖,主要用在需要數據依存關系時來確認某行記錄是否存在,並確保沒有人對這個記錄進行UPDATE或者DELETE操作。但是如果當前事務也需要對該記錄進行更新操作,則很有可能造成死鎖,對於鎖定行記錄后需要進行更新操作的應用,應該使用SELECT... FOR UPDATE方式獲得排他鎖。

3.2.2  InnoDB行鎖實現方式:

InnoDB行鎖是通過給索引上的索引項加鎖來實現的,如果沒有索引,InnoDB將通過隱藏的聚簇索引來對記錄加鎖。InnoDB行鎖分為3種情形。

Record lock:對索引項加鎖。

Gap lock:對索引項之間的“間隙”、第一條記錄前的“間隙”或最后一條記錄后的“間隙”加鎖。

Next-key lock:前兩種的組合,對記錄及其前面的間隙加鎖。

InnoDB這種行鎖實現特點意味着:如果不通過索引條件檢索數據,那么InnoDB將對表中的所有記錄加鎖,實際效果跟表鎖一樣!有一下情況需要考慮:

1)在不通過索引條件查詢時,InnoDB會鎖定表中的所有記錄。

2)由於 MySQL 的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然是訪問不同行的記錄,但是如果是使用相同的索引鍵,是會出現鎖沖突的。應用設計的時候要注意這一點。

3)當表有多個索引的時候,不同的事務可以使用不同的索引鎖定不同的行,不論是使用主鍵索引、唯一索引或普通索引,InnoDB都會使用行鎖來對數據加鎖。

4)即便在條件中使用了索引字段,但是否使用索引來檢索數據是由MySQL通過判斷不同執行計划的代價來決定的,如果MySQL認為全表掃描效率更高,比如對一些很小的表,它就不會使用索引,這種情況下InnoDB也會對所有記錄加鎖。

 3.2.3 Next-Key鎖:

定義:當我們用范圍條件而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB 會給符合條件的已有數據記錄的索引項加鎖;對於鍵值在條件范圍內但並不存在的記錄,叫做“間隙(GAP)”,InnoDB也會對這個“間隙”加鎖,這種鎖機制就是所謂的Next-Key鎖。

如果使用了Next-Key鎖,就會對間隙進行加鎖,這將導致並發插入時嚴重的鎖等待

在使用范圍條件檢索並鎖定記錄時,InnoDB 這種加鎖機制會阻塞符合條件范圍內鍵值的並發插入,這往往會造成嚴重的鎖等待。因此,在實際應用開發中,尤其是並發插入比較多的應用,我們要盡量優化業務邏輯,盡量使用相等條件來訪問更新數據,避免使用范圍條件。

InnoDB使用Next-Key鎖的目的,一方面是為了防止幻讀,以滿足相關隔離級別的要求。另一方面,是為了滿足其恢復和復制的需要。

 3.2.4 恢復和復制的需要:

MySQL通過BINLOG記錄執行成功的INSERT、UPDATE、DELETE等更新數據的SQL語句,並由此實現MySQL數據庫的恢復和主從復制。MySQL 5.6支持 3種日志格式,即基於語句的日志格式SBL、基於行的日志格式RBL和混合格式。它還支持4種復制模式。

1 基於SQL語句的復制SBR:這也是MySQL最早支持的復制模式。

2 基於行數據的復制RBR:這是MySQL 5.1以后開始支持的復制模式,主要優點是支持對非安全SQL的復制。

3 混合復制模式:對安全的SQL語句采用基於SQL語句的復制模式,對於非安全的SQL語句采用居於行的復制模式。

4 使用全局事務ID(GTIDs)的復制:主要是解決主從自動同步一致問題。

3.2.5 InnoDB在不同隔離級別下的一致性讀及鎖的差異:

對於許多SQL,隔離級別越高,InnoDB給記錄集加的鎖就越嚴格(尤其是使用范圍條件的時候),產生鎖沖突的可能性也就越高,從而對並發性事務處理性能的影響也就越大。因此,我們在應用中,應該盡量使用較低的隔離級別,以減少鎖爭用的機率。實際上,通過優化事務邏輯,大部分應用使用Read Committed隔離級別就足夠了。對於一些確實需要更高隔離級別的事務,可以通過在程序中執行 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ或SET SESSION TRANSACTION ISOLATION LEVELSERIALIZABLE動態改變隔離級別的方式滿足需求。

3.2.6 什么時候使用表級鎖:

對於InnoDB表,在絕大部分情況下都應該使用行級鎖,因為事務和行鎖往往是我們選擇InnoDB表的理由。但在個別特殊事務中,也可以考慮使用表級鎖。

第一種情況是:事務需要更新大部分或全部數據,表又比較大,如果使用默認的行鎖,不僅這個事務執行效率低,而且可能造成其他事務長時間鎖等待和鎖沖突,這種情況下可以考慮使用表鎖來提高該事務的執行速度。

第二種情況是:事務涉及多個表,比較復雜,很可能引起死鎖,造成大量事務回滾。這種情況也可以考慮一次性鎖定事務涉及的表,從而避免死鎖,減少數據庫因事務回滾帶來的開銷。

3.2.7 死鎖:

MyISAM表鎖是 deadlock free的,這是因為MyISAM總是一次獲得所需的全部鎖,要么全部滿足,要么等待,因此不會出現死鎖。但在InnoDB中,除單個SQL組成的事務外,鎖是逐步獲得的,這就決定了在InnoDB中發生死鎖是可能的。

InnoDB死鎖如果不涉及表級鎖,一般可以自動檢測到,使其中一個事務釋放鎖並回退; 為了提高並發的效率,也可以設置鎖等待的超時時間

InnoDB發生死鎖后, 一般都能自動檢測到,並使一個事務釋放鎖並回退,另一個事務獲得鎖,繼續完成事務。但在涉及外部鎖或涉及表鎖的情況下,InnoDB 並不能完全自動檢測到死鎖,這需要通過設置鎖等待超時參數innodb_lock_wait_timeout來解決。需要說明的是,這個參數並不是只用來解決死鎖問題,在並發訪問比較高的情況下,如果大量事務因無法立即獲得所需的鎖而掛起,會占用大量計算機資源,造成嚴重性能問題,甚至拖垮數據庫。我們通過設置合適的鎖等待超時閾值,可以避免這種情況發生。通常來說,死鎖都是應用設計的問題,通過調整業務流程、數據庫對象設計、事務大小,以及訪問數據庫的 SQL 語句,絕大部分死鎖都可以避免。下面就通過實例來介紹幾種避免死鎖的常用方法。

避免死鎖的建議:

在了解InnoDB鎖特性后,用戶可以通過設計和SQL調整等措施減少鎖沖突和死鎖,包括以下幾項:

盡量使用較低的隔離級別;

精心設計索引,並盡量使用索引訪問數據,使加鎖更精確,從而減少鎖沖突的機會;

選擇合理的事務大小,小事務發生鎖沖突的幾率也更小;

給記錄集顯式加鎖時,最好一次性請求足夠級別的鎖。比如要修改數據,最好直接申請排他鎖,而不是先申請共享鎖,修改時再請求排他鎖,這樣容易產生死鎖;

不同的程序訪問一組表時,應盡量約定以相同的順序訪問各表,對一個表而言,盡可能以固定的順序存取表中的行。這樣可以大大減少死鎖的機會;

盡量用相等條件訪問數據,這樣可以避免Next-Key鎖對並發插入的影響;

不要申請超過實際需要的鎖級別;除非必須,查詢時不要顯示加鎖;

對於一些特定的事務,可以使用表鎖來提高處理速度或減少發生死鎖的幾率。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM