03 | 事務隔離:爲何你改了我還看不見?

提到事務,你確定不陌生,和數據庫打交道的時候,咱們老是會用到事務。最經典的例子就是轉帳,你要給朋友小王轉100塊錢,而此時你的銀行卡只有100塊錢。mysql

轉帳過程具體到程序裏會有一系列的操做,好比查詢餘額、作加減法、更新餘額等,這些操做必須保證是一體的,否則等程序查完以後,還沒作減法以前,你這100塊錢,徹底能夠藉着這個時間差再查一次,而後再給另一個朋友轉帳,若是銀行這麼整,不就亂了麼?這時就要用到「事務」這個概念了。sql

簡單來講,事務就是要保證一組數據庫操做,要麼所有成功,要麼所有失敗。在MySQL中,事務支持是在引擎層實現的。你如今知道,MySQL是一個支持多引擎的系統,但並非全部的引擎都支持事務。好比MySQL原生的MyISAM引擎就不支持事務,這也是MyISAM被InnoDB取代的重要緣由之一。數據庫

今天的文章裏,我將會以InnoDB爲例,剖析MySQL在事務支持方面的特定實現,並基於原理給出相應的實踐建議,但願這些案例能加深你對MySQL事務原理的理解。併發

隔離性與隔離級別

提到事務,你確定會想到ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔離性、持久性),今天咱們就來講說其中I,也就是「隔離性」。框架

當數據庫上有多個事務同時執行的時候,就可能出現髒讀(dirty read)、不可重複讀(non-repeatable read)、幻讀(phantom read)的問題,爲了解決這些問題,就有了「隔離級別」的概念。線程

在談隔離級別以前,你首先要知道,你隔離得越嚴實,效率就會越低。所以不少時候,咱們都要在兩者之間尋找一個平衡點。SQL標準的事務隔離級別包括:讀未提交(read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和串行化(serializable )。下面我逐一爲你解釋:日誌

  • 讀未提交是指,一個事務還沒提交時,它作的變動就能被別的事務看到。
  • 讀提交是指,一個事務提交以後,它作的變動纔會被其餘事務看到。
  • 可重複讀是指,一個事務執行過程當中看到的數據,老是跟這個事務在啓動時看到的數據是一致的。固然在可重複讀隔離級別下,未提交變動對其餘事務也是不可見的。
  • 串行化,顧名思義是對於同一行記錄,「寫」會加「寫鎖」,「讀」會加「讀鎖」。當出現讀寫鎖衝突的時候,後訪問的事務必須等前一個事務執行完成,才能繼續執行。

其中「讀提交」和「可重複讀」比較難理解,因此我用一個例子說明這幾種隔離級別。假設數據表T中只有一列,其中一行的值爲1,下面是按照時間順序執行兩個事務的行爲。code

mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1);


咱們來看看在不一樣的隔離級別下,事務A會有哪些不一樣的返回結果,也就是圖裏面V一、V二、V3的返回值分別是什麼。orm

  • 若隔離級別是「讀未提交」, 則V1的值就是2。這時候事務B雖然尚未提交,可是結果已經被A看到了。所以,V二、V3也都是2。
  • 若隔離級別是「讀提交」,則V1是1,V2的值是2。事務B的更新在提交後才能被A看到。因此, V3的值也是2。
  • 若隔離級別是「可重複讀」,則V一、V2是1,V3是2。之因此V2仍是1,遵循的就是這個要求:事務在執行期間看到的數據先後必須是一致的。
  • 若隔離級別是「串行化」,則在事務B執行「將1改爲2」的時候,會被鎖住。直到事務A提交後,事務B才能夠繼續執行。因此從A的角度看, V一、V2值是1,V3的值是2。

在實現上,數據庫裏面會建立一個視圖,訪問的時候以視圖的邏輯結果爲準。在「可重複讀」隔離級別下,這個視圖是在事務啓動時建立的,整個事務存在期間都用這個視圖。在「讀提交」隔離級別下,這個視圖是在每一個SQL語句開始執行的時候建立的。這裏須要注意的是,「讀未提交」隔離級別下直接返回記錄上的最新值,沒有視圖概念;而「串行化」隔離級別下直接用加鎖的方式來避免並行訪問。blog

咱們能夠看到在不一樣的隔離級別下,數據庫行爲是有所不一樣的。Oracle數據庫的默認隔離級別其實就是「讀提交」,所以對於一些從Oracle遷移到MySQL的應用,爲保證數據庫隔離級別的一致,你必定要記得將MySQL的隔離級別設置爲「讀提交」。

配置的方式是,將啓動參數transaction-isolation的值設置成READ-COMMITTED。你能夠用show variables來查看當前的值。

mysql> show variables like 'transaction_isolation';

+-----------------------+----------------+

| Variable_name | Value |

+-----------------------+----------------+

| transaction_isolation | READ-COMMITTED |

+-----------------------+----------------+

總結來講,存在即合理,哪一個隔離級別都有它本身的使用場景,你要根據本身的業務狀況來定。我想你可能會問那何時須要「可重複讀」的場景呢?咱們來看一個數據校對邏輯的案例。

假設你在管理一個我的銀行帳戶表。一個表存了每月月底的餘額,一個表存了帳單明細。這時候你要作數據校對,也就是判斷上個月的餘額和當前餘額的差額,是否與本月的帳單明細一致。你必定但願在校對過程當中,即便有用戶發生了一筆新的交易,也不影響你的校對結果。

這時候使用「可重複讀」隔離級別就很方便。事務啓動時的視圖能夠認爲是靜態的,不受其餘事務更新的影響。

事務隔離的實現

理解了事務的隔離級別,咱們再來看看事務隔離具體是怎麼實現的。這裏咱們展開說明「可重複讀」。

在MySQL中,實際上每條記錄在更新的時候都會同時記錄一條回滾操做。記錄上的最新值,經過回滾操做,均可以獲得前一個狀態的值。

假設一個值從1被按順序改爲了二、三、4,在回滾日誌裏面就會有相似下面的記錄。


當前值是4,可是在查詢這條記錄的時候,不一樣時刻啓動的事務會有不一樣的read-view。如圖中看到的,在視圖A、B、C裏面,這一個記錄的值分別是一、二、4,同一條記錄在系統中能夠存在多個版本,就是數據庫的多版本併發控制(MVCC)。對於read-view A,要獲得1,就必須將當前值依次執行圖中全部的回滾操做獲得。

同時你會發現,即便如今有另一個事務正在將4改爲5,這個事務跟read-view A、B、C對應的事務是不會衝突的。

你必定會問,回滾日誌總不能一直保留吧,何時刪除呢?答案是,在不須要的時候才刪除。也就是說,系統會判斷,當沒有事務再須要用到這些回滾日誌時,回滾日誌會被刪除。

何時纔不須要了呢?就是當系統裏沒有比這個回滾日誌更早的read-view的時候。

基於上面的說明,咱們來討論一下爲何建議你儘可能不要使用長事務。

長事務意味着系統裏面會存在很老的事務視圖。因爲這些事務隨時可能訪問數據庫裏面的任何數據,因此這個事務提交以前,數據庫裏面它可能用到的回滾記錄都必須保留,這就會致使大量佔用存儲空間。

在MySQL 5.5及之前的版本,回滾日誌是跟數據字典一塊兒放在ibdata文件裏的,即便長事務最終提交,回滾段被清理,文件也不會變小。我見過數據只有20GB,而回滾段有200GB的庫。最終只好爲了清理回滾段,重建整個庫。

除了對回滾段的影響,長事務還佔用鎖資源,也可能拖垮整個庫,這個咱們會在後面講鎖的時候展開。

事務的啓動方式

如前面所述,長事務有這些潛在風險,我固然是建議你儘可能避免。其實不少時候業務開發同窗並非有意使用長事務,一般是因爲誤用所致。MySQL的事務啓動方式有如下幾種:

  1. 顯式啓動事務語句, begin 或 start transaction。配套的提交語句是commit,回滾語句是rollback。

  2. set autocommit=0,這個命令會將這個線程的自動提交關掉。意味着若是你只執行一個select語句,這個事務就啓動了,並且並不會自動提交。這個事務持續存在直到你主動執行commit 或 rollback 語句,或者斷開鏈接。

有些客戶端鏈接框架會默認鏈接成功後先執行一個set autocommit=0的命令。這就致使接下來的查詢都在事務中,若是是長鏈接,就致使了意外的長事務。

所以,我會建議你老是使用set autocommit=1, 經過顯式語句的方式來啓動事務。

可是有的開發同窗會糾結「多一次交互」的問題。對於一個須要頻繁使用事務的業務,第二種方式每一個事務在開始時都不須要主動執行一次 「begin」,減小了語句的交互次數。若是你也有這個顧慮,我建議你使用commit work and chain語法。

在autocommit爲1的狀況下,用begin顯式啓動的事務,若是執行commit則提交事務。若是執行 commit work and chain,則是提交事務並自動啓動下一個事務,這樣也省去了再次執行begin語句的開銷。同時帶來的好處是從程序開發的角度明確地知道每一個語句是否處於事務中。

你能夠在information_schema庫的innodb_trx這個表中查詢長事務,好比下面這個語句,用於查找持續時間超過60s的事務。

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

小結

這篇文章裏面,我介紹了MySQL的事務隔離級別的現象和實現,根據實現原理分析了長事務存在的風險,以及如何用正確的方式避免長事務。但願我舉的例子可以幫助你理解事務,並更好地使用MySQL的事務特性。

我給你留一個問題吧。你如今知道了系統裏面應該避免長事務,若是你是業務開發負責人同時也是數據庫負責人,你會有什麼方案來避免出現或者處理這種狀況呢?

你能夠把你的思考和觀點寫在留言區裏,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。

上期問題時間

在上期文章的最後,我給你留下的問題是一天一備跟一週一備的對比。

好處是「最長恢復時間」更短。

在一天一備的模式裏,最壞狀況下須要應用一天的binlog。好比,你天天0點作一次全量備份,而要恢復出一個到昨天晚上23點的備份。

一週一備最壞狀況就要應用一週的binlog了。

系統的對應指標就是 @尼古拉斯·趙四 @慕塔 提到的RTO(恢復目標時間)。

固然這個是有成本的,由於更頻繁全量備份須要消耗更多存儲空間,因此這個RTO是成本換來的,就須要你根據業務重要新來評估了。

相關文章
相關標籤/搜索