mysql行鎖+可重復讀+讀提交


行鎖

  • innodb支持行鎖,myisam只支持表鎖,同一時刻每張表只能有一條數據被更新

  • 在InnoDB事務中,行鎖是在需要的時候才加上的,但並不是不需要了就立刻釋放, 而是要等到事務結束時才釋放。這個就是兩階段鎖協議。

  • 如果你的事務中需要鎖多個行,要把最可能造成鎖沖突、最可能影響並 發度的鎖的申請時機盡量往后放。

  • 例子:假設你負責實現一個電影票在線交易業務,顧客A要在影院B購買電影票。我們簡化一點,這個業務需要涉及到以下操作:

    1. 從顧客A賬戶余額中扣除電影票價;

    2. 給影院B的賬戶余額增加這張電影票價;

    3. 記錄一條交易日志。

    • 也就是說,要完成這個交易,我們需要update兩條記錄,並insert一條記錄。當然,為了保證交易的原子性,我們要把這三個操作放在一個事務中。那么,你會怎樣安排這三個語句在事務中的順序呢? 試想如果同時有另外一個顧客C要在影院B買票,那么這兩個事務沖突的部分就是語句2了。因為它們要更新同一個影院賬戶的余額,需要修改同一行數據。根據兩階段鎖協議,不論你怎樣安排語句順序,所有的操作需要的行鎖都是在事務提交的時候才 釋放的。所以,如果你把語句2安排在最后,比如按照3、1、2這樣的順序,那么影院賬戶余額這一行的鎖時間就最少。這就最大程度地減少了事務之間的鎖等待,提升了並發度

  • 死鎖:事務A和事務B在互相等待對方的資源釋放,就是進入了死鎖狀態。

    • 一種策略是,直接進入等待,直到超時。這個超時時間可以通過參數 innodb_lock_wait_timeout來設置。

      • 默認是50s,在正常生產環境中是不可接受的

      • 設置時間過短可能會誤傷很多,比如簡單的鎖等待

    • 另一種策略是,發起死鎖檢測,發現死鎖后,主動回滾死鎖鏈條中的某一個事務,讓其他事務得以繼續執行。將參數innodb_deadlock_detect設置為on,表示開啟這個邏輯。

      • 如果有1000條並發更新同一行,那么會有1000*1000並發量死鎖檢測,導致cpu上升

      • 如果確定不會出現死鎖,可以關閉死鎖檢測。但是這種操作本身帶有一定的風險,因為業務設計的時候一般不會把死鎖當做一個嚴重錯誤,畢竟出現死鎖了,就回滾,然后通過業務重試一般就沒問題了,這是業務無損的。而關掉死鎖檢測意味着可能會出現大量的超時,這是業務有損的。

      • 控制並發度,在進入mysql服務端設置中間件,或者通過修改mysql源碼來是這樣的並發線程排隊,不會出現死鎖檢測。

 

事物隔離      

  

進行以下流程操作:

  • 注意:begin/start transaction 命令並不是一個事務的起點,在執行到它們之后的第一個操作InnoDB表的語句(第一個快照讀語句),事務才真正啟動。如果你想要馬上啟動一個事務,可以使用start transaction with consistent snapshot 這個命令。

  • mysql有兩個視圖的概念

    1. view,用查詢語句定義的虛擬表,在調用的時候執行查詢語句並生成結果,創建視圖的語法是create view

    2. innodb在實現mvcc時用到的一致性讀視圖,用於支持讀提交,可重復度隔離級別的實現。

  • 快照

    • innodb里面每個事物有唯一的事物id,叫做transaction id,在事物開始時想innodb事物系統申請的,是按申請順序嚴格遞增的。

    • 每行數據有多個版本的,每次更新數據的時候,都會生成一個新的數據版本,並且將transaction id賦值給這個數據版本的事物id 記為row trx_id,同時舊的數據版本要保留,並且在新的數據版本中,能夠有信息可以直接拿到它。所以數據表中的一行記錄,其實有多個版本,每個版本有自己的row trx_id

 

  • 如圖:同一行數據四個版本,最新版本是v4,k的值是22,它被transaction id為25的事物更新,因此它的row trx_id也是25

  • 虛線就是回滾日志,V1,V2,V3都不是物理存在的,而是需要根據當前版本和undolog計算出來的。

  • 回顧快照

    • 可重復度:一個事物啟動后,能夠看到所有已提交事物結果,但是,這個事物執行期間,其它事物的更新對他不可見。因此一個事物只需在啟動的時候說明,,“以我啟動的時刻為准,如果一個數據版本是在我啟動之前生成的,就認;如果是我啟動以后才生成的,我就不認,我必須要找到它的上一個版本”。當然,如果“上一個版本”也不可見,那就得繼續往前找。還有,如果是這個事務自己更新的數據,它自己還是要認的。

    • innodb還為每個事物構造了數組,用來保存事物啟動的瞬間,當前啟動了還沒提交的事物id。

      • 數組里事物id的最小值記為低水位,系統里事物id最大值+1記為高水位

      • 試圖數組和高水位就構成了當前事物的一致性視圖

      • 這個視圖數組把所有的row trx_id分成了幾種不同的情況。

  

 

    • 對於當前事物啟動瞬間來說,一個數據版本的row trx_id有以下幾種可能

      1. 落在綠色部分,表示這個版本是已提交的事物或者是自己生成的,可見

      2. 落在紅色部分,這個版本是由將來事物生成的,不可見。

      3. 如果在黃色部分

        1. 若row trx_id在數組中,表示這個版本是由還沒提交的事物生成的,不可見

        2. 若row trx_id不再數組中,表示這個版本是已提交的事物生成的,課件

    • 比如上方圖(數據的四個版本),如果有一個事物,它的低水位是18,那么當他訪問這一行數據時,v4通過u3算出v3,得到值是11。

  • 第一張圖中的三個事物

    1. 假設事物A開始前,系統里只有一個活躍事物id是99,

    2. 事物A,B,C版本號分別是100,101,102,並且當前只有這四個事物

    3. 三個事物開始前(1,1)這一行的數據row trx_id是90

    • 所以事物A的數組就是[99,100],B[99,100,101],C[99,100,101,102]

 

 

 

    • 從圖中可以看到,第一個有效更新是事務C,把數據從(1,1)改成了(1,2)。這時候,這個數據的最新版本的row trx_id是102,而90這個版本已經成為了歷史版本。

    • 第二個有效更新是事務B,把數據從(1,2)改成了(1,3)。這時候,這個數據的最新版本(即row trx_id)是101,而102又成為了歷史版本。

    • 在事務A查詢的時候,其實事務B還沒有提交,但是它生成的(1,3)這個版本已經變成當前版本了。但這個版本對事務A必須是不可見的,否則就變成臟讀了。事務A查詢語句的讀數據流程是這樣的

      • 找到(1,3)的時候,判斷出row trx_id=101,比高水位大,處於紅色區域,不可見;

      • 接着,找到上一個歷史版本,一看row trx_id=102,比高水位大,處於紅色區域,不可見;

      • 再往前找,終於找到了(1,1),它的row trx_id=90,比低水位小,處於綠色區域,可見。

    • 這樣執行下來,雖然期間這一行數據被修改過,但是事務A不論在什么時候查詢,看到這行數據的結果都是一致的,所以我們稱之為一致性讀。

  • 一個數據版本,對於一個事務視圖來說,除了自己的更新總是可見以外,有三種情況:

    1. 版本未提交,不可見

    2. 版本已提交,但是是在視圖創建后提交的,不可見;

    3. 版本已提交,而且是在視圖創建前提交的,可見。

  • 事務A的查詢語句的視圖數組是在事務A啟動的時候生成的,這時候:

    • (1,3)還沒提交,屬於情況1,不可見;

    • (1,2)雖然提交了,但是是在視圖數組創建之后提交的,屬於情況2,不可見;

    • (1,1)是在視圖數組創建之前提交的,可見。

 對於B來說,遵循更新規則:更新數據都是先讀后寫的,而這個讀,只能讀當前的值,稱 為“當前讀”(current read)。除了update語句外,如果select語句加鎖,也是當前讀。所以,如果把事務A的查詢語句select * from t where id=1修改一下,加上lock in share mode或 for update,也都可以讀到版本號是101的數據,返回的k的值是3。

 

 

事務C’的不同是,更新后並沒有馬上提交,在它提交前,事務B的更新語句先發起了。前面說過了,雖然事務C’還沒提交,但是(1,2)這個版本也已經生成了,並且是當前的最新版本。那么,事務B的更新語句會怎么處理呢? 這時候,我們在上一篇文章中提到的“兩階段鎖協議”就要上場了。事務C’沒提交,也就是說(1,2)這個版本上的寫鎖還沒釋放。而事務B是當前讀,必須要讀最新版本,而且必須加鎖,因此就被鎖住了,必須等到事務C’釋放這個鎖,才能繼續它的當前讀。

 

 可重復讀的核心就是一致性讀(consistent read);而事務更新數據的時候,只能用當前讀。如果當前的記錄的行鎖被其他事務占用的話,就需要進入鎖等待。

而讀提交的邏輯和可重復讀的邏輯類似,它們最主要的區別是:在可重復讀隔離級別下,只需要在事務開始的時候創建一致性視圖,之后事務里的其他查詢都共用這個一致性視圖;在讀提交隔離級別下,每一個語句執行前都會重新算出一個新的視圖。

 

那么,我們再看一下,在讀提交隔離級別下,事務A和事務B的查詢語句查到的k,分別應該是多少呢? 這里需要說明一下,“start transaction with consistent snapshot; ”的意思是從這個語句開始,創建一個持續整個事務的一致性快照。所以,在讀提交隔離級別下,這個用法就沒意義了,等效於普通的start transaction。下面是讀提交時的狀態圖,可以看到這兩個查詢語句的創建視圖數組的時機發生了變化,就是圖中的read view框。(注意:這里,我們用的還是事務C的邏輯直接提交,而不是事務C’)

 

 

這時,事務A的查詢語句的視圖數組是在執行這個語句的時候創建的,時序上(1,2)、(1,3)的生成時間都在創建這個視圖數組的時刻之前。但是,在這個時刻: (1,3)還沒提交,屬於情況1,不可見; (1,2)提交了,屬於情況3,可見。 所以,這時候事務A查詢語句返回的是k=2。 顯然地,事務B查詢結果k=3。

 

小結

innoDB的行數據有多個版本,每個數據版本有自己的row trx_id,每個事務或者語句有自己的一致性視圖。普通查詢語句是一致性讀,一致性讀會根據row trx_id和一致性視圖確定數據版本的可見性。

  • 對於可重復讀,查詢只承認在事務啟動前就已經提交完成的數據;

  • 對於讀提交,查詢只承認在語句啟動前就已經提交完成的數據;

  • 而當前讀,總是讀取已經提交完成的最新版本。

 


免責聲明!

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



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