1. 程式人生 > 其它 >mysql行鎖+可重複讀+讀提交

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和一致性檢視確定資料版本的可見性。

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

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

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

 

行鎖

  • 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和一致性檢視確定資料版本的可見性。

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

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

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