1. 程式人生 > 實用技巧 >mysql 5 事務詳解

mysql 5 事務詳解

一 概念

事務(Transaction)是訪問和更新資料庫的程式執行單元;事務中可能包含一個或多個sql語句,這些語句要麼都執行,要麼都不執行。作為一個關係型資料庫,MySQL支援事務,本文介紹基於MySQL5.6。

一個經典案例說明事務

銀行引用是事務的一個經典例子:假如銀行有兩張表,一張支票表,一張儲蓄表,現在需要從Jones使用者的支票賬戶轉移200¥ 至儲蓄賬戶,那麼至少需要三步:
    1. 檢查Jones的支票賬戶餘額是否大於200¥
    2. Jones的支票賬戶-2003. Jones的儲蓄賬戶+200¥
上述三步可組成一個事務,當2、3步故障時,之前執行的操作會自動回滾,保證資料的一致性。

二 事務的ACID特性

ACID是衡量事務的四個特性:

  • 原子性(Atomicity,或稱不可分割性):語句要麼全執行,要麼全不執行,是事務最核心的特性,事務本身就是以原子性來定義的;實現主要基於undo log
  • 永續性(Durability):保證事務提交後不會因為宕機等原因導致資料丟失;實現主要基於redo log
  • 隔離性(Isolation):保證事務執行儘可能不受其他事務影響;InnoDB預設的隔離級別是RR,RR的實現主要基於鎖機制(包含next-key lock)、MVCC(包括資料的隱藏列、基於undo log的版本鏈、ReadView)
  • 一致性(Consistency):事務追求的最終目標,一致性的實現既需要資料庫層面的保障,也需要應用層面的保障

1 原子性

定義

原子性是指一個事務是一個不可分割的工作單位,其中的操作要麼都做,要麼都不做;如果事務中一個sql語句執行失敗,則已執行的語句也必須回滾,資料庫退回到事務前的狀態。

實現原理:undo log

在說明原子性原理之前,首先介紹一下MySQL的事務日誌。MySQL的日誌有很多種,如二進位制日誌、錯誤日誌、查詢日誌、慢查詢日誌等,此外InnoDB儲存引擎還提供了兩種事務日誌redo log(重做日誌)和undo log(回滾日誌)。其中redo log用於保證事務永續性;undo log則是事務原子性和隔離性實現的基礎。

下面說回undo log。實現原子性的關鍵,是當事務回滾時能夠撤銷所有已經成功執行的sql語句。InnoDB

實現回滾,靠的是undo log:當事務對資料庫進行修改時,InnoDB會生成對應的undo log;如果事務執行失敗或呼叫了rollback,導致事務需要回滾,便可以利用undo log中的資訊將資料回滾到修改之前的樣子。

undo log屬於邏輯日誌,它記錄的是sql執行相關的資訊。當發生回滾時,InnoDB會根據undo log的內容做與之前相反的工作:對於每個insert,回滾時會執行delete;對於每個delete,回滾時會執行insert;對於每個update,回滾時會執行一個相反的update,把資料改回去。

以update操作為例:當事務執行update時,其生成的undo log中會包含被修改行的主鍵(以便知道修改了哪些行)、修改了哪些列、這些列在修改前後的值等資訊,回滾時便可以使用這些資訊將資料還原到update之前的狀態。

2 永續性

1)定義

永續性是指事務一旦提交,它對資料庫的改變就應該是永久性的。接下來的其他操作或故障不應該對其有任何影響。

2). 實現原理:redo log

redo log和undo log都屬於InnoDB的事務日誌。下面先聊一下redo log存在的背景。

InnoDB作為MySQL的儲存引擎,資料是存放在磁碟中的,但如果每次讀寫資料都需要磁碟IO,效率會很低。為此,InnoDB提供了快取(Buffer Pool),Buffer Pool中包含了磁碟中部分資料頁的對映,作為訪問資料庫的緩衝:當從資料庫讀取資料時,會首先從Buffer Pool中讀取,如果Buffer Pool中沒有,則從磁碟讀取後放入Buffer Pool;當向資料庫寫入資料時,會首先寫入Buffer Pool,Buffer Pool中修改的資料會定期重新整理到磁碟中(這一過程稱為刷髒)。

Buffer Pool的使用大大提高了讀寫資料的效率,但是也帶了新的問題:如果MySQL宕機,而此時Buffer Pool中修改的資料還沒有重新整理到磁碟,就會導致資料的丟失,事務的永續性無法保證。

於是,redo log被引入來解決這個問題:當資料修改時,除了修改Buffer Pool中的資料,還會在redo log記錄這次操作;當事務提交時,會呼叫fsync介面對redo log進行刷盤(redo log內容刷到磁碟上)。如果MySQL宕機,重啟時可以讀取redo log中的資料,對資料庫進行恢復。redo log採用的是WAL(Write-ahead logging,預寫式日誌),所有修改先寫入日誌,再更新到Buffer Pool,保證了資料不會因MySQL宕機而丟失,從而滿足了永續性要求。

既然redo log也需要在事務提交時將日誌寫入磁碟,為什麼它比直接將Buffer Pool中修改的資料寫入磁碟(即刷髒)要快呢?主要有以下兩方面的原因:

(1)刷髒是隨機IO,因為每次修改的資料位置隨機,但寫redo log是追加操作,屬於順序IO。

(2)刷髒是以資料頁(Page)為單位的,MySQL預設頁大小是16KB,一個Page上一個小修改都要整頁寫入;而redo log中只包含真正需要寫入的部分,無效IO大大減少。

3). redo log與binlog

我們知道,在MySQL中還存在binlog(二進位制日誌)也可以記錄寫操作並用於資料的恢復,但二者是有著根本的不同的:

(1)作用不同:redo log是用於crash recovery的,保證MySQL宕機也不會影響永續性;binlog是用於point-in-time recovery的,保證伺服器可以基於時間點恢復資料,此外binlog還用於主從複製。

(2)層次不同:redo log是InnoDB儲存引擎實現的,而binlog是MySQL的伺服器層(可以參考文章前面對MySQL邏輯架構的介紹)實現的,同時支援InnoDB和其他儲存引擎。

(3)內容不同:redo log是物理日誌,內容基於磁碟的Page;binlog的內容是二進位制的,根據binlog_format引數的不同,可能基於sql語句、基於資料本身或者二者的混合。

(4)寫入時機不同:binlog在事務提交時寫入;redo log的寫入時機相對多元:

  • 前面曾提到:當事務提交時會呼叫fsync對redo log進行刷盤;這是預設情況下的策略,修改innodb_flush_log_at_trx_commit引數可以改變該策略,但事務的永續性將無法保證。
  • 除了事務提交時,還有其他刷盤時機:如master thread每秒刷盤一次redo log等,這樣的好處是不一定要等到commit時刷盤,commit速度大大加快

3、隔離性

1). 定義

與原子性、永續性側重於研究事務本身不同,隔離性研究的是不同事務之間的相互影響。隔離性是指,事務內部的操作與其他事務是隔離的,併發執行的各個事務

之間不能互相干擾。嚴格的隔離性,對應了事務隔離級別中的Serializable (可序列化),但實際應用中出於效能方面的考慮很少會使用可序列化。

通過鎖機制來實現,同一時間鎖住的資源只能被一個執行緒(會話)訪問

4、一致性

1). 基本概念

一致性是指事務執行結束後,資料庫的完整性約束沒有被破壞,事務執行的前後都是合法的資料狀態。資料庫的完整性約束包括但不限於:實體完整性(如行的主鍵存在且唯一)、列完整性(如欄位的型別、大小、長度要符合要求)、外來鍵約束、使用者自定義完整性

如轉賬前後,兩個賬戶餘額的和應該不變,A賬號給B賬號轉賬500 A減少500 B只能增加500 不能說因為網路問題轉了2次500 ,一致性就要求,A-500 B+500

2). 實現

可以說,一致性是事務追求的最終目標:前面提到的原子性、永續性和隔離性,都是為了保證資料庫狀態的一致性。此外,除了資料庫層面的保障,一致性的實現也需要應用層面進行保障。

實現一致性的措施包括:

  • 保證原子性、永續性和隔離性,如果這些特性無法保證,事務的一致性也無法保證
  • 資料庫本身提供保障,例如不允許向整形列插入字串值、字串長度不能超過列的限制等
  • 應用層面進行保障,例如如果轉賬操作只扣除轉賬者的餘額,而沒有增加接收者的餘額,無論資料庫實現的多麼完美,也無法保證狀態的一致

三 事務隔離級別

SQL標準中定義了四種隔離級別,並規定了每種隔離級別下上述幾個問題是否存在。一般來說,隔離級別越低,系統開銷越低,可支援的併發越高,但隔離性也越差。隔離級別與讀問題的關係如下:

  • 讀未提交:read uncommitted 可能產生髒讀
  • 讀已提交:read committed 不可重複讀(可能讀到別的事務改變後的資料)
  • 可重複讀:repeatable read mysql預設隔離級別,讀當前事務的版本資料,但可能產生幻讀(讀到別的事務新增/刪除後的資料)
  • 序列化:serializable 能解決以上所有問題,包括幻讀問題 (鎖表機制實現的)

髒讀:

一個事務讀取到另外一個事務未提交的資料。

例子:A向B轉賬,A執行了轉賬語句,但A還沒有提交事務,B讀取資料,發現

自己賬戶錢變多了!B跟A說,我已經收到錢了。A回滾事務【rollback】,等B再檢視賬戶的錢時,發現錢並沒有多...

讀取的資料被別的事務回滾了

出現髒讀的本質就是因為操作(修改)完該資料就立馬釋放掉鎖,導致讀的資料就變成了無用的或者是錯誤的資料

不可重複讀

一個事務讀取到另外一個事務已經提交的資料,也就是說一個事務可以看到其他事務所做的修改,例如:A查詢資料庫得到

資料,B去修改資料庫的資料,導致A多次查詢資料庫的結果都不一樣【危害:A每次查詢的結果都是受B的影響的,那麼A查詢出來的資訊就沒有意思了】

虛讀(幻讀)

是指在一個事務內讀取到了別的事務插入的資料,導致前後讀取不一致。和不可重複讀類似,但虛讀(幻讀)會讀到其他事務的插入的資料,導致前後讀取不

一致,幻讀的重點在於新增或者刪除 (資料條數變化),不可重複讀的重點是修改.

RR解決髒讀、不可重複讀、幻讀等問題,使用的是MVCC

四 MVCC (多版本的併發控制協議)

RR解決髒讀、不可重複讀、幻讀等問題,使用的是MVCC:MVCC全稱Multi-Version Concurrency Control,即多版本的併發控制協議。

下面的例子很好的體現了MVCC的特點

在同一時刻,不同的事務讀取到的資料可能是不同的(即多版本)——在T5時刻,事務A和事務C可以讀取到不同版本的資料。

MVCC最大的優點是讀不加鎖,因此讀寫不衝突,併發效能好。InnoDB實現MVCC,多個版本的資料可以共存,主要基於以下技術及資料結構:

1)隱藏列:InnoDB中每行資料都有隱藏列,隱藏列中包含了本行資料的事務id、指向undo log的指標等。

2)基於undo log的版本鏈:前面說到每行資料的隱藏列中包含了指向undo log的指標,而每條undo log也會指向更早版本的undo log,從而形成一條版本鏈。

3)ReadView:通過隱藏列和版本鏈,MySQL可以將資料恢復到指定版本;但是具體要恢復到哪個版本,則需要根據ReadView來確定。所謂ReadView,是指事務(記做事務A)在某一時刻給整個事務系統(trx_sys)打快照,之後再進行讀操作時,會將讀取到的資料中的事務id與trx_sys快照比較,從而判斷資料對該ReadView是否可見,即對事務A是否可見。

上面第3條也可能很好的解釋了,上圖案例中為什麼事務C查詢到結果是200,因為在查詢之前事務給整個系統打的快照裡,餘額已經是200了(被事務B修改為了200)

mvcc基本特徵

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

InnoDB儲存引擎MVCC的實現策略

在每一行資料中額外儲存兩個隱藏的列:當前行建立時的版本號刪除時的版本號(可能為空,其實還有一列稱為回滾指標,用於事務回滾,不在本文範疇)。這裡的版本號並不是實際的時間值,而是系統版本號。每開始新的事務,系統版本號都會自動遞增。事務開始時刻的系統版本號會作為事務的版本號,用來和查詢每行記錄的版本號進行比較。

每個事務又有自己的版本號,這樣事務內執行CRUD操作時,就通過版本號的比較來達到資料版本控制的目的。

MVCC下InnoDB的增刪查改是怎麼work的

1.插入資料(insert):記錄的版本號即當前事務的版本號

執行一條資料語句:insert into testmvcc values(1,"test");

假設事務id為1,那麼插入後的資料行如下:

2、更新資料: 採用的是先標記舊的那行記錄為已刪除,並且刪除版本號是事務版本號,然後插入一行新的記錄的方式。

比如,針對上面那行記錄,事務Id為2 要把name欄位更新

update table set name= 'new_value' where id=1;

3、刪除資料: 就把事務版本號作為刪除版本號。比如

delete from table where id=1;

4、查詢操作:

從上面的描述可以看到,在查詢時要符合以下兩個條件的記錄才能被事務查詢出來:

1) 刪除版本號未指定或者大於當前事務版本號,即查詢事務開啟後確保讀取的行未被刪除。(即上述事務id為2的事務查詢時,依然能讀取到事務id為3所刪除的資料行)

2) 建立版本號 小於或者等於 當前事務版本號 ,就是說記錄建立是在當前事務中(等於的情況)或者在當前事務啟動之前的其他事物進行的insert。

(即事務id為2的事務只能讀取到create version<=2的已提交的事務的資料集)

關於Mysql中MVCC的總結

客觀上,我們認為他就是樂觀鎖的一整實現方式,就是每行都有版本號,儲存時根據版本號決定是否成功。

但由於Mysql的寫操作會加排他鎖(前文有講),如果鎖定了還算不算是MVCC?

瞭解樂觀鎖的小夥伴們,都知道其主要依靠版本控制,即消除鎖定,二者相互矛盾,so從某種意義上來說,Mysql的MVCC並非真正的MVCC,他只是借用MVCC的名號實現了讀的非阻塞而已。