MySQL 事務與鎖機制
下表展示了本人安裝的MariaDB(10.1.19,MySQL的分支)所支持的所有存儲引擎概況,其中支持事務的有InnoDB、SEQUENCE,另外InnoDB還支持XA事務,MyISAM不支持事務。鎖可以通過SQL語句(如 LOCK TABLES )顯式申請,也可以由InnoDB引擎自動為你獲取。下文將討論InnoDB和MyISAM在事務與鎖定方面的相關話題
ENGINE | SUPPORT | COMMENT | TRANSACTIONS | XA | SAVEPOINTS |
---|---|---|---|---|---|
CSV | YES | CSV storage engine | NO | NO | NO |
MyISAM | YES | MyISAM storage engine | NO | NO | NO |
MRG_MyISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
Aria | YES | Crash-safe tables with MyISAM heritage | NO | NO | NO |
MEMORY | YES | Hash based, stored in memory, useful for temporary... | NO | NO | NO |
PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO |
SEQUENCE | YES | Generated tables filled with sequential values | YES | NO | YES |
InnoDB | DEFAULT | Percona-XtraDB, Supports transactions, row-level l... | YES | YES | YES |
一. 事務四要素
數據庫事務正確執行的四個基本要素包括原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability),簡稱ACID。目前要實現ACID主要有兩種方式:一種是Write ahead logging,也就是日誌式的方式(現代數據庫均基於這種方式);另一種是Shadow paging。
原子性:整個事務中的所有操作,要麽全部完成,要麽全部不完成,不可能停滯在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態
一致性:事務執行前與執行後都必須始終保持系統處於一致的狀態
隔離性
持久性:在事務完成以後,該事務對數據庫所作的更改便持久的保存在數據庫之中
二. MyISAM表鎖定
所有用戶行為最後執行的基本單位是單條操作命令(SELECT、INSERT、UPDATE、DELETE等),它們都是原子操作。按照事務術語,MyISAM表總能高效地工作在AUTOCOMMIT=1模式下,原子操作通常能提供可比較的完整性以及更好的性能。這意味著,你能確信在每個特性更新運行的同時,其他用戶不能幹涉它,而且不會出現自動回滾(如果你不小心,對於事務性表,這種情況可能發生),MySQL服務器還能保證不存在臟讀。
一般而言,所有由事務解決的完整性問題均能用LOCK TABLES或原子更新解決,從而確保了服務器不會自動中斷,後者是事務性數據庫系統的常見問題。MyISAM只支持表級鎖定,允許多重讀或一次寫。LOCK TABLES可以鎖定用於當前線程的表,如果表被其它線程鎖定,則造成堵塞,直到可以獲取所有鎖定為止。UNLOCK TABLES可以釋放被當前線程保持的任何鎖定。當線程發布另一個LOCK TABLES時,或當與服務器的連接被關閉時,所有由當前線程鎖定的表被隱含地解鎖。如果您想要確保在同一業務流中的SELECT和UPDATE之間沒有其它線程訪問修改數據,就必須使用LOCK TABLES。如果你獲得了對某一表的READ LOCAL鎖定(與寫鎖定相對),該表允許在表尾執行並行插入,當其他客戶端執行插入操作時,允許執行讀操作。新插入的記錄不會被有讀鎖定屬性的客戶端看到,直至解除了該鎖定為止。使用INSERT DELAYED,能夠將插入項置於本地隊列中,直至鎖定解除,不會讓客戶端等待插入完成。
三. InnoDB事務與鎖定
在 InnoDB 中,所有用戶行為都在事務內發生,事務是其執行的基本單位,默認運行查詢為非鎖定持續讀。如果自動提交模式被允許,即 AUTOCOMMIT = 1,每個SQL語句都將以一個單獨的事務來運行;如果自動提交模式被用 SET AUTOCOMMIT = 0 關閉,那麽我們可以認為一個用戶總是有一個事務打開著,一個SQL COMMIT或ROLLBACK語句結束當前事務並且一個新事務開始,兩個語句都釋放所有在當前事務中被設置的InnoDB鎖定。
InnoDB除了表級鎖定之外,還支持更細粒度的行級鎖定,且行鎖和表鎖多重粒度可共存。下面是InnoDB中與鎖定有關的幾個重要概念:
持續非鎖定讀:無格式SELECT語句默認模式,InnoDB使用它的多版本化來給一個查詢展示某個時間點處數據庫的快照。查詢看到在那個時間點之前被提交的那些確切事務做的更改,並且沒有其後的事務或未提交事務做的改變。這個規則的例外是,查詢看到發布該查詢的事務本身所做的改變,但持續讀不在DROP TABLE和ALTER TABLE上作用。持續讀不在任何它訪問的表上設置鎖定,因此,其它用戶可自由地在持續讀在一個表上執行的同一時間修改這些表。如果你運行在默認的REPEATABLE READ隔離級別,則在同一事務內的所有持續讀讀取由該事務中第一個這樣的讀所確立的快照。你可以通過提交當前事務並在發布新查詢的事務之後,為你的查詢獲得一個更新鮮的快照
Shared (S) Locks:一個共享鎖允許事務獲取鎖來讀取一行(READ)。如果事務T1持有對行 r 的 S 鎖, 那麽另外一個事務T2對行 r 的鎖需求會如下處理,T2對S鎖的請求會被馬上授權,因此T1、T2都對r同時分別持有一個共享鎖。但T2對r的X鎖請求不會被馬上授權,這看上去似乎READ優先於WRITE,以致WRITE請求被餓死
Exclusive (X) Locks:一個排他鎖允許事務獲取鎖來更新或刪除一行(WRITE)。如果事務T1持有一個r的X鎖, 那麽T2對r的任何鎖類型都無法被馬上授權
Intention Locks:意向鎖在InnoDB中是表鎖,他表明S或X鎖將會在一個事務中對某一行使用。Intention shared (IS) 表明事務T打算設置S鎖到表t上,Intention exclusive (IX) 表明事務T打算設置X鎖到行上。意向鎖協議如下:
1. 在一個事務獲取表t的某行的S鎖之前, 他必須獲取表t的一個IS鎖或更強的鎖 2. 在一個事務獲取表t某行的X鎖之前, 他必須獲取一個t的IX鎖 3. 這些規則可以總結為如下鎖類型兼容矩陣: X IX S IS X Conflict Conflict Conflict Conflict IX Conflict Compatible Conflict Compatible S Conflict Conflict Compatible Compatible IS Conflict Compatible Compatible Compatible 4. 一個鎖如果和已經存在的鎖兼容, 就可以授權給請求他的事務, 但如果和已存在的鎖不兼容則不行
一個事務必須等待直到沖突的鎖被釋放。如果一個鎖請求和一個已經存在的鎖沖突, 並且一直不能被授權, 就會造成死鎖。一旦死鎖發生,InnoDB會選擇其中一個報錯並釋放其持有的鎖,直至解除死鎖。意向鎖並不會阻塞任何事情,除非是對全表的請求(例如, LOCK TABLES ... WRITE), IX和IS鎖的主要目的是表示有人正在或者準備鎖定一行
Next-Key鎖定:InnoDB執行行級鎖定將搜索或掃描表的索引,對遇到的索引記錄設置共享或獨占鎖定。因此,Next-Key鎖定是一種執行行鎖定的方式,行級鎖定事實上是索引記錄鎖定。如果一個用戶對一個索引上的記錄R有共享或獨占的鎖定,另一個用戶 不能緊接在R之前以索引的順序插入一個新索引記錄,Next-Key鎖定這個間隙以防止所謂的“幽靈問題”
假設你想要從有一個標識符值大於100的child讀並鎖定所有某些記錄,並想著隨後在選定行中更新一些列: SELECT * FROM child WHERE id > 100 FOR UPDATE; 假設在id列有一個索引,查詢從id大於100的第一個記錄開始掃描。如果設置在索引記錄上的鎖定不把在間隙生成的插入排除在外,一個新行可能與此同時被插進表中。如果你在同一事務內執行同樣的SELECT,你可能會在該查詢返回的結果包裏看到一個新行。這與事務的隔離原則是相反的:一個事務應該能夠運行,以便它已經讀的數據在事務過程中不改變。如果我們把結果集視為一個數據項,新的“幽靈”記錄出現會違反這一隔離原則。 當InnoDB掃描一個索引之時,它也鎖定範圍中最後一個記錄之後的間隙。剛才前一個例子中:InnoDB設置的鎖定防止任何表中出現id大過100的記錄
事務隔離級別:MySQL/InnoDB 提供SQL標準所描述的所有四個事務隔離級別,默認是可重復讀的(REPEATABLE READ)。你可以在命令行用--transaction-isolation選項,或在選項文件裏,為所有連接設置 默認隔離級別。下面將分別介紹這四個事務隔離級別
· READ UNCOMMITTED SELECT語句以非鎖定方式被執行,但是一個可能更早期版本的記錄會被用到,讀是不連貫的,也被稱為“臟讀”(dirty read)。另外,這個隔離級別象READ COMMITTED一樣作用。 · READ COMMITTED 所有SELECT ... FOR UPDATE和SELECT ... LOCK IN SHARE MOD語句僅鎖定索引記錄,而不鎖定記錄前的間隙,因而允許隨意緊挨著已鎖定的記錄插入新記錄。UPDATE和DELETE語句使用一個帶唯一搜索條件的唯一的索引僅鎖定找到的索引記錄,而不包括記錄前的間隙。在範圍類型UPDATE和DELETE語句,InnoDB必須對範圍覆蓋的間隙設置next-key鎖定或間隙鎖定其它用戶做的塊插入,因為要讓MySQL復制和恢復起作用,“幽靈行”必須被阻止掉。 · REPEATABLE READ 這是InnoDB的默認隔離級別。帶唯一搜索條件使用唯一索引的SELECT ... FOR UPDATE, SELECT ... LOCK IN SHARE MODE, UPDATE 和DELETE語句只鎖定找到的索引記錄,而不鎖定記錄前的間隙。用其它搜索條件,這些操作采用next-key鎖定,用next-key鎖定或者間隙鎖定鎖住搜索的索引範圍,並且阻止其它用戶的新插入。在持續讀中,有一個與之前隔離級別重要的差別:在這個級別,在同一事務內所有持續讀讀取由第一次讀所確定的同一快照。這個慣例意味著如果你在同一事務內發出數個無格式SELECT語句,這些SELECT語句對相互之間也是持續的。 · SERIALIZABLE 這個級別類似REPEATABLE READ,但是所有無格式SELECT語句被 隱式轉換成SELECT ... LOCK IN SHARE MODE。
從上可知,READ UNCOMMITTED隔離約束最弱,SERIALIZABLE隔離約束最強,隔離約束逐級提高。
四. 行鎖與表鎖優劣對比
行級鎖定的優點:
當在許多線程中訪問不同的行時只存在少量鎖定沖突
回滾時只有少量的更改
可以長時間鎖定單一的行
行級鎖定的缺點:
比頁級或表級鎖定占用更多的內存
當在表的大部分中使用時,比頁級或表級鎖定速度慢,因為你必須獲取更多的鎖
如果你在大部分數據上經常進行GROUP BY操作或者必須經常掃描整個表,比其它鎖定明顯慢很多
用高級別鎖定,通過支持不同的類型鎖定,你也可以很容易地調節應用程序,因為其鎖成本小於行級鎖定
MySQL表鎖定機制:當一個鎖定被釋放時,鎖定可被寫鎖定隊列中的線程得到,然後是讀鎖定隊列中的線程。這意味著,如果你在一個表上有許多更新,SELECT語句將等待直到沒有更多的更新。表更新通常情況認為比表檢索更重要,因此給予它們更高的優先級,但這應確保更新一個表的活動不能“餓死”,即使該表上有很繁重的SELECT活動
對WRITE,MySQL使用的表鎖定方法原理如下:
如果在表上沒有鎖,在它上面放一個寫鎖
否則,把鎖定請求放在寫鎖定隊列中
對READ,MySQL使用的表鎖定方法原理如下:
如果在表上沒有寫鎖定,把一個讀鎖定放在它上面
否則,把鎖請求放在讀鎖定隊列中
在以下情況下,表鎖定優於行級鎖定:
表的大部分語句用於讀取
對嚴格的unique_key進行讀取和更新,你可以更新或刪除可以用單一的讀取的關鍵字來提取的一行:
UPDATE tbl_name SET column=value WHERE unique_key_col=key_value; DELETE FROM tbl_name WHERE unique_key_col=key_value;
SELECT 結合並行的INSERT語句,並且只有很少的UPDATE或DELETE語句
在整個表上有許多掃描或GROUP BY操作,沒有任何寫操作
對於大表,對於大多數應用程序,表鎖定比行鎖定更好,但存在部分缺陷
表鎖定的有關事項:
使用SET LOW_PRIORITY_UPDATES=1語句可指定具體連接中的所有更新應使用低優先級;
或用LOW_PRIORITY屬性給與一個特定的INSERT、UPDATE或DELETE語句較低優先級;
或用HIGH_PRIORITY屬性給與一個特定的SELECT語句較高優先級;
或為max_write_lock_count系統變量指定一個低值來啟動mysqld來強制MySQL在具體數量的插入完成後臨時提高所有等待一個表的SELECT語句的優先級;
或用--low-priority-updates啟動mysqld,這將給所有更新(修改)一個表的語句以比SELECT語句低的優先級如果你有關於INSERT結合SELECT的問題,切換到使用新的MyISAM表,因為它們支持並發的SELECT和INSERT;
如果你對同一個表混合插入和刪除,INSERT DELAYED將會有很大的幫助;
如果你對同一個表混合使用SELECT和DELETE語句出現問題,DELETE的LIMIT選項可以有所幫助;
對SELECT語句使用SQL_BUFFER_RESULT可以幫助使表鎖定時間變短;
可以更改mysys/thr_lock.c中的鎖代碼以使用單一的隊列,在這種情況下,寫鎖定和讀鎖定將具有相同的優先級;
如果不混合更新與需要在同一個表中檢查許多行的選擇,可以進行並行操作;
可以使用LOCK TABLES來提高速度,因為在一個鎖定中進行許多更新比沒有鎖定的更新要快得多,另外將表中的內容切分為幾個表也可以有所幫助
五. 選擇MyISAM
一般而言,若要使用MyISAM存儲引擎,應看看應用程序做什麽並且混合使用什麽樣的讀和寫。例如,大多數Web應用程序執行許多select,而很少進行delete,只對key的值進行更新,並且只插入少量行,此時使用MyISAM會更佳,而且MyISAM支持並發select和insert。在MySQL中對於使用表級鎖定的存儲引擎,表鎖定時不會死鎖的,這通過總是在一個查詢開始時立即請求所有必要的鎖定並且總是以同樣的順序鎖定表來管理。
可以通過檢查table_locks_waited和table_locks_immediate狀態變量來分析系統上的表鎖定爭奪:
mysql> SHOW STATUS LIKE ‘Table%‘; +-----------------------+---------+ | Variable_name | Value | +-----------------------+---------+ | Table_locks_immediate | 1151552 | | Table_locks_waited | 15324 | +-----------------------+---------+
如果數據文件不包含空塊(從表的中部刪除或更新的行可能導致空洞),INSERT語句不沖突,可以自由為MyISAM表混合並行的INSERT和SELECT語句而不需要鎖定,你可以在其它客戶正讀取MyISAM表的時候插入行,記錄總是插入在數據文件的尾部;如果不能同時插入,為了在一個表中進行多次INSERT和SELECT操作,可以在臨時表中插入行並且立即用臨時表中的記錄更新真正的表,這也適合做批量延遲插入:
mysql> LOCK TABLES real_table WRITE, insert_table WRITE; mysql> INSERT INTO real_table SELECT * FROM insert_table; mysql> TRUNCATE TABLE insert_table; mysql> UNLOCK TABLES;
六. 選擇InnoDB
InnoDB使用行鎖定,可能存在死鎖。這是因為,在SQL語句處理期間,InnoDB自動獲得行鎖定,而不是在事務啟動時獲得。對於InnoDB和BDB(BerkeleyDB)表,如果你用LOCK TABLES顯式鎖定表,MySQL只使用表鎖定,建議不要使用LOCK TABLES,因為InnoDB使用自動行級鎖定而BDB使用頁級鎖定來保證事務隔離。
InnoDB支持外鍵約束。
一般說來,以多讀為主也附帶少量寫首選MyISAM,否則選擇InnoDB或其他引擎會更佳。
七. 悲觀鎖與樂觀鎖
悲觀鎖和樂觀鎖不是MySQL數據庫中的標準概念,而只是一種通俗說法。
悲觀鎖:悲觀鎖指對數據被意外修改持保守態度,依賴數據庫原生支持的鎖機制來保證當前事務處理的安全性,防止其他並發事務對目標數據的破壞或破壞其他並發事務數據,將在事務開始執行前或執行中申請鎖定,執行完後再釋放鎖定。這對於長事務來講,可能會嚴重影響系統的並發處理能力
LOCK TABLES a WRITE; INSERT INTO a VALUES (1,23),(2,34),(4,33); INSERT INTO a VALUES (8,26),(6,29); UNLOCK TABLES;
鎖定表可以加速用多個語句執行的INSERT操作,因為索引緩存區僅在所有INSERT語句完成後刷新到磁盤上一次。一般有多少INSERT語句即有多少索引緩存區刷新,如果能用一個語句插入所有的行,就不需要鎖定;對於事務表,應使用BEGIN和COMMIT代替LOCK TABLES來加快插入
樂觀鎖:樂觀鎖相對悲觀鎖而言,先假想數據不會被並發操作修改,沒有數據沖突,只在數據進行提交更新的時候,才會正式對數據的沖突與否進行檢測,如果發現沖突了,則宣告失敗,否則更新數據。這就要求避免使用長事務和鎖機制,以免導致系統並發處理能力降低,保障系統生產效率。下面將說明使用樂觀鎖時的大致業務處理流程
首 步:執行一次查詢 select some_column as old_value from some_table where id = id_value (假設該值在當前業務處理過程中不會被其他並發事務修改) ... 第n步:old_value參與中間業務處理,比如old_value被自己修改 new_value = f(old_value)。這期間可能耗時很長,但不會為持有 some_column 而申請所在的行或表鎖定,因此其他並發事務可以獲得該鎖 ... 尾 步:執行條件更新 update some_table set some_column = new_value where id = id_value and some_column = old_value (條件更新中檢查old_value是否被修改)
MySQL 事務與鎖機制