MySQL 事務與鎖機制

  下表展示了本人安裝的MariaDB(10.1.19,MySQL的分支)所支持的所有存儲引擎概況,其中支持事務的有InnoDB、SEQUENCE,另外InnoDB還支持XA事務,MyISAM不支持事務。鎖可以通過SQL語句(如 LOCK TABLES )顯式申請,也可以由InnoDB引擎自動爲你獲取。下文將討論InnoDB和MyISAM在事務與鎖定方面的相關話題

ENGINESUPPORTCOMMENTTRANSACTIONSXASAVEPOINTS
CSVYESCSV storage engineNONONO
MyISAMYESMyISAM storage engineNONONO
MRG_MyISAMYESCollection of identical MyISAM tablesNONONO
AriaYESCrash-safe tables with MyISAM heritageNONONO
MEMORYYESHash based, stored in memory, useful for temporary...NONONO
PERFORMANCE_SCHEMAYESPerformance SchemaNONONO
SEQUENCEYESGenerated tables filled with sequential valuesYESNOYES
InnoDBDEFAULTPercona-XtraDB, Supports transactions, row-level l...YESYESYES

 

一. 事務四要素

  數據庫事務正確執行的四個基本要素包括原子性(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中與鎖定有關的幾個重要概念:

  • 非鎖定讀:InnoDB對無格式SELECT執行非鎖定讀,但在不同事務隔離級別條件下表現有所不同。
  • Shared (S) Locks 與 Exclusive (X) Locks:行級鎖定事實上是索引記錄鎖定。

    (1)一個共享鎖S允許事務獲取鎖來讀取一行(READ)。如果事務T1持有對行 r 的 S 鎖, 那麼另外一個事務T2對行 r 的鎖需求會如下k處理,T2對S鎖的請求會被馬上授權,因此T1、T2都對r同時分別持有一個共享鎖。但T2對r的X鎖請求不會被馬上授權,這看上去似乎READ優先於WRITE,甚至WRITE請求被餓死;
    (2)一個排他鎖X允許事務獲取鎖來更新或刪除一行(WRITE)。如果事務T1持有一個r的X鎖, 那麼T2對r的任何鎖類型都無法被馬上授權。

    髒讀:事務A更新了某個數據項D,由於某種錯誤事件發生導致事務A回滾;但在回滾之前,另一個事務B讀取了數據項D的值,
    A回滾後數據項D恢復到原值,此時事務B讀取的數據項D值稱爲髒數據。造成髒讀的原因是事務A、B缺少隔離性。

     

  • 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鎖定Next-Key鎖定匹配的行(Record Lock)以及相應的缺口(Gap Lock),以防止所謂的“幽靈行問題”。
    假設你想要從有一個標識符值大於100的child讀並鎖定所有某些記錄,並想着隨後在選定行中更新一些列:
    
    SELECT * FROM child WHERE id > 100 FOR UPDATE;
    
    假設在id列有一個索引,查詢從id大於100的第一個記錄開始掃描。如果設置在索引記錄上的鎖定不把在間隙生成的插入排除在外,
    一個新行可能與此同時被插進表中。如果你在同一事務內執行同樣的SELECT,你可能會在該查詢返回的結果包裏看到一個新行。
    這與事務的隔離原則是相反的:一個事務已經讀的數據在事務過程中不被其他事務改變。如果我們把結果集視爲一個數據項,
    新的“幽靈”記錄出現會違反這一隔離原則。所以該例中,InnoDB設置的鎖定會阻止child表中出現id大於100的記錄
  • Insert Intention Locks:插入意向鎖是一種Gap Lock類型,多個事務同時往同一個Gap插入數據時,只要不在同一位置插入就不衝突。插入意向鎖是先於(插入行)排他鎖而設置的。
  • 事務隔離級別:MySQL/InnoDB 提供SQL標準所描述的所有四個事務隔離級別,默認是可重複讀的(REPEATABLE READ)。你可以在命令行用--transaction-isolation選項或在選項文件裏,爲所有連接設置隔離級別,或通過 SET TRANSACTION
    SET [GLOBAL | SESSION] TRANSACTION
        transaction_characteristic [, transaction_characteristic] ...
    
    transaction_characteristic:
        ISOLATION LEVEL level
      | READ WRITE
      | READ ONLY
    
    level:
         REPEATABLE READ
       | READ COMMITTED
       | READ UNCOMMITTED
       | SERIALIZABLE

     

    下面將分別介紹這四個事務隔離級別:https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html
    READ UNCOMMITTED
    每個無格式SELECT都是非鎖定非一致讀,可能讀到其他失敗事務曾經修改過但尚未完成回滾的數據,也被稱爲“髒讀”(Dirty Read);除此而外,其他約束與READ COMMITTED一樣作用。
    
    2· READ COMMITTED
    每個無格式SELECT都是非鎖定一致讀,擁有自己快照。SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE和DELETE僅鎖定索引記錄(使用Record Lock),而不鎖定記錄前的間隙(不使用Gap Lock),因而允許其他事務緊挨着已鎖定的記錄插入新記錄。對於 UPDATE 或 DELETE,InnoDB先鎖定那些未被鎖定的行,然後對不滿足WHERE條件的行釋放鎖,可以減少死鎖發生的可能性;另外對於UPDATE,如果一個行已經被鎖定,InnoDB先執行“半一致讀”獲取最新提交版本值,如果該值滿足WHERE條件,InnoDB將再執行一次讀並獲取或等待鎖定。 REPEATABLE READ
    每個無格式SELECT都是非鎖定一致讀,相同讀共享同一快照(由第一次讀所確定的快照)。SELECT ... FOR UPDATE, SELECT ... LOCK IN SHARE MODE, UPDATE 和DELETE使用Record Lock或Gap Lock(Next-Key Lock)。SELECT可看到發佈該SELECT的事務本身所做的改變。UPDATE 鎖住所有可鎖定的行(包括那些並不滿足WHERE條件的行),直到提交或回滾後再釋放鎖。
    
    4· SERIALIZABLE
    這個級別類似REPEATABLE READ。但在autocommit未開啓情形下,無格式SELECT語句將被隱式轉換成SELECT ... LOCK IN SHARE MODE;而在autocommit開啓情形下,無格式SELECT語句是非鎖定讀且可串行化的。

     從上可知,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存儲引擎,取決於主要的操作類型。如果是讀多寫少, 使用MyISAM會更佳,且MyISAM支持併發select和insert。存儲引擎使用表級鎖定不會發生死鎖,因爲所有必要的鎖定在查詢開始時就能以同樣的順序確定下來。

  可以通過檢查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支持事務以及外鍵約束。

 

七. 悲觀鎖與樂觀鎖

  悲觀鎖和樂觀鎖不是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是否被修改)

    有一種樂觀鎖是通過CAS(Compare and Swap)實現的,CAS本身是原子操作。CAS有3個操作數,目標屬性V,舊的預期值A,要修改的新值B,當且僅當預期值A和當前V相同時,將V的值修改爲B,否則失敗重試。現代的CPU提供了特殊的指令,允許算法執行讀-修改-寫操作,而無需害怕其他線程同時修改變量,因爲如果其他線程修改變量,那麼CAS會檢測它並失敗,算法可以對該操作重新計算。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章