天天看點

資料庫之為什麼RR讀可以解決可重複讀

資料庫事務隔離級别RR為什麼可以解決可重複讀

髒讀與幻讀與不可重複讀

  • 髒讀(Drity Read):某個事務已更新一份資料,另一個事務在此時讀取了同一份資料,由于某些原因,前一個RollBack了操作,則後一個事務所讀取的資料就會是不正确的。
  • 不可重複讀(Non-repeatable read):在一個事務的兩次查詢之中資料不一緻,這可能是兩次查詢過程中間插入了一個事務更新的原有的資料。
  • 幻讀(Phantom Read):在一個事務的兩次查詢中資料筆數不一緻,例如有一個事務查詢了幾列(Row)資料,而另一個事務卻在此時插入了新的幾列資料,先前的事務在接下來的查詢中,就會發現有幾列資料是它先前所沒有的。

事務的隔離級别

為了達到事務的四大特性,資料庫定義了4種不同的事務隔離級别,由低到高依次為Read uncommitted、Read committed、Repeatable read、Serializable,這四個級别可以逐個解決髒讀、不可重複讀、幻讀這幾類問題。

隔離級别 髒讀 不可重複讀 幻影讀
READ-UNCOMMITTED
READ-COMMITTED ×
REPEATABLE-READ × ×
SERIALIZABLE × × ×
  • ① 髒讀: 髒讀就是指當一個事務正在通路資料,并且對資料進行了修改,而這種修改還沒有送出到資料庫中,這時,另外一個事務也通路這個資料,然後使用了這個資料。

解決方案:将隔離級别改為讀已送出

  • ② 不可重複讀:是指在一個事務内,多次讀同一資料。在這個事務還沒有結束時,另外一個事務也通路該同一資料。那麼,在第一個事務中的兩次讀資料之間,由于第二個事務的修改,那麼第一個事務兩次讀到的的資料可能是不一樣的。這樣就發生了在一個事務内兩次讀到的資料是不一樣的,是以稱為是不可重複讀

解決方案:把資料庫的事務隔離級别調整到REPEATABLE_READ(可重複讀)

  • ③ 幻讀:第一個事務對一個表中的資料進行了修改,這種修改涉及到表中的全部資料行。同時,第二個事務也修改這個表中的資料,這種修改是向表中插入一行新資料。那麼,以後就會發生操作第一個事務的使用者發現表中還有沒有修改的資料行,就好象發生了幻覺一樣,幻讀是資料行記錄變多了或者少了。

解決方案:(1)多并發版本控制(MVCC)(MVCC在快照讀時可以解決幻讀)、(2)next-key鎖(目前讀解決幻讀)

SQL 标準定義了四個隔離級别:

  • READ-UNCOMMITTED(讀取未送出): 最低的隔離級别,允許讀取尚未送出的資料變更,可能會導緻髒讀、幻讀或不可重複讀。
  • READ-COMMITTED(讀取已送出): 允許讀取并發事務已經送出的資料,可以阻止髒讀,但是幻讀或不可重複讀仍有可能發生。
  • REPEATABLE-READ(可重複讀): 對同一字段的多次讀取結果都是一緻的,除非資料是被本身事務自己所修改,可以阻止髒讀和不可重複讀,但幻讀仍有可能發生。
  • SERIALIZABLE(可串行化): 最高的隔離級别,完全服從ACID的隔離級别。所有的事務依次逐個執行,這樣事務之間就完全不可能産生幹擾,也就是說,該級别可以防止髒讀、不可重複讀以及幻讀。

MySQL的預設隔離級别

Mysql 預設采用的 REPEATABLE_READ(可重複讀)隔離級别 Oracle 預設采用的 READ_COMMITTED(讀已送出)隔離級别

事務隔離機制的實作基于鎖機制和并發排程。其中并發排程使用的是MVVC(多版本并發控制),通過儲存修改的舊版本資訊來支援并發一緻性讀和復原等特性。

因為隔離級别越低,事務請求的鎖越少,是以大部分資料庫系統的隔離級别都是READ-COMMITTED(讀取送出内容):,但是你要知道的是InnoDB 存儲引擎預設使用 **REPEATABLE-READ(可重讀)**并不會有任何性能損失。

InnoDB 存儲引擎在 分布式事務 的情況下一般會用到**SERIALIZABLE(可串行化)**隔離級别。

事務傳播行為

1、PROPAGATION_REQUIRED:如果目前沒有事務,就建立一個新事務,如果目前存在事務,就加入該事務,該設定是最常用的設定。

2、PROPAGATION_REQUIRES_NEW:支援目前事務,建立新事務,無論目前存不存在事務,都建立新事務。

3、PROPAGATION_SUPPORTS:支援目前事務,如果目前存在事務,就加入該事務,如果目前不存在事務,就以非事務執行。

4、PROPAGATION_NOT_SUPPORTED:以非事務方式執行操作,如果目前存在事務,就把目前事務挂起。

5、PROPAGATION_NESTED:如果目前存在事務,則在嵌套事務内執行。如果目前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作

6、PROPAGATION_MANDATORY:支援目前事務,如果目前存在事務,就加入該事務,如果目前不存在事務,就抛出異常。

7、PROPAGATION_NEVER:以非事務方式執行,如果目前存在事務,則抛出異常。

RR為什麼可以解決可重複讀問題

可重複讀就是多次讀取資料都是一樣的結果,那麼隔離級别為可重複讀的時候是為什麼可以保證這個?

場景:(前提是兩個操作在同一個事務中)

同一個事務下,若事務A讀取一個資料age為100,事務B更新了這個資料為200,那麼事務A再讀,應該是多少?

兩種情況:

  • A讀取的是200,那麼就違背了可重複讀
  • A讀取的是100,那麼是為什麼?

跟快照讀有關,A讀取的隻是一個快照,也就是說不是資料庫中最新的資料,讀的是老版本的資料,隻有A在更新資料的時候,需要比較目前版本與自己讀取的資料版本是否一緻,如果一緻則更新,不一緻則需要借助機制進行相應實作。

具體可以通過多版本并發控制(MVCC,Multiversion Concurrency Control)機制實作

1、MVCC?

MVCC MVCC,全稱Multi-Version Concurrency Control,即多版本并發控制。MVCC是一種并發控制的方法,一般在資料庫管理系統中,實作對資料庫的并發通路,在程式設計語言中實作事務記憶體。

MVCC在MySQL InnoDB中的實作主要是為了提高資料庫并發性能,用更好的方式去處理讀-寫沖突,做到即使有讀寫沖突時,也能做到不加鎖,非阻塞并發讀

什麼是目前讀和快照讀?

在學習MVCC多版本并發控制之前,我們必須先了解一下,什麼是MySQL InnoDB下的目前讀和快照讀?

  • 目前讀

    ​ 像select lock in share mode(共享鎖), select for update ; update, insert ,delete(排他鎖)這些操作都是一種目前讀,為什麼叫目前讀?就是它讀取的是記錄的最新版本,讀取時還要保證其他并發事務不能修改目前記錄,會對讀取的記錄進行加鎖。

  • 快照讀

    ​ 像不加鎖的select操作就是快照讀,即不加鎖的非阻塞讀;快照讀的前提是隔離級别不是串行級别,串行級别下的快照讀會退化成目前讀;之是以出現快照讀的情況,是基于提高并發性能的考慮,快照讀的實作是基于多版本并發控制,即MVCC,可以認為MVCC是行鎖的一個變種,但它在很多情況下,避免了加鎖操作,降低了開銷;既然是基于多版本,即快照讀可能讀到的并不一定是資料的最新版本,而有可能是之前的曆史版本

說白了MVCC就是為了實作讀-寫沖突不加鎖,而這個讀指的就是快照讀, 而非目前讀,目前讀實際上是一種加鎖的操作,是悲觀鎖的實作

目前讀,快照讀和MVCC的關系

  • 準确的說,MVCC多版本并發控制指的是 “維持一個資料的多個版本,使得讀寫操作沒有沖突” 這麼一個概念。僅僅是一個理想概念
  • 而在MySQL中,實作這麼一個MVCC理想概念,我們就需要MySQL提供具體的功能去實作它,而快照讀就是MySQL為我們實作MVCC理想模型的其中一個具體非阻塞讀功能。而相對而言,目前讀就是悲觀鎖的具體功能實作
  • 要說的再細緻一些,快照讀本身也是一個抽象概念,再深入研究。MVCC模型在MySQL中的具體實作則是由 3個隐式字段,undo日志 ,Read View 等去完成的,具體可以看下面的MVCC實作原理

MVCC能解決什麼問題,好處是?

資料庫并發場景有三種,分别為:
  • 讀-讀:不存在任何問題,也不需要并發控制
  • 讀-寫:有線程安全問題,可能會造成事務隔離性問題,可能遇到髒讀,幻讀,不可重複讀
  • 寫-寫:有線程安全問題,可能會存在更新丢失問題,比如第一類更新丢失,第二類更新丢失
MVCC帶來的好處是?

多版本并發控制(MVCC)是一種用來解決讀-寫沖突的無鎖并發控制,也就是為事務配置設定單向增長的時間戳,為每個修改儲存一個版本,版本與事務時間戳關聯,讀操作隻讀該事務開始前的資料庫的快照。 是以MVCC可以為資料庫解決以下問題

  • 在并發讀寫資料庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了資料庫并發讀寫的性能
  • 同時還可以解決髒讀,幻讀,不可重複讀等事務隔離問題,但不能解決更新丢失問題

小結

總之,MVCC就是因為大牛們,不滿意隻讓資料庫采用悲觀鎖這樣性能不佳的形式去解決讀-寫沖突問題而提出的解決方案,是以在資料庫中,因為有了MVCC,是以我們可以形成兩個組合:

  • MVCC + 悲觀鎖 MVCC解決讀寫沖突,悲觀鎖解決寫寫沖突
  • MVCC + 樂觀鎖 MVCC解決讀寫沖突,樂觀鎖解決寫寫沖突 這種組合的方式就可以最大程度的提高資料庫并發性能,并解決讀寫沖突,和寫寫沖突導緻的問題

2、MVCC的實作原理

MVCC的目的就是多版本并發控制,在資料庫中的實作,就是為了解決讀寫沖突,它的實作原理主要是依賴記錄中的 3個隐式字段,undo日志 ,Read View 來實作的。是以我們先來看看這個三個point的概念

隐式字段

每行記錄除了我們自定義的字段外,還有資料庫隐式定義的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段

  • DB_TRX_ID 6byte,最近修改(修改/插入)事務ID:記錄建立這條記錄/最後一次修改該記錄的事務ID
  • DB_ROLL_PTR 7byte,復原指針,指向這條記錄的上一個版本(存儲于rollback segment裡)
  • DB_ROW_ID 6byte,隐含的自增ID(隐藏主鍵),如果資料表沒有主鍵,InnoDB會自動以DB_ROW_ID産生一個聚簇索引
  • 實際還有一個删除flag隐藏字段, 既記錄被更新或删除并不代表真的删除,而是删除flag變了
    資料庫之為什麼RR讀可以解決可重複讀

如上圖,DB_ROW_ID是資料庫預設為該行記錄生成的唯一隐式主鍵,DB_TRX_ID是目前操作該記錄的事務ID,而DB_ROLL_PTR是一個復原指針,用于配合undo日志,指向上一個舊版本

undo日志

undo log主要分為兩種:

  • insert undo log 代表事務在insert新記錄時産生的undo log, 隻在事務復原時需要,并且在事務送出後可以被立即丢棄
  • update undo log 事務在進行update或delete時産生的undo log; 不僅在事務復原時需要,在快照讀時也需要;是以不能随便删除,隻有在快速讀或事務復原不涉及該日志時,對應的日志才會被purge線程統一清除
  • 從前面的分析可以看出,為了實作InnoDB的MVCC機制,更新或者删除操作都隻是設定一下老記錄的deleted_bit,并不真正将過時的記錄删除。
  • 為了節省磁盤空間,InnoDB有專門的purge線程來清理deleted_bit為true的記錄。為了不影響MVCC的正常工作,purge線程自己也維護了一個read view(這個read view相當于系統中最老活躍事務的read view);如果某個記錄的deleted_bit為true,并且DB_TRX_ID相對于purge線程的read view可見,那麼這條記錄一定是可以被安全清除的。

對MVCC有幫助的實質是update undo log ,undo log實際上就是存在rollback segment中舊記錄鍊,它的執行流程如下:

一、 比如一個有個事務插入persion表插入了一條新記錄,記錄如下,name為Jerry, age為24歲,隐式主鍵是1,事務ID和復原指針,我們假設為NULL

資料庫之為什麼RR讀可以解決可重複讀

二、 現在來了一個事務1對該記錄的name做出了修改,改為Tom

  • 在事務1修改該行(記錄)資料時,資料庫會先對該行加排他鎖
  • 然後把該行資料拷貝到undo log中,作為舊記錄,既在undo log中有目前行的拷貝副本
  • 拷貝完畢後,修改該行name為Tom,并且修改隐藏字段的事務ID為目前事務1的ID, 我們預設從1開始,之後遞增,復原指針指向拷貝到undo log的副本記錄,既表示我的上一個版本就是它
  • 事務送出後,釋放鎖
    資料庫之為什麼RR讀可以解決可重複讀

三、 又來了個事務2修改person表的同一個記錄,将age修改為30歲

  • 在事務2修改該行資料時,資料庫也先為該行加鎖
  • 然後把該行資料拷貝到undo log中,作為舊記錄,發現該行記錄已經有undo log了,那麼最新的舊資料作為連結清單的表頭,插在該行記錄的undo log最前面
  • 修改該行age為30歲,并且修改隐藏字段的事務ID為目前事務2的ID, 那就是2,復原指針指向剛剛拷貝到undo log的副本記錄
  • 事務送出,釋放鎖
    資料庫之為什麼RR讀可以解決可重複讀

從上面,我們就可以看出,不同僚務或者相同僚務的對同一記錄的修改,會導緻該記錄的undo log成為一條記錄版本線性表,既連結清單,undo log的鍊首就是最新的舊記錄,鍊尾就是最早的舊記錄(當然就像之前說的該undo log的節點可能是會purge線程清除掉,向圖中的第一條insert undo log,其實在事務送出之後可能就被删除丢失了,不過這裡為了示範,是以還放在這裡)

Read View幾個屬性

  • trx_ids: 目前系統活躍(未送出)事務版本号集合。
  • low_limit_id: 建立目前read view 時“目前系統最大事務版本号+1”。
  • up_limit_id: 建立目前read view 時“系統正處于活躍事務最小版本号”
  • creator_trx_id: 建立目前read view的事務版本号;

(1)db_trx_id < up_limit_id || db_trx_id == creator_trx_id(顯示)

​ 如果資料事務ID小于read view中的最小活躍事務ID,則可以肯定該資料是在目前事務啟之前就已經存在了的,是以可以顯示。

​ 或者資料的事務ID等于creator_trx_id ,那麼說明這個資料就是目前事務自己生成的,自己生成的資料自己當然能看見,是以這種情況下此資料也是可以顯示的。

(2)db_trx_id >= low_limit_id(不顯示)

​ 如果資料事務ID大于read view 中的目前系統的最大事務ID,則說明該資料是在目前read view 建立之後才産生的,是以資料不顯示。如果小于則進入下一個判斷

(3)db_trx_id是否在活躍事務(trx_ids)中

​ 不存在:則說明read view産生的時候事務已經commit了,這種情況資料則可以顯示。

​ 已存在:則代表我Read View生成時刻,你這個事務還在活躍,還沒有Commit,你修改的資料,我目前事務也是看不見的。