MySQL事務處理與鎖機制詳解


MySQL事務處理

事務的基本概念

事務 是數據庫管理系統執行過程中的一個邏輯單位,由一個有限的數據庫操作序列構成。

MySQL 事務主要用於處理操作量大,複雜度高的數據。比如說,在學生管理系統中,你刪除一個學生,你既需要刪除學生的基本資料,也要刪除和該學生相關的選課信息,成績信息等等,這樣,這些數據庫操作語句就構成一個事務,通常一個事務對應一個完整的業務

  • 在 MySQL 中只有使用了 Innodb 數據庫引擎的數據庫或表才支持事務
  • 事務處理可以用來維護數據庫的完整性,保證成批的 SQL 語句要麼全部執行,要麼全部不執行。
  • 一個完整的業務需要批量的DML(insert、update、delete)語句共同聯合完成
  • 事務只和DML語句有關,或者說DML語句纔有事務。這個和業務邏輯有關,業務邏輯不同,DML語句的個數不同

事務的ACID特性

事務的ACID特性指的是事務的原子性、一致性、隔離性和持久性。


原子性(Atomicity)

原子性是指事務包含的所有操作要麼全部成功,要麼全部失敗回滾,因此事務的操作如果成功就必須要完全應用到數據庫,如果操作失敗則不能對數據庫有任何影響。


一致性(Consistency)

一致性是指事務必須使數據庫從一個一致性狀態變換到另一個一致性狀態,也就是說一個事務執行之前和執行之後都必須處於一致性狀態。

以轉賬來說,假設用戶A和用戶B兩者的錢加起來一共是5000,那麼不管A和B之間如何轉賬,轉幾次賬,事務結束後兩個用戶的錢相加起來應該還得是5000,這就是事務的一致性。

有關數據不一致情況:髒讀、不可重複讀和幻讀(虛讀) 等情況,稍後會介紹到。


隔離性(Isolation)

隔離性是當多個用戶併發訪問數據庫時,比如操作同一張表時,數據庫爲每一個用戶開啓的事務,不能被其他事務的操作所幹擾,多個併發事務之間要相互隔離

即要達到這麼一種效果:對於任意兩個併發的事務T1和T2,在事務T1看來,T2要麼在T1開始之前就已經結束,要麼在T1結束之後纔開始,這樣每個事務都感覺不到有其他事務在併發地執行

關於事務的隔離性數據庫提供了多種隔離級別:未提交讀、已提交讀、可重複讀、串行化,稍後會介紹到。


持久性(Durability)

持久性是指一個事務一旦被提交了,那麼對數據庫中的數據的改變就是永久性的,即便是在數據庫系統遇到故障的情況下也不會丟失提交事務的操作


以上介紹完事務的四大特性(簡稱ACID),現在重點來說明下事務的隔離性,當多個線程都開啓事務操作數據庫中的數據時,數據庫系統要能進行隔離操作,以保證各個線程獲取數據的準確性,在介紹數據庫提供的各種隔離級別之前,我們先看看如果不考慮事務的隔離性,會發生的幾種問題。


數據不一致問題

數據不一致問題有三種:髒讀、不可重複讀 和 幻讀(虛讀)

髒讀

髒讀是指在一個事務處理過程裏讀取了另一個未提交的事務中的數據

當一個事務正在多次修改某個數據,而在這個事務中這多次的修改都還未提交,這時一個併發的事務來訪問該數據,就會造成兩個事務得到的數據不一致。

例如:用戶A向用戶B轉賬100元,對應SQL命令如下

update account set money=money+100 where name=’B’;  (此時A通知B)
update account set money=money - 100 where name=’A’;

當只執行第一條SQL時,A通知B查看賬戶,B發現確實錢已到賬(此時即發生了髒讀),而之後無論第二條SQL是否執行,只要該事務不提交,則所有操作都將回滾,那麼當B以後再次查看賬戶時就會發現錢其實並沒有轉。


不可重複讀

不可重複讀是指在對於數據庫中的某個數據,一個事務範圍內多次查詢卻返回了不同的數據值,這是由於在查詢間隔,被另一個事務修改並提交了。

例如事務T1在讀取某一數據,而事務T2立馬修改了這個數據並且提交事務給數據庫,事務T1再次讀取該數據就得到了不同的結果,發送了不可重複讀。

不可重複讀和髒讀的區別是:髒讀是某一事務讀取了另一個事務未提交的髒數據,而不可重複讀則是讀取了前一事務提交的數據

在某些情況下,不可重複讀並不是問題,比如我們多次查詢某個數據當然以最後查詢得到的結果爲主。但在另一些情況下就有可能發生問題。


虛讀(幻讀)

幻讀是事務非獨立執行時發生的一種現象。

例如事務T1對一個表中所有的行的某個數據項做了從“1”修改爲“2”的操作,這時事務T2又對這個表中插入了一行數據項,而這個數據項的數值還是爲“1”並且提交給數據庫。而操作事務T1的用戶如果再查看剛剛修改的數據,會發現還有一行沒有修改,其實這行是從事務T2中添加的,就好像產生幻覺一樣,這就是發生了幻讀。

幻讀和不可重複讀都是讀取了另一條已經提交的事務(這點就和髒讀不同),所不同的是不可重複讀查詢的都是同一個數據項,而幻讀針對的是一批數據整體(比如數據的個數)。


事務的隔離級別

針對可能的問題,InnoDB 提供了四種不同級別的機制保證數據隔離性:未提交讀、已提交讀、可重複讀、串行化

  • Read uncommitted (未提交讀):最低級別,任何情況都無法保證。
  • Read committed (已提交讀):可避免髒讀的發生。
  • Repeatable read (可重複讀):可避免髒讀、不可重複讀的發生。
  • Serializable (串行化):可避免髒讀、不可重複讀、幻讀的發生。

下圖簡單的分析的不同隔離級別與數據不一致情況的對應關係:

以上四種隔離級別最高的是Serializable串行化級別,最低的是未提交讀級別

當然級別越高,執行效率就越低。像Serializable這樣的級別,就是以鎖表的方式,使得其他的線程只能在鎖外等待,所以平時選用何種隔離級別應該根據實際情況

在MySQL數據庫中默認的隔離級別爲Repeatable read (可重複讀)。

在MySQL 8.0以下版本使用下述命令查看當前會話的數據庫隔離級別:

SELECT @@tx_isolation;

在MySQL 8.0及其以上版本使用下述命令查看當前會話的數據庫隔離級別:

SELECT @@global.transaction_isolation;

在MySQL數據庫中,支持上面四種隔離級別,默認的爲Repeatable read (可重複讀);而在Oracle數據庫中,只支持Serializable (串行化)級別和Read committed (讀已提交)這兩種級別,其中默認的爲Read committed級別。

在MySQL數據庫中設置事務的隔離級別:

set  [glogal | session]  transaction isolation level 隔離級別名稱;

set transaction_isolation=’隔離級別名稱;

注意:

  • 設置數據庫的隔離級別一定要是在開啓事務之前
  • 隔離級別的設置只對當前鏈接有效。對於使用MySQL命令窗口而言,一個窗口就相當於一個鏈接,當前窗口設置的隔離級別只對當前窗口中的事務有效

事務的提交與回滾

  • 任何一條DML語句(insert、update、delete)執行,標誌事務的開啓
  • 提交:成功的結束,將所有的DML語句操作歷史記錄和內存數據進行同步
  • 回滾:失敗的結束,將所有的DML語句操作歷史記錄全部清空

在事務進行過程中,未結束之前,DML語句是不會更改底層數據,只是將歷史操作記錄一下,在內存中完成記錄。只有在事物結束的時候,而且是成功的結束的時候,纔會修改底層硬盤文件中的數據。

在MySQL中,默認情況下,事務是自動提交的,也就是說,只要執行一條DML語句就開啓了事物,並且提交了事務。自動提交機制是可以關閉的。

將自動提交功能置爲ON:

SET AUTOCOMMIT=0;

將自動提交功能置爲OFF:

SET AUTOCOMMIT=1;

但是如果存儲引擎爲 InnoDB 時,當執行了START TRANSACTION或BEGIN命令後,將不會自動提交了,只有明確執行了COMMIT命令後纔會被提交,在這之前可以執行ROLLBACK 命令回滾更新操作。

接下來我們使用begin開啓事務,並向student表中插入一條記錄,但是沒有commit提交,而是rollback回滾了,我們可以看到新紀錄並沒有插入到表中

接下來我們使用START TRANSACTION開啓事務,和上面一樣插入新紀錄,但是這次我們使用commit提交事務,可以看到表中記錄被更新了。


事務的隔離性是通過鎖機制實現的,不同於MyISAM使用表級別的鎖,InnoDB採用更細粒度的行級別鎖,提高了數據表的性能。InnoDB的鎖通過鎖定索引來實現,如果查詢條件中有主鍵則鎖定主鍵,如果有索引則先鎖定對應索引然後再鎖定對應的主鍵(可能造成死鎖),如果連索引都沒有則會鎖定整個數據表。

接下來我們簡單介紹MySQL的鎖機制。


MySQL的鎖機制

鎖是計算機協調多個進程或線程併發訪問某一資源的機制。在數據庫中,除傳統的計算資源(如CPU、RAM、I/O等)的爭用以外,數據也是一種供許多用戶共享的資源。如何保證數據併發訪問的一致性、有效性是所有數據庫必須解決的一個問題,鎖衝突也是影響數據庫併發訪問性能的一個重要因素。從這個角度來說,鎖對數據庫而言顯得尤其重要,也更加複雜

Mysql用到了很多這種鎖機制,比如行鎖,表鎖,讀鎖,寫鎖等,都是在做操作之前先上鎖。這些鎖統稱爲悲觀鎖(Pessimistic Lock)。


相對其他數據庫而言,MySQL的鎖機制比較簡單,其最顯著的特點是不同的存儲引擎支持不同的鎖機制。比如:

  • MyISAM 和 MEMORY存儲引擎採用的是表級鎖(table-level locking)
  • BDB存儲引擎採用的是頁面鎖(page-level locking),但也支持表級鎖
  • InnoDB存儲引擎既支持行級鎖(row-level locking),也支持表級鎖,但默認情況下是採用行級鎖

這些鎖的區別如下:

  • 表級鎖:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖衝突的概率最高,併發度最低。
  • 行級鎖:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖衝突的概率最低,併發度也最高。
  • 頁面鎖:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,併發度一般

從上述特點可見,很難籠統地說哪種鎖更好,只能就具體應用的特點來說哪種鎖更合適!僅從鎖的角度來說:

  • 表級鎖更適合於以查詢爲主,只有少量按索引條件更新數據的應用,如Web應用
  • 行級鎖則更適合於有大量按索引條件併發更新少量不同數據,同時又有併發查詢的應用,如一些在線事務處理(OLTP)系統

MyISAM表鎖

MySQL的表級鎖有兩種模式:

  • 表共享讀鎖(Table Read Lock)
  • 表獨佔寫鎖(Table Write Lock)

對MyISAM表的讀操作,不會阻塞其他用戶對同一表的讀請求,但會阻塞對同一表的寫請求。

對MyISAM表的寫操作,則會阻塞其他用戶對同一表的讀和寫操作。MyISAM表的讀操作與寫操作之間,以及寫操作之間是串行的。當一個線程獲得對一個表的寫鎖後,只有持有鎖的線程可以對錶進行更新操作。其他線程的讀、寫操作都會等待,直到鎖被釋放爲止。


MyISAM的鎖調度

MyISAM存儲引擎的讀鎖和寫鎖是互斥的,讀寫操作是串行的

那麼,一個進程請求某個 MyISAM表的讀鎖,同時另一個進程也請求同一表的寫鎖,MySQL如何處理呢?

答案是寫進程先獲得鎖。不僅如此,即使讀請求先到鎖等待隊列,寫請求後 到,寫鎖也會插到讀鎖請求之前!這是因爲MySQL認爲寫請求一般比讀請求要重要。這也正是MyISAM表不太適合於有大量更新操作和查詢操作應用的原因,因爲,大量的更新操作會造成查詢操作很難獲得讀鎖,從而可能永遠阻塞。這種情況有時可能會變得非常糟糕!幸好我們可以通過一些設置來調節MyISAM 的調度行爲。

  • 通過指定啓動參數low-priority-updates,使MyISAM引擎默認給予讀請求以優先的權利。
  • 通過執行命令SET LOW_PRIORITY_UPDATES=1,使該連接發出的更新請求優先級降低。
  • 通過指定INSERT、UPDATE、DELETE語句的LOW_PRIORITY屬性,降低該語句的優先級。

雖然上面3種方法都是要麼更新優先,要麼查詢優先的方法,但還是可以用其來解決查詢相對重要的應用(如用戶登錄系統)中,讀鎖等待嚴重的問題。

另外,MySQL也提供了一種折中的辦法來調節讀寫衝突,即給系統參數max_write_lock_count設置一個合適的值,當一個表的讀鎖達到這個值後,MySQL就暫時將寫請求的優先級降低,給讀進程一定獲得鎖的機會。

上面已經討論了寫優先調度機制帶來的問題和解決辦法。這 、裏還要強調一點:一些需要長時間運行的查詢操作,也會使寫進程“餓死”! 因此,應用中應儘量避免出現長時間運行的查詢操作,不要總想用一條SELECT語句來解決問題,因爲這種看似巧妙的SQL語句,往往比較複雜,執行時間較長,在可能的情況下可以通過使用中間表等措施對SQL語句做一定的“分解”,使每一步查詢都能在較短時間完成,從而減少鎖衝突。如果複雜查詢不可避免,應儘量安排在數據庫空閒時段執行,比如一些定期統計可以安排在夜間執行。


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 語句默認不會加任何鎖類型,加過排他鎖的數據行在其他事務種是不能修改數據的,也不能通過for update和lock in share mode鎖的方式查詢數據,但可以直接通過select 查詢數據,因爲普通查詢沒有任何鎖機制

另外,爲了允許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖。

意向共享鎖(IS):事務打算給數據行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖

意向排他鎖(IX):事務打算給數據行加排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的IX鎖

如果一個事務請求的鎖模式與當前的鎖兼容,InnoDB就請求的鎖授予該事務;反之,如果兩者兩者不兼容,該事務就要等待鎖釋放。

意向鎖是InnoDB自動加的,不需用戶干預。對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及數據集加排他鎖(X);對於普通SELECT語句,InnoDB不會加任何鎖。

事務可以通過以下語句顯式給記錄集加共享鎖或排他鎖:

  • 共享鎖(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE。
  • 排他鎖(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使用的是表鎖,而不是行鎖
  • 由於MySQL的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然是訪問不同行的記錄,但是如果是使用相同的索引鍵,是會出現鎖衝突的。應用設計的時候要注意這一點。
  • 當表有多個索引的時候,不同的事務可以使用不同的索引鎖定不同的行,另外,不論是使用主鍵索引、唯一索引或普通索引,InnoDB都會使用行鎖來對數據加鎖
  • 即便在條件中使用了索引字段,但是否使用索引來檢索數據是由MySQL通過判斷不同執行計劃的代價來決定的,如果MySQL認爲全表掃描效率更高,比如對一些很小的表,它就不會使用索引,這種情況下InnoDB將使用表鎖,而不是行鎖。因此,在分析鎖衝突 時,別忘了檢查SQL的執行計劃,以確認是否真正使用了索引。

間隙鎖(Next-Key鎖)

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

InnoDB使用間隙鎖的目的:

  • 爲了防止幻讀,以滿足相關隔離級別的要求
  • 爲了滿足其恢復和複製的需要

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

還要特別說明的是,InnoDB除了通過範圍條件加鎖時使用間隙鎖外,如果使用相等條件請求給一個不存在的記錄加鎖,InnoDB也會使用間隙鎖


總結

對於MyISAM的表鎖:

  • 共享讀鎖(S)之間是兼容的,但共享讀鎖(S)與排他寫鎖(X)之間,以及排他寫鎖(X)之間是互斥的,也就是說讀和寫是串行的
  • 在一定條件下,MyISAM允許查詢和插入併發執行,我們可以利用這一點來解決應用中對同一表查詢和插入的鎖爭用問題。
  • MyISAM默認的鎖調度機制是寫優先,這並不一定適合所有應用,用戶可以通過設置LOW_PRIORITY_UPDATES參數,或在INSERT、UPDATE、DELETE語句中指定LOW_PRIORITY選項來調節讀寫鎖的爭用。
  • 由於表鎖的鎖定粒度大,讀寫之間又是串行的,因此,如果更新操作較多,MyISAM表可能會出現嚴重的鎖等待,可以考慮採用InnoDB表來減少鎖衝突

對於InnoDB表:

  • InnoDB的行鎖是基於索引實現的,如果不通過索引訪問數據,InnoDB會使用表鎖

在瞭解InnoDB鎖特性後,用戶可以通過設計和SQL調整等措施減少鎖衝突和死鎖,包括:

  • 儘量使用較低的隔離級別;精心設計索引,並儘量使用索引訪問數據,使加鎖更精確,從而減少鎖衝突的機會
  • 選擇合理的事務大小,小事務發生鎖衝突的機率也更小;
  • 給記錄集顯式加鎖時,最好一次性請求足夠級別的鎖。比如要修改數據的話,最好直接申請排他鎖,而不是先申請共享鎖,修改時再請求排他鎖,這樣容易產生死鎖;
  • 不同的程序訪問一組表時,應儘量約定以相同的順序訪問各表,對一個表而言,儘可能以固定的順序存取表中的行。這樣可以大大減少死鎖的機會;
  • 儘量用相等條件訪問數據,這樣可以避免間隙鎖對併發插入的影響; 不要申請超過實際需要的鎖級別;除非必須,查詢時不要顯示加鎖;
  • 對於一些特定的事務,可以使用表鎖來提高處理速度或減少死鎖的可能

參考:唐大麥《MySQL中的鎖(表鎖、行鎖,共享鎖,排它鎖,間隙鎖)》

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