事物的级别_浅谈MySQL并发控制:隔离级别、锁与MVCC
推薦閱讀:
恕我直言!收藏這個MySQL文檔,你其余的MySQL學習資料都可以扔了
前言
如果數據庫中的事務都是串行執行的,這種方式可以保障事務的執行不會出現異常和錯誤,但帶來的問題是串行執行會帶來性能瓶頸;而事務并發執行,如果不加以控制則會引發諸多問題,包括死鎖、更新丟失等等。這就需要我們在性能和安全之間做出合理的權衡,使用適當的并發控制機制保障并發事務的執行。
并發事務帶來的問題
首先我們先來了解一下并發事務會帶來哪些問題。并發事務訪問相同記錄大致可歸納為以下3種情況:
- 讀-讀:即并發事務相繼讀取同一記錄;
- 寫-寫:即并發事務相繼對同一記錄做出修改;
- 寫-讀或讀-寫:即兩個并發事務對同一記錄分別進行讀操作和寫操作。
讀-讀
因為讀取記錄并不會對記錄造成任何影響,所以同個事務并發讀取同一記錄也就不存在任何安全問題,所以允許這種操作。
寫-寫
如果允許并發事務都讀取同一記錄,并相繼基于舊值對這一記錄做出修改,那么就會出現前一個事務所做的修改被后面事務的修改覆蓋,即出現提交覆蓋的問題。
另外一種情況,并發事務相繼對同一記錄做出修改,其中一個事務提交之后之后另一個事務發生回滾,這樣就會出現已提交的修改因為回滾而丟失的問題,即回滾覆蓋問題。
這兩種問題都造成丟失更新,其中回滾覆蓋稱為第一類丟失更新問題,提交覆蓋稱為第二類丟失更新問題。
寫-讀或讀-寫
這種情況較為復雜,也最容易出現問題。
如果一個事務讀取了另一個事務尚未提交的修改記錄,那么就出現了臟讀的問題;
如果我們加以控制使得一個事務只能讀取其他已提交事務的修改的數據,那么這個事務在另一事物提交修改前后讀取到的數據是不一樣的,這就意味著發生了不可重復讀;
如果一個事務根據一些條件查詢到一些記錄,之后另一事物向表中插入了一些記錄,原先的事務以相同條件再次查詢時發現得到的結果跟第一次查詢得到的結果不一致,這就意味著發生了幻讀。
事務的隔離級別
對于以上提到的并發事務執行過程中可能出現的問題,其嚴重性也是不一樣的,我們可以按照問題的嚴重程度排個序:
丟失更新?>?臟讀?>?不可重復讀?>?幻讀因此如果我們可以容忍一些嚴重程度較輕的問題,我們就能獲取一些性能上的提升。于是便有了事務的四種隔離級別:
- 讀未提交(Read Uncommitted):允許讀取未提交的記錄,會發生臟讀、不可重復讀、幻讀;
- 讀已提交(Read Committed):只允許讀物已提交的記錄,不會發生臟讀,但會出現重復讀、幻讀;
- 可重復讀(Repeatable Read):不會發生臟讀和不可重復讀的問題,但會發生幻讀問題;但MySQL在此隔離級別下利用間隙鎖可以禁止幻讀問題的發生;
- 可串行化(Serializable):即事務串行執行,以上各種問題自然也就都不會發生。
值得注意的是以上四種隔離級別都不會出現回滾覆蓋的問題,但是提交覆蓋的問題對于MySQL來說,在Read Uncommitted、Read Committed以及Repeatable Read這三種隔離級別下都會發生(標準的Repeatable Read隔離級別不允許出現提交覆蓋的問題),需要額外加鎖來避免此問題。
隔離級別的實現
SQL規范定義了以上四種隔離級別,但是并沒有給出如何實現四種隔離級別,因此不同數據庫的實現方式和使用方式也并不相同。而SQL隔離級別的標準是依據基于鎖的實現方式來制定的,因為有必要先了解一下傳統的基于鎖的隔離級別是如何實現的。
傳統隔離級別的實現
既然說到傳統的隔離級別是基于鎖實現的,我們先來了解一下鎖。
鎖
傳統的鎖有兩種:
- 共享鎖(Shared Locks):簡稱S鎖,事務對一條記錄進行讀操作時,需要先獲取該記錄的共享鎖。
- 排他鎖(Exclusive Locks):簡稱X鎖,事務對一條記錄進行寫操作時,需要先獲取該記錄的排他鎖。
需要注意的是,加了共享鎖的記錄,其他事務也可以獲得該記錄的共享鎖,但是無法獲取該記錄的排他鎖,即S鎖和S鎖是兼容的,S鎖和X鎖是不兼容的;而加了排他鎖的記錄,其他事務既無法獲取該記錄的共享鎖也無法獲取排他鎖,即X鎖和X鎖也是不兼容的。
另外,剛剛說到事務對一條記錄進行讀操作時,需要先獲取該記錄的S鎖,但有時事務在讀取記錄時需要阻止其他事務訪問該記錄,這時就需要獲取該記錄的X鎖。以MySQL為例,有以下兩種鎖定讀的方式:
- 讀取時對記錄加S鎖:
SELECT?...?LOCK?IN?SHARE?MODE;如果事務執行了該語句,則會在讀取的記錄上加S鎖,這樣就允許其他事務也能獲取到該記錄的S鎖;而如果其他事務需要獲取該記錄的X鎖,那么就需要等待當前事務提交后釋放掉S鎖。
- 讀取時對記錄加X鎖:
SELECT?...?FOR?UPDATE;如果事務執行了該語句,則會在讀取的記錄上加X鎖,這樣其他事務想要說去該記錄的S鎖或X鎖,那么需要等待當前事務提交后釋放掉X鎖。
對于鎖的粒度而言,鎖又可以分為兩種:
- 行鎖:只鎖住某一行記錄,其他行的記錄不受影響。
- 表鎖:鎖住整個表,所有對于該表的操作都會受影響。
基于鎖實現隔離級別
在基于鎖的實現方式下,四種隔離級別的區別就在于加鎖方式的區別:
- 讀未提交:讀操作不加鎖,讀讀,讀寫,寫讀并行;寫操作加X鎖且直到事務提交后才釋放。
- 讀已提交:讀操作加S鎖,寫操作加X鎖且直到事務提交后才釋放;讀操作不會阻塞其他事務讀或寫,寫操作會阻塞其他事務寫和讀,因此可以防止臟讀問題。
- 可重復讀:讀操作加S鎖且直到事務提交后才釋放,寫操作加X鎖且直到事務提交后才釋放;讀操作不會阻塞其他事務讀但會阻塞其他事務寫,寫操作會阻塞其他事務讀和寫,因此可以防止臟讀、不可重復讀。
- 串行化:讀操作和寫操作都加X鎖且直到事務提交后才釋放,粒度為表鎖,也就是嚴格串行。
這里面有一些細節值得注意:
- 如果鎖獲取之后直到事務提交后才釋放,這種鎖稱為長鎖;如果鎖在操作完成之后就被釋放,這種鎖稱為短鎖。例如,在讀已提交隔離級別下,讀操作所加S鎖為短鎖,寫操作所加X鎖為長鎖。
- 對于可重復讀和串行化隔離級別,讀操作所加S鎖和寫操作所加X鎖均為長鎖,即事務獲取鎖之后直到事務提交后才能釋放,這種把獲取鎖和釋放鎖分為兩個不同的階段的協議稱為兩階段鎖協議(2-phase locking)。兩階段鎖協議規定在加鎖階段,一個事務可以獲得鎖但是不能釋放鎖;而在解鎖階段事務只可以釋放鎖,并不能獲得新的鎖。兩階段鎖協議能夠保證事務串行化執行,解決事務并發問題,但也會導致死鎖發生的概率大大提升。
MySQL隔離級別的實現
不同數據庫對于SQL標準中規定的隔離級別支持是不一樣的,數據庫引擎實現隔離級別的方式雖然都在盡可能地貼近標準的隔離級別規范,但和標準的預期還是有些不一樣的地方。
MySQL(InnoDB)支持的4種隔離級別,與標準的各級隔離級別允許出現的問題有些出入,比如MySQL在可重復讀隔離級別下可以防止幻讀的問題出現,但也會出現提交覆蓋的問題。
相對于傳統隔離級別基于鎖的實現方式,MySQL 是通過MVCC(多版本并發控制)來實現讀-寫并發控制,又是通過兩階段鎖來實現寫-寫并發控制的。MVCC是一種無鎖方案,用以解決事務讀-寫并發的問題,能夠極大提升讀-寫并發操作的性能。
MVCC的實現原理
為了方便描述,首先我們創建一個表book,就三個字段,分別是主鍵book_id, 名稱book_name, 庫存stock。然后向表中插入一些數據:
INSERT?INTO?book?VALUES(1,?'數據結構',?100);INSERT?INTO?book?VALUES(2,?'C++指南',?100);INSERT?INTO?book?VALUES(3,?'精通Java',?100);版本鏈
對于使用InnoDB存儲引擎的表,其聚簇索引記錄中包含了兩個重要的隱藏列:
- trx_id:每當事務對聚簇索引中的記錄進行修改時,都會把當前事務的事務id記錄到trx_id中。
- roll_pointer:每當事務對聚簇索引中的記錄進行修改時,都會把該記錄的舊版本記錄到undo日志中,通過roll_pointer這個指針可以用來獲取該記錄舊版本的信息。
如果在一個事務中多次對記錄進行修改,則每次修改都會生成undo日志,并且這些undo日志通過roll_pointer指針串聯成一個版本鏈,版本鏈的頭結點是該記錄最新的值,尾結點是事務開始時的初始值。
例如,我們在表book中做以下修改:
BEGIN;UPDATE?book?SET?stock?=?200?WHERE?id?=?1;UPDATE?book?SET?stock?=?300?WHERE?id?=?1;那么id=1的記錄此時的版本鏈就如下圖所示:
ReadView
對于使用Read Uncommitted隔離級別的事務來說,只需要讀取版本鏈上最新版本的記錄即可;對于使用Serializable隔離級別的事務來說,InnoDB使用加鎖的方式來訪問記錄。而Read Committed和Repeatable Read隔離級別來說,都需要讀取已經提交的事務所修改的記錄,也就是說如果版本鏈中某個版本的修改沒有提交,那么該版本的記錄時不能被讀取的。所以需要確定在Read Committed和Repeatable Read隔離級別下,版本鏈中哪個版本是能被當前事務讀取的。于是ReadView的概念被提出以解決這個問題。
首先我們需要知道的一個事實是:事務id是遞增分配的。ReadView的機制就是在生成ReadView時確定了以下幾種信息:
- m_ids:表示在生成ReadView時當前系統中活躍的讀寫事務的事務id列表。
- min_trx_id:表示在生成ReadView時當前系統中活躍的讀寫事務中最小的事務id,也就是m_ids中的最小值。
- max_trx_id:表示生成ReadView時系統中將要分配給下一個事務的id值。
- creator_trx_id:表示生成該ReadView的事務的事務id。
這樣事務id就可以分成3個區間:
- 區間(0, min_trx_id):事務id在這個范圍內的事務在生成此ReadView時已經提交,因此這些事務修改的版本記錄都是被當前事務可以讀取的;
- 區間[min_trx_id, max_trx_id): 事務id在這個范圍內的事務可能是活躍的,也有可能是已經提交的,而事務id存在于m_ids中的事務都是活躍事務,否則就是已提交事務。
- 區間[max_trx_id, +∞):事務id在這個范圍內的事務都是在生成ReadView之后創建的。
下面我們根據ReadView提供的條件信息,順著版本鏈從頭結點開始查找最新的可被讀取的版本記錄:
1、首先判斷版本記錄的trx_id與ReadView中的creator_trx_id是否相等。如果相等,那就說明該版本的記錄是在當前事務中生成的,自然也就能夠被當前事務讀取;否則進行第2步。
2、根據版本記錄的trx_id以及上述3個區間信息,判斷生成該版本記錄的事務是否是已提交事務,進而確定該版本記錄是否可被當前事務讀取。
如果某個版本記錄經過以上步驟判斷確定其可被當前事務讀取,則查詢結果返回此版本記錄;否則讀取下一個版本記錄繼續按照上述步驟進行判斷,直到版本鏈的尾結點。如果遍歷完版本鏈沒有找到可讀取的版本,則說明該記錄對當前事務不可見,查詢結果為空。
在MySQL中,Read Committed和Repeatable Read隔離級別下的區別就是它們生成ReadView的時機不同。
MVCC實現不同隔離級別
之前說到ReadView的機制只在Read Committed和Repeatable Read隔離級別下生效,所以只有這兩種隔離級別才有MVCC。在Read Committed隔離級別下,每次讀取數據時都會生成ReadView;而在Repeatable Read隔離級別下只會在事務首次讀取數據時生成ReadView,之后的讀操作都會沿用此ReadView。
下面我們通過例子來看看Read Committed和Repeatable Read隔離級別下MVCC的不同表現。我們繼續以表book為例進行演示。
Read Committed隔離級別下MVCC工作原理
假設在Read Committed隔離級別下,有如下事務在執行,事務id為10:
BEGIN;?//?開啟Transaction?10UPDATE?book?SET?stock?=?200?WHERE?id?=?2;UPDATE?book?SET?stock?=?300?WHERE?id?=?2;此時該事務尚未提交,id為2的記錄版本鏈如下圖所示:
然后我們開啟一個事務對id為2的記錄進行查詢:
BEGIN;當執行SELECT語句時會生成一個ReadView,該ReadView中的m_ids為[10],min_trx_id為10,max_trx_id為11,creator_trx_id為0(因為事務中當執行寫操作時才會分配一個單獨的事務id,否則事務id為0)。按照我們之前所述ReadView的工作原理,我們查詢到的版本記錄為
+----------+-----------+-------+|?book_id??|?book_name?|?stock?|+----------+-----------+-------+|?2????????|?C++指南????|??100??|+----------+-----------+-------+然后我們將事務id為10的事務提交:
BEGIN;?//?開啟Transaction?10UPDATE?book?SET?stock?=?200?WHERE?id?=?2;UPDATE?book?SET?stock?=?300?WHERE?id?=?2;COMMIT;同時開啟執行另一事務id為11的事務,但不提交:
BEGIN;?//?開啟Transaction?11UPDATE?book?SET?stock?=?400?WHERE?id?=?2;此時id為2的記錄版本鏈如下圖所示:
然后我們回到剛才的查詢事務中再次查詢id為2的記錄:
BEGIN;SELECT?*?FROM?book?WHERE?id?=?2;?//?此時Transaction?10?未提交SELECT?*?FROM?book?WHERE?id?=?2;?//?此時Transaction?10?已提交當第二次執行SELECT語句時會再次生成一個ReadView,該ReadView中的m_ids為[11],min_trx_id為11,max_trx_id為12,creator_trx_id為0。按照ReadView的工作原理進行分析,我們查詢到的版本記錄為
+----------+-----------+-------+|?book_id??|?book_name?|?stock?|+----------+-----------+-------+|?2????????|?C++指南????|?300???|+----------+-----------+-------+從上述分析可以發現,因為每次執行查詢語句都會生成新的ReadView,所以在Read Committed隔離級別下的事務讀取到的是查詢時刻表中已提交事務修改之后的數據。
Repeatable Read隔離級別下MVCC工作原理
我們在Repeatable Read隔離級別下重復上面的事務操作:
BEGIN;?//?開啟Transaction?20UPDATE?book?SET?stock?=?200?WHERE?id?=?2;UPDATE?book?SET?stock?=?300?WHERE?id?=?2;此時該事務尚未提交,然后我們開啟一個事務對id為2的記錄進行查詢:
BEGIN;SELECT?*?FROM?book?WHERE?id?=?2;當事務第一次執行SELECT語句時會生成一個ReadView,該ReadView中的m_ids為[20],min_trx_id為20,max_trx_id為21,creator_trx_id為0。根據ReadView的工作原理,我們查詢到的版本記錄為
+----------+-----------+-------+|?book_id??|?book_name?|?stock?|+----------+-----------+-------+|?2????????|?C++指南????|??100??|+----------+-----------+-------+然后我們將事務id為20的事務提交:
BEGIN;?//?開啟Transaction?20UPDATE?book?SET?stock?=?200?WHERE?id?=?2;UPDATE?book?SET?stock?=?300?WHERE?id?=?2;COMMIT;同時開啟執行另一事務id為21的事務,但不提交:
BEGIN;?//?開啟Transaction?21UPDATE?book?SET?stock?=?400?WHERE?id?=?2;然后我們回到剛才的查詢事務中再次查詢id為2的記錄:
BEGIN;SELECT?*?FROM?book?WHERE?id?=?2;?//?此時Transaction?10?未提交SELECT?*?FROM?book?WHERE?id?=?2;?//?此時Transaction?10?已提交當第二次執行SELECT語句時不會生成新的ReadView,依然會使用第一次查詢時生成ReadView。因此我們查詢到的版本記錄跟第一次查詢到的結果是一樣的:
+----------+-----------+-------+|?book_id??|?book_name?|?stock?|+----------+-----------+-------+|?2????????|?C++指南????|??100??|+----------+-----------+-------+從上述分析可以發現,因為在Repeatable Read隔離級別下的事務只會在第一次執行查詢時生成ReadView,該事務中后續的查詢操作都會沿用這個ReadView,因此此隔離級別下一個事務中多次執行同樣的查詢,其結果都是一樣的,這樣就實現了可重復讀。
快照讀和當前讀
快照讀
在Read Committed和Repeatable Read隔離級別下,普通的SELECT查詢都是讀取MVCC版本鏈中的一個版本,相當于讀取一個快照,因此稱為快照讀。這種讀取方式不會加鎖,因此讀操作時非阻塞的,因此也叫非阻塞讀。
在標準的Repeatable Read隔離級別下讀操作會加S鎖,直到事務結束,因此可以阻止其他事務的寫操作;但在MySQL的Repeatable Read隔離級別下讀操作沒有加鎖,不會阻止其他事務對相同記錄的寫操作,因此在后續進行寫操作時就有可能寫入基于版本鏈中的舊數據計算得到的結果,這就導致了提交覆蓋的問題。想要避免此問題,就需要另外加鎖來實現。
當前讀
之前提到MySQL有兩種鎖定讀的方式:
SELECT?...?LOCK?IN?SHARE?MODE;?//?讀取時對記錄加S鎖,直到事務結束SELECT?...?FOR?UPDATE;?//?讀取時對記錄加X鎖,直到事務結束這種讀取方式讀取的是記錄的當前最新版本,稱為當前讀。另外對于DELETE、UPDATE操作,也是需要先讀取記錄,獲取記錄的X鎖,這個過程也是一個當前讀。由于需要對記錄進行加鎖,會阻塞其他事務的寫操作,因此也叫加鎖讀或阻塞讀。
當前讀不僅會對當前記錄加行記錄鎖,還會對查詢范圍空間的數據加間隙鎖(GAP LOCK),因此可以阻止幻讀問題的出現。
總結
本文介紹了事務的多種并發問題,以及用以避免不同程度問題的隔離級別,并較為詳細描述了傳統隔離級別的實現方式以及MySQL隔離級別的實現方式。但數據庫的并發機制較為復雜,本文也只是做了大致的描述和介紹,很多細節還需要讀者自己查詢相關資料進行更細致的了解。
作者:Turling_hu
鏈接:https://juejin.im/post/5e72246ae51d4527143e6a0d
推薦閱讀:
恕我直言!收藏這個MySQL文檔,你其余的MySQL學習資料都可以扔了
總結
以上是生活随笔為你收集整理的事物的级别_浅谈MySQL并发控制:隔离级别、锁与MVCC的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 威纶通触摸屏与单片机MODBUS_威纶通
- 下一篇: 确认和回调_【短线回调,确认突破点】