MySQL高級 之 事務(ACID特性 與 隔離級別)

什麼是事務?

事務就是DBMS當中用戶程序的任何一次執行,事務是DBMS能看到的基本修改單元。

事務是指對系統進行的一組操作,爲了保證系統的完整性,事務需要具有ACID特性。即原子性(atomicity),一致性(consistency),隔離性(isolation),持久性(durability)。

MySQL事務實現機制

MySQL提供了兩種事務型的存儲引擎:InnoDB和NDB Cluster(主要用於MySQL Cluster 分佈式集羣環境)。另外還有一些第三方存儲引擎也支持事務,比較知名的包括XtraDB和PBXT。下面以InnoDB來說明。

事務日誌

MySQL會最大程度的使用緩存機制來提高數據庫的訪問效率,但是萬一數據庫發生斷電,因爲緩存的數據沒有寫入磁盤,導致緩存在內存中的數據丟失而導致數據不一致怎麼辦?

InnoDB主要是通過事務日誌實現ACID特性,事務日誌可以幫助提高事務的效率。使用事務日誌,存儲引擎在修改表的數據時只需要修改其內存拷貝,再把該修改行爲記錄到持久在硬盤上的事務日誌中,而不用每次都將修改的數據本身持久到磁盤。

事務日誌採用追加的方式,因此寫日誌的操作是磁盤上一小塊區域的順序I/O,而不像隨機I/O需要在磁盤的多個地方移動磁頭。所以事務日誌的方式相對來說要快得多。事務日誌持久以後,內存中被修改的數據在後臺可以慢慢的刷回磁盤。目前大多數存儲引擎是這樣實現的,我們稱之爲預寫式日誌,即修改數據需要寫兩次磁盤。

Redo、Undo、Checkpoint

事務日誌包括重做日誌redo和回滾日誌undo

redo記錄的是已經全部完成的事務,就是執行了commit的事務,記錄文件是ib_logfile0 、ib_logfile1。

Undo記錄的是已部分完成並且寫入硬盤的未完成的事務,默認情況下回滾日誌是記錄在表空間中的(共享表空間或者獨享表空間)。

一般情況下,mysql在崩潰之後,重啓服務,innodb通過回滾日誌undo將所有已完成並寫入磁盤的未完成事務進行rollback,然後將redo中的事務全部重新執行一遍即可恢復數據,但是隨着redo的量增加,每次從redo的第一條開始恢復就會浪費長的時間,所以引入了checkpoint機制。

一般業務運行過程中,當業務需要對某張表的某行數據進行修改的時候,innodb會先將該數據從磁盤讀取到緩存中去,然後在緩存中對這條數據進行修改,這樣緩存中的數據就和磁盤的數據不一致了,這個時候緩存中的數據就稱爲dirty page,只有當髒頁統一刷新到磁盤中才會是clean page

Checkpoint:如果在某個時間點,髒頁的數據被刷新到了磁盤,系統就把這個刷新的時間點記錄到redo log的結尾位置,在進行恢復數據的時候,checkpoint時間點之前的數據就不需要進行恢復了,可以縮短時間。

Innodb_log_buffer_size:redo(重做日誌)緩存大小

Innodb_log_file_size:redo(重做日誌)log文件大小,文件越大數據恢復的時間越長

Innodb_log_file_group: redo(重做日誌)og文件數量,默認是2個 :ib_logfile0、ib_logfile1

事務日誌的的機制實際上是滿足的事務的原子性和持久性,即要麼都成功,要麼都失敗。而談到事務的一致性和隔離性,就要談到“鎖”了。

InnoDB採用的是兩階段鎖定協議。在事務執行過程中,隨時都可以執行鎖定,鎖只有在執行COMMIT或ROLLBACK時纔會釋放,並且所有的鎖同一時刻釋放。InnoDB會根據隔離級別在需要的時候自動加鎖。

共享鎖:由讀表操作加上的鎖,加鎖後其他用戶只能獲取該表或行的共享鎖,不能獲取排它鎖,也就是說只能讀不能寫

排它鎖:由寫表操作加上的鎖,加鎖後其他用戶不能獲取該表或行的任何鎖,典型是mysql事務中

start transaction;

select * from user where userId = 1 for update;

執行完這句以後

  1)當其他事務想要獲取共享鎖,比如事務隔離級別爲SERIALIZABLE的事務,執行

select * from user;

  將會被掛起,因爲SERIALIZABLE的select語句需要獲取共享鎖

  2)當其他事務執行

  select * from user where userId = 1 for update;

  update user set userAge = 100 where userId = 1; 

  也會被掛起,因爲for update會獲取這一行數據的排它鎖,需要等到前一個事務釋放該排它鎖纔可以繼續進行

鎖的範圍:

行鎖: 對某行記錄加上鎖

表鎖: 對整個表加上鎖

這樣組合起來就有,行級共享鎖,表級共享鎖,行級排他鎖,表級排他鎖

MVCC

MVCC 全稱 Multi-Version Concurrency Control,多版本併發控制,也可稱之爲一致性非鎖定讀;它通過行的多版本控制方式來讀取當前執行時間數據庫中的行數據。實質上使用的是快照數據。

爲什麼要使用MVCC

消除鎖的開銷;這個較好理解,如果要保證數據的一致性,最簡單的方式就是對操作數據進行加鎖,但是加鎖不可避免的會有鎖開銷。所以,如果有能避免進行加鎖的方式當然是最好的。

提高併發度。

MVCC與事務隔離級別

在InnoDB事務隔離級別“讀已提交”與“可重複讀”下 ,InnoDB存儲引擎使用MVCC機制(非鎖定一致性讀)。在“讀已提交”事務隔離級別下,對於快照數據,MVCC讀總是讀取被鎖定行的最新的快照數據。而“可重複讀”讀到的總是讀取事務開始時的行數據版本。

MVCC原理

對於“可重複讀”讀到的總是讀取事務開始時的行數據版本,那MVCC機制是如何保證可重複讀的?其實就是在每一行記錄的後面增加兩個隱藏列,記錄創建事務ID(創建版本號)和刪除事務ID(刪除版本號),在每次執行新的事務時,都會更新並且遞增唯一的事務ID。

SELECT
InnoDB會根據以下兩個條件檢查每行記錄:
a. InnoDB只查找版本早於當前事務版本的數據行(也就是,行的系統版本號<=事務的系統版本好),這樣可 以確保事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或修改過的。
b. 行的刪除版本要麼未定義,要麼大於當前事務版本號。這樣可以確保事務讀取到的行,在事務開始之前未被 刪除。
只有符合上述兩個條件的記錄,才能返回作爲查詢結果。

INSERT
InnoDB爲新插入的每一行保存當前系統版本號作爲行版本號。

DELETE
InnoDB爲刪除的每一行保存當前系統版本號作爲刪除標識。

UPDATE
InnoDB爲插入一行新記錄,保存當前系統版本號作爲行版本號,同時保存當前系統版本號到原來的行作爲行 刪除標識。

保存這兩個額外系統版本號,使大多數讀操作都可以不用加鎖。這樣讀操作簡單而性能好,也能保證讀到符合標準的行。不足之處是需要額外的存儲空間,需要做更多的行檢查工作,以及一些額外的維護工作。另外,MVCC只在REPEATABLE READ和READ COMMITED兩個隔離級別下工作。其他兩個隔離級別都和MVCC不兼容,因爲READ UNCOMMITED縱是讀取到最新的數據行,而不是符合當前事務版本的數據行,而SERIALIZABLE則會對所有讀取的行都加鎖。

ACID特性

1. 原子性(Atomicity)

事務是一個原子操作單元,一個事務包含多個操作,這些操作要麼全部執行,要麼全都不執行。實現事務的原子性,要支持回滾操作,在某個操作失敗後,回滾到事務執行之前的狀態。

回滾實際上是一個比較高層抽象的概念,大多數DB在實現事務時,是在事務操作的數據快照上進行的(比如,MVCC),並不修改實際的數據,如果有錯並不會提交,所以很自然的支持回滾。

而在其他支持簡單事務的系統中,不會在快照上更新,而直接操作實際數據。可以先預演一邊所有要執行的操作,如果失敗則這些操作不會被執行,通過這種方式很簡單的實現了原子性。

2. 一致性(Consistency)

數據庫一致性(Database Consistency)是指事務執行的結果必須是使數據庫從一個一致性狀態變到另一個一致性狀態。通俗的理解爲:一次事務對於一次數據庫狀態變更,一次事務只能體現在一個狀態中不可能跨越數據庫的幾次狀態,數據庫的一個狀態只能有一個事務變更數據。

數據庫狀態如何變化?

每一次數據變更就會導致數據庫的狀態遷移。如果數據庫的初始狀態是C0,第一次事務T1的提交就會導致系統生成一個SYSTEM CHANGE NUMBER(SCN),這時數據庫狀態從C0轉變成C1。執行第二個事務T2的時候數據庫狀態從C1變成C2,以此類推,執行第Tn次事務的時候數據庫狀態由C(n-1)變成Cn。

一致寫

一致寫:事務執行的數據變更只能基於上一個一致的狀態,且只能體現在一個狀態中。T(n)的變更結果只能基於C(n-1),C(n-2), …C(1)狀態,且只能體現在C(n)狀態中。也就是說,一個狀態只能有一個事務變更數據,不允許有2個或者2個以上事務在一個狀態中變更數據。至於具體一致寫基於哪個狀態,需要判斷T(n)事務是否和T(n-1),T(n-2),…T(1)有依賴關係。

舉個栗子:

定義100個事務T(1)…T(100)實現相同的邏輯 update table set i=i+1,i的初始值是0,那麼併發執行這100個事務之後i的值是多少?可能很容易想到是100。那麼怎麼從一致性角度去理解呢?

數據庫隨機調度到T(50)執行,此時數據庫狀態是C(0),而其它事務都和T(50)有依賴關係,根據寫一致性原理,其它事務必須等到T(50)執行完畢後數據庫狀態變爲C(1)纔可以執行。因此數據庫利用鎖機制阻塞其它事務的執行。直到T(50)執行完畢,數據庫狀態從C(0)遷移到C(1)。數據庫喚醒其它事務後隨機調度到T(89)執行,以此類推直到所有事務調度執行完畢,數據庫狀態最終變爲C(100)。

一致讀

一致讀:事務讀取數據只能從一個狀態中讀取,不能從2個或者2個以上狀態讀取。也就是T(n)只能從C(n-1),C(n-2)… C(1)中的一個狀態讀取數據,不能一部分數據讀取自C(n-1),而另一部分數據讀取自C(n-2)。

舉個栗子:

還是上面的例子,假設T(1)…T(100)順序執行,在不同的時機執行select i from table,我們看到i的值是什麼?
T(1)的執行過程中。數據庫狀態尚未遷移,讀到的i=0。T(1)執行完畢,T(2)的執行過程中,數據庫狀態遷移至C(1),讀到的i=1。

3. 隔離性(Isolation)

數據庫系統提供一定的隔離機制,保證事務在不受外部併發操作影響的“獨立”環境執行。這意味着事務處理過程中的中間狀態對外部是不可見的。隔離級別指併發事務之間互相影響的程度,根據影響程度的不同分爲四種隔離級別。

四種事務隔離級別

未提交讀(Read Uncommitted):一個事務可以讀到另一個事務未提交的結果。所有的併發事務問題都會發生。允許髒讀。

提交讀(Read Committed):只有在事務提交後,其更新結果纔會被其他事務看見。可以解決髒讀問題。Oracle等多數數據庫默認都是該級別。

可重複讀(Repeated Read):在一個事務中,對於同一份數據的讀取結果總是相同的,無論是否有其他事務對這份數據進行操作,以及這個事務是否提交。可以解決髒讀、不可重複讀。InnoDB默認級別

串行讀(Serializable):事務串行化執行,每次讀都需要獲得表級共享鎖,讀寫相互都會阻塞,隔離級別最高,犧牲了系統的併發性。可以解決併發事務的所有問題。

三種併發問題

髒讀:事務A修改了一個數據,但未提交,事務B讀到了事務A未提交的更新結果,如果事務A提交失敗,事務B讀到的就是髒數據。

不可重複讀:在同一個事務中,對於同一份數據讀取到的結果不一致。比如,事務B在事務A提交前讀到的結果,和事務A提交後讀到的結果可能不同。不可重複讀出現的原因就是事務併發修改記錄,要避免這種情況,最簡單的方法就是對要修改的記錄加鎖,這回導致鎖競爭加劇,影響性能。另一種方法是通過MVCC可以在無鎖的情況下,避免不可重複讀。

幻讀:在同一個事務中,同一個查詢多次返回的結果不一致。事務A新增了一條記錄,事務B在事務A提交前後各執行了一次查詢操作,發現後一次比前一次多了一條記錄。幻讀是由於併發事務增加記錄導致的,這個不能像不可重複讀通過記錄加鎖解決,因爲對於新增的記錄根本無法加鎖。需要將事務串行化,才能避免幻讀。

髒讀、不可重複讀主要是事務B裏面修改了數據
幻讀主要是事務B裏面新增了數據

隔離級別 髒讀(Dirty Read) 不可重複讀(NonRepeatable Read) 幻讀(Phantom Read)
未提交讀(Read uncommitted) 可能 可能 可能
提交讀(Read committed) 不可能 可能 可能
可重複讀(Repeatable read) 不可能 不可能 可能
串行讀(Serializable ) 不可能 不可能 不可能

《高性能mysql》一書中的說明:

這裏寫圖片描述這裏寫圖片描述這裏寫圖片描述

修改事務隔離級別的方法

1.全局修改,修改mysql.ini配置文件

#可選參數有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE
2 [mysqld]
3 transaction-isolation = REPEATABLE-READ

2.對當前session修改,在登錄mysql客戶端後,執行命令

SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

舉個幾個栗子

髒讀

當一個事務正在訪問數據,並且對數據進行了修改,而這種修改還沒有提交到數據庫中,這時,另外一個事務也訪問這個數據,然後使用了這個數據。

session1 session2
foo foo
這裏寫圖片描述
//事務1更新了數據
這裏寫圖片描述
//默認隔離級別:可重複讀(Repeatable read)下,未出現髒讀

//事務1尚未提交
這裏寫圖片描述
//修改session2的隔離級別爲:未提交讀(Read uncommitted),出現髒讀

結論:session2在READ-UNCOMMITTED下讀取到session 1中未提交事務修改的數據。

不可重複讀

在一個事務內,多次讀同一數據。在這個事務還沒有結束時,另外一個事務也訪問該同一數據。那麼,在第一個事務中的兩次讀數據之間,由於第二個事務的修改,那麼第一個事務兩次讀到的的數據可能是不一樣的。這樣就發生了在一個事務內兩次讀到的數據是不一樣的,因此稱爲是不可重複讀。

session1 session2
這裏寫圖片描述
//開啓事務1,隔離級別:已提交讀(Read committed),查詢結果只有一條數據
這裏寫圖片描述
//默認隔離級別下,開啓事務2,新增一條數據,並提交
這裏寫圖片描述
//讀到了事務2提交的數據,和第一次的結果不一樣,出現了不可重複讀
-

可重複讀

session1 session2
這裏寫圖片描述 這裏寫圖片描述
這裏寫圖片描述
//和第一次的結果一樣,REPEATABLE-READ級別可以重複讀
-
這裏寫圖片描述
//commit session1之後 再查詢可以看到session2插入的數據3了
-

幻讀

情況一:

第一個事務對一個表中的數據進行了修改,這種修改涉及到表中的全部數據行。同時,第二個事務也修改這個表中的數據,這種修改是向表中插入一行新數據。那麼,以後就會發生操作第一個事務的用戶發現表中還有沒有修改的數據行,就好象發生了幻覺一樣。

session1 session2
這裏寫圖片描述 這裏寫圖片描述
這裏寫圖片描述 -

session1 中事務第一次讀取出3行,做了一次更新後,session2事務裏提交的數據就出現了。可以看做是一種幻讀。

情況二:

session1 session2
這裏寫圖片描述 這裏寫圖片描述
這裏寫圖片描述 -
這裏寫圖片描述 -

以爲表裏沒有數據,其實數據已經存在了,傻乎乎的提交後,才發現數據衝突了。可以看做是另一種幻讀。

當隔離級別是可重複讀,且禁用innodb_locks_unsafe_for_binlog的情況下,在搜索和掃描index的時候使用的next-key locks可以避免幻讀

4. 持久性(Durability)

事務提交後,對數據的修改、系統的影響是永久的。即使出現系統故障也能夠保持。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章