1. 程式人生 > >MySQL事務隔離級別以及MVCC機制

MySQL事務隔離級別以及MVCC機制

一、事務隔離級別

SQL標準定義了4類隔離級別,包括了一些具體規則,用來限定事務內外的哪些改變是可見的,哪些是不可見的。低級別的隔離級一般支援更高的併發處理,並擁有更低的系統開銷。

1.Read Uncommitted(讀取未提交內容)

       在該隔離級別,所有事務都可以看到其他未提交事務的執行結果。本隔離級別很少用於實際應用,因為它的效能也不比其他級別好多少。讀取未提交的資料,也被稱之為髒讀(Dirty Read)。

2.Read Committed(讀取提交內容)

       這是大多數資料庫系統的預設隔離級別(但不是MySQL預設的)。它滿足了隔離的簡單定義:一個事務只能看見已經提交事務所做的改變。這種隔離級別 也支援所謂的不可重複讀(Nonrepeatable Read),因為同一事務的其他例項在該例項處理其間可能會有新的commit,所以同一select可能返回不同結果。

3.Repeatable Read(可重讀)

       這是MySQL的預設事務隔離級別,它確保同一事務的多個例項在併發讀取資料時,會看到同樣的資料行。不過理論上,這會導致另一個棘手的問題:幻讀 (Phantom Read)。簡單的說,幻讀指當用戶讀取某一範圍的資料行時,另一個事務又在該範圍內插入了新行,當用戶再讀取該範圍的資料行時,會發現有新的“幻影” 行。InnoDB和Falcon儲存引擎通過多版本併發控制(MVCC,Multiversion Concurrency Control)機制解決了該問題。

4.Serializable(可序列化)

 
       這是最高的隔離級別,它通過強制事務排序,使之不可能相互衝突,從而解決幻讀問題。簡言之,它是在每個讀的資料行上加上共享鎖。在這個級別,可能導致大量的超時現象和鎖競爭。

         這四種隔離級別採取不同的鎖型別來實現,若讀取的是同一個資料的話,就容易發生問題。例如:

         髒讀(Drity Read):某個事務已更新一份資料,另一個事務在此時讀取了同一份資料,由於某些原因,前一個RollBack了操作,則後一個事務所讀取的資料就會是不正確的。

         不可重複讀(Non-repeatable read):在一個事務的兩次查詢之中資料不一致,這可能是兩次查詢過程中間插入了一個事務更新的原有的資料。

         幻讀(Phantom Read):在一個事務的兩次查詢中資料筆數不一致,例如有一個事務查詢了幾列(Row)資料,而另一個事務卻在此時插入了新的幾列資料,先前的事務在接下來的查詢中,就會發現有幾列資料是它先前所沒有的。

         在MySQL中,實現了這四種隔離級別,分別有可能產生問題如下所示:

 

下面,將利用MySQL的客戶端程式,分別測試幾種隔離級別。測試資料庫為test,表為tx;表結構:

id                               int

num

                              int

兩個命令列客戶端分別為A,B;不斷改變A的隔離級別,在B端修改資料。

(一)、將A的隔離級別設定為read uncommitted(未提交讀)

 在B未更新資料之前:

B更新資料:

客戶端B:

客戶端A:

        經過上面的實驗可以得出結論,事務B更新了一條記錄,但是沒有提交,此時事務A可以查詢出未提交記錄。造成髒讀現象。未提交讀是最低的隔離級別。

(二)、將客戶端A的事務隔離級別設定為read committed(已提交讀)

 在B未更新資料之前:

客戶端A:

B更新資料:

客戶端B:

客戶端A:

       經過上面的實驗可以得出結論,已提交讀隔離級別解決了髒讀的問題,但是出現了不可重複讀的問題,即事務A在兩次查詢的資料不一致,因為在兩次查詢之間事務B更新了一條資料。已提交讀只允許讀取已提交的記錄,但不要求可重複讀。

(三)、將A的隔離級別設定為repeatable read(可重複讀)

 在B未更新資料之前:

客戶端A:

B更新資料:

客戶端B:

客戶端A:

B插入資料:

客戶端B:

客戶端A:

       由以上的實驗可以得出結論,可重複讀隔離級別只允許讀取已提交記錄,而且在一個事務兩次讀取一個記錄期間,其他事務部的更新該記錄。但該事務不要求與其他事務可序列化。例如,當一個事務可以找到由一個已提交事務更新的記錄,但是可能產生幻讀問題(注意是可能,因為資料庫對隔離級別的實現有所差別)。像以上的實驗,就沒有出現數據幻讀的問題。

(四)、將A的隔離級別設定為 可序列化 (Serializable)

A端開啟事務,B端插入一條記錄

事務A端:

事務B端:

因為此時事務A的隔離級別設定為serializable,開始事務後,並沒有提交,所以事務B只能等待。

事務A提交事務:

事務A端

事務B端

      

         serializable完全鎖定欄位,若一個事務來查詢同一份資料就必須等待,直到前一個事務完成並解除鎖定為止 。是完整的隔離級別,會鎖定對應的資料表格,因而會有效率的問題。

 

 

 

 

二、MVCC

  在Mysql中MVCC是在Innodb儲存引擎中得到支援的,Innodb為每行記錄都實現了三個隱藏欄位:

  • 6位元組的事務ID(DB_TRX_ID )
  • 7位元組的回滾指標(DB_ROLL_PTR
  • 隱藏的ID

6位元組的事物ID用來標識該行所述的事務,7位元組的回滾指標需要了解下Innodb的事務模型。

 

1. Innodb的事務相關概念

為了支援事務,Innbodb引入了下面幾個概念:

  • redo log
    redo log就是儲存執行的SQL語句到一個指定的Log檔案,當Mysql執行recovery時重新執行redo log記錄的SQL操作即可。當客戶端執行每條SQL(更新語句)時,redo log會被首先寫入log buffer;當客戶端執行COMMIT命令時,log buffer中的內容會被視情況重新整理到磁碟。redo log在磁碟上作為一個獨立的檔案存在,即Innodb的log檔案。
  • undo log
    與redo log相反,undo log是為回滾而用,具體內容就是copy事務前的資料庫內容(行)到undo buffer,在適合的時間把undo buffer中的內容重新整理到磁碟。undo buffer與redo buffer一樣,也是環形緩衝,但當緩衝滿的時候,undo buffer中的內容會也會被重新整理到磁碟;與redo log不同的是,磁碟上不存在單獨的undo log檔案,所有的undo log均存放在主ibd資料檔案中(表空間),即使客戶端設定了每表一個數據檔案也是如此。
  • rollback segment
    回滾段這個概念來自Oracle的事物模型,在Innodb中,undo log被劃分為多個段,具體某行的undo log就儲存在某個段中,稱為回滾段。可以認為undo log和回滾段是同一意思。

  • Innodb提供了基於行的鎖,如果行的數量非常大,則在高併發下鎖的數量也可能會比較大,據Innodb文件說,Innodb對鎖進行了空間有效優化,即使併發量高也不會導致記憶體耗盡。
    對行的鎖有分兩種:排他鎖、共享鎖。共享鎖針對對,排他鎖針對寫,完全等同讀寫鎖的概念。如果某個事務在更新某行(排他鎖),則其他事物無論是讀還是寫本行都必須等待;如果某個事物讀某行(共享鎖),則其他讀的事物無需等待,而寫事物則需等待。通過共享鎖,保證了多讀之間的無等待性,但是鎖的應用又依賴Mysql的事務隔離級別。
  • 隔離級別
    隔離級別用來限制事務直接的互動程度,目前有幾個工業標準:
    - READ_UNCOMMITTED:髒讀
    - READ_COMMITTED:讀提交
    - REPEATABLE_READ:重複讀
    - SERIALIZABLE:序列化
    Innodb對四種類型都支援,髒讀和序列化應用場景不多,讀提交、重複讀用的比較廣泛,後面會介紹其實現方式。

2. 行的更新過程

下面演示下事務對某行記錄的更新過程:

1. 初始資料行

F1~F6是某行列的名字,1~6是其對應的資料。後面三個隱含欄位分別對應該行的事務號和回滾指標,假如這條資料是剛INSERT的,可以認為ID為1,其他兩個欄位為空。

2.事務1更改該行的各欄位的值

當事務1更改該行的值時,會進行如下操作:

  • 用排他鎖鎖定該行
  • 記錄redo log
  • 把該行修改前的值Copy到undo log,即上圖中下面的行
  • 修改當前行的值,填寫事務編號,使回滾指標指向undo log中的修改前的行

3.事務2修改該行的值

與事務1相同,此時undo log,中有有兩行記錄,並且通過回滾指標連在一起。

因此,如果undo log一直不刪除,則會通過當前記錄的回滾指標回溯到該行建立時的初始內容,所幸的時在Innodb中存在purge執行緒,它會查詢那些比現在最老的活動事務還早的undo log,並刪除它們,從而保證undo log檔案不至於無限增長。

4. 事務提交

當事務正常提交時Innbod只需要更改事務狀態為COMMIT即可,不需做其他額外的工作,而Rollback則稍微複雜點,需要根據當前回滾指標從undo log中找出事務修改前的版本,並恢復。如果事務影響的行非常多,回滾則可能會變的效率不高,根據經驗值沒事務行數在1000~10000之間,Innodb效率還是非常高的。很顯然,Innodb是一個COMMIT效率比Rollback高的儲存引擎。據說,Postgress的實現恰好與此相反。

5. Insert Undo log

上述過程確切地說是描述了UPDATE的事務過程,其實undo log分insert和update undo log,因為insert時,原始的資料並不存在,所以回滾時把insert undo log丟棄即可,而update undo log則必須遵守上述過程。

3. 事務級別

眾所周知地是更新(update、insert、delete)是一個事務過程,在Innodb中,查詢也是一個事務,只讀事務。當讀寫事務併發訪問同一行資料時,能讀到什麼樣的內容則依賴事務級別:

  • READ_UNCOMMITTED
    讀未提交時,讀事務直接讀取主記錄,無論更新事務是否完成
  • READ_COMMITTED
    讀提交時,讀事務每次都讀取undo log中最近的版本,因此兩次對同一欄位的讀可能讀到不同的資料(幻讀),但能保證每次都讀到最新的資料。
  • REPEATABLE_READ
    每次都讀取指定的版本,這樣保證不會產生幻讀,但可能讀不到最新的資料
  • SERIALIZABLE
    鎖表,讀寫相互阻塞,使用較少

讀事務一般有SELECT語句觸發,在Innodb中保證其非阻塞,但帶FOR UPDATE的SELECT除外,帶FOR UPDATE的SELECT會對行加排他鎖,等待更新事務完成後讀取其最新內容。就整個Innodb的設計目標來說,就是提供高效的、非阻塞的查詢操作。

4. MVCC

上述更新前建立undo log,根據各種策略讀取時非阻塞就是MVCC,undo log中的行就是MVCC中的多版本,這個可能與我們所理解的MVCC有較大的出入,一般我們認為MVCC有下面幾個特點:

  • 每行資料都存在一個版本,每次資料更新時都更新該版本
  • 修改時Copy出當前版本隨意修改,個事務之間無干擾
  • 儲存時比較版本號,如果成功(commit),則覆蓋原記錄;失敗則放棄copy(rollback)

就是每行都有版本號,儲存時根據版本號決定是否成功,聽起來含有樂觀鎖的味道。。。,而Innodb的實現方式是:

  • 事務以排他鎖的形式修改原始資料
  • 把修改前的資料存放於undo log,通過回滾指標與主資料關聯
  • 修改成功(commit)啥都不做,失敗則恢復undo log中的資料(rollback)

二者最本質的區別是,當修改資料時是否要排他鎖定,如果鎖定了還算不算是MVCC? 

 

Innodb的實現真算不上MVCC,因為並沒有實現核心的多版本共存,undo log中的內容只是序列化的結果,記錄了多個事務的過程,不屬於多版本共存。但理想的MVCC是難以實現的,當事務僅修改一行記錄使用理想的MVCC模式是沒有問題的,可以通過比較版本號進行回滾;但當事務影響到多行資料時,理想的MVCC據無能為力了。

 

比如,如果Transaciton1執行理想的MVCC,修改Row1成功,而修改Row2失敗,此時需要回滾Row1,但因為Row1沒有被鎖定,其資料可能又被Transaction2所修改,如果此時回滾Row1的內容,則會破壞Transaction2的修改結果,導致Transaction2違反ACID。

 

理想MVCC難以實現的根本原因在於企圖通過樂觀鎖代替二段提交。修改兩行資料,但為了保證其一致性,與修改兩個分散式系統中的資料並無區別,而二提交是目前這種場景保證一致性的唯一手段。二段提交的本質是鎖定,樂觀鎖的本質是消除鎖定,二者矛盾,故理想的MVCC難以真正在實際中被應用,Innodb只是借了MVCC這個名字,提供了讀的非阻塞而已。

5.總結

也不是說MVCC就無處可用,對一些一致性要求不高的場景和對單一資料的操作的場景還是可以發揮作用的,比如多個事務同時更改使用者線上數,如果某個事務更新失敗則重新計算後重試,直至成功。這樣使用MVCC會極大地提高併發數,並消除執行緒鎖。

 

 

MYSQL MVCC實現及其機制

多版本併發控制------------------------Multiversion Concurrency Control

  大部分的MySQL的儲存引擎,比如InnoDB,Falcon,以及PBXT並不是簡簡單單的使用行鎖機制。它們都使用了行鎖結合一種提高併發的技術,被稱為MVCC(多版本併發控制)。MVCC並不單單應用在MySQL中,其他的資料庫如Oracle,PostgreSQL,以及其他資料庫也使用這個技術。

  MVCC避免了許多需要加鎖的情形以及降低消耗。這取決於它實現的方式,它允許非阻塞讀取,在寫的操作的時候阻塞必要的記錄。

  MVCC儲存了某一時刻資料的一個快照。意思就是無論事物運行了多久,它們都能看到一致的資料。也就是說在相同的時間下,不同的事物看相同表的資料是不同的。如果你從來沒有這方面的經驗,可能說這些有點令人困惑。但是在以後這個會很容易理解和熟悉的。

  每個儲存引擎實現MVCC方式都是不同的。有許多種包含了樂觀(optimistic)和悲觀(pessimistic)的併發控制。我們用簡單的InnoDb的行為來舉例說明MVCC工作方式。

   InnoDB實現MVCC的方法是,它儲存了每一行的兩個額外的隱藏欄位,這兩個隱藏欄位分別記錄了行的建立的時間和刪除的時間。在每個事件發生的時 候,每行儲存版本號,而不是儲存事件實際發生的時間。每次事物的開始這個版本號都會增加。自記錄時間開始,每個事物都會儲存記錄的系統版本號。依照事物的 版本來檢查每行的版本號。在事物隔離級別為可重複讀的情況下,來看看怎樣應用它。

  SELECT

  InnoDB檢查每行,要確定它符合兩個標準。

  InnoDB必須知道行的版本號,這個行的版本號至少要和事物版本號一樣的老。(也就是是說它的版本號可能少於或者和事物版本號相同)。這個既能確定事物開始之前行是存在的,也能確定事物建立或修改了這行。

  行的刪除操作的版本一定是未定義的或者大於事物的版本號。確定了事物開始之前,行沒有被刪除。

  符合了以上兩點。會返回查詢結果。

  INSERT

  InnoDB記錄了當前新增行的系統版本號。

  DELETE

  InnoDB記錄的刪除行的系統版本號作為行的刪除ID。

  UPDATE

  InnoDB複製了一行。這個新行的版本號使用了系統版本號。它也把系統版本號作為了刪除行的版本。

  所有其他記錄的結果儲存是,從未獲得鎖的查詢。這樣它們查詢的資料就會盡可能的快。要確定查詢行要遵循這些標準。缺點是儲存引擎要為每一行儲存更多的資料,檢查行的時候要做更多的處理以及其他內部的一些操作。

  MVCC只能在可重複讀和可提交讀的隔離級別下生效。不可提交讀不能使用它的原因是不能讀取符合事物版本的行版本。它們總是讀取最新的行版本。可序列化不能使用MVCC的原因是,它總是要鎖定行。

  下面的表說明了在MySQL中不同鎖的模式以及併發級別。

鎖的策略 併發性 開銷 引擎
最低 最低 MyISAM,Merge,Memory
NDB Cluster
行和MVCC 最高 最高 InnoDB,Falcon,PBXT,solidD

也即使說,對某行的寫操作會阻塞所有對該行的讀取操作,對某行的讀操作會阻塞所有對該行的寫操作,在系統存在讀、寫併發時,不論系統IO能力有多高,會受限於鎖而導致效能低下。

MVCC用於解決這個問題來提高系統性能,MVCC並沒有統一的標準,各個資料庫實現均採用不同方式來實現MVCC,InnoDB的實現方式如下:

準備工作:

(1)對每行記錄增加行標誌和刪除標誌兩個欄位;

(2)維護一個全域性的系統版本號,每開始一個事務(注意select也是事務,讀事務),將該系統版本號加1並作為事務的版本號

插入記錄的行標誌設定為本事務版本號,刪除標誌為空;

刪除記錄的刪除標誌設定為本事務版本號;

修改的處理過程:將原記錄的刪除版本號修改為本事務版本號;新插入一條記錄,包含原記錄資料及本次修改,行記錄標誌設定為本事務版本號,刪除標誌為空;

讀取的處理過程:

僅讀取同時滿足以下條件的記錄行:

(1)行標誌小於或等於本事務版本號(等於用於保證能夠讀取到本事務內提交的增加);

(2)刪除標誌為空或者大於本事務版本號(不包括等於以保證不會讀取到本事務刪除的記錄);

相當於在讀事務開始的時刻點,建立了一個系統的快照,該事務讀取的所有資料,均是從快照中讀取的,因此滿足可重複讀的條件,並且可解決幻讀的問題,並且也不會讀到產生“同樣查詢條件,事務中第一次讀到的記錄數大於第二次讀到的記錄數的問題“(由併發刪除引起)

從上可知,使用MVCC後,大部分讀都不再需要加讀鎖,因此讀不再阻塞寫,寫也不再阻塞讀。讀操作只再受限於系統IO能力。

 

MVCC多版本併發控制

原理:

mvcc提供基於某個時間的快照,使得對於事務看來,總是可以提供與事務開始時刻相一致的資料,而不管這個事

務執行的時間有多長,故在不同事務看來,同一時刻看到的相同的行資料可能是不一樣的,即:每一行資料會有

多個版本資料(副本)

InnoDB中每行隱含2個欄位:更新或修改版本號和刪除版本號(可以為空),每一個事務開始也有自己的版本號且是遞增(

類似於SCN)

以select,delete,insert update語句來說明:

1)select 同時滿足2個條件的行,才能被返回:

*行的被修改版本號<=該版本號

*行的被刪除版本號要麼沒有被定義,要麼大於事務的版本號:行的刪除版本號如沒被定義,說明行沒有被刪除過;如刪除

版本號>當前事務的版本號,說明該行的是被該事務的後面啟動事務刪除過(接著看下去..)

2)insert

對新插入的行, 行的更新版本被修改為該事務的版本號

3)delete

對於刪除,innodb直接把該行的被刪除版本號設定為當前事務版本號,相當於標記刪除,不是實際刪除

4)update

在更新行的時候,innodb會把原來的行復制一份到回滾端的表空間中,若成功,並把當前事務的版本號作為該行

的更新版本號,否則rollback;

 

mvcc優缺點:

在讀取資料時,innodb幾乎不用獲取任何鎖,在每個查詢通過版本檢查,只獲取需要的資料版本,提高系統併發度

缺點:為了實現多版本,innodb必須對每行增加相應欄位來儲存版本資訊,同時需要維護每一行的版本資訊,而且

在檢索行的時候,需要進行版本的比較,因而減低了查詢效率;innodb還需要定期清理不再需要的行版本,及時回收

空間,這也增加開銷;

 

innodb支援事務隔離級別:

1)read uncommitted: (讀沒有提交的資料),無法避免髒讀;

2)read committed: (只能讀提交的資料),其他事務對資料庫的修改,只能已提交,其修改的結果可以看見,與這2個事務

開始的先後順序無關,這個級別避免髒讀,無法實現可重複讀,可能會產生幻讀

不可重複讀:   t1:讀取一行 t2:再讀取這行時,可能被修改了,看不到啦

幻讀:  t1:讀取有一行,   t2:再讀取相同資料時,比t1時間多了資料

3)repeatable read:(可重複讀), 只能讀取在它開始之前提交事務對資料庫的修改,在它開始之後,所有其他事務對資料庫

的修改對它來說均不可見.