提到事務,你確定不陌生,和數據庫打交道的時候,咱們老是會用到事務。最經典的例子就是轉帳,你要給朋友小王轉 100 塊錢,而此時你的銀行卡只有 100 塊錢。mysql
轉帳過程具體到程序裏會有一系列的操做,好比查詢餘額、作加減法、更新餘額等,這些操做必須保證是一體的,否則等程序查完以後,還沒作減法以前,你這 100 塊錢,徹底能夠藉着這個時間差再查一次,而後再給另一個朋友轉帳,若是銀行這麼整,不就亂了麼?這時就要用到「事務」這個概念了。sql
簡單來講,事務就是要保證一組數據庫操做,要麼所有成功,要麼所有失敗。在 MySQL 中,事務支持是在引擎層實現的。你如今知道,MySQL 是一個支持多引擎的系統,但並非全部的引擎都支持事務。好比 MySQL 原生的 MyISAM 引擎就不支持事務,這也是 MyISAM 被InnoDB 取代的重要緣由之一。數據庫
今天的文章裏,我將會以 InnoDB 爲例,剖析 MySQL 在事務支持方面的特定實現,並基於原理給出相應的實踐建議,但願這些案例能加深你對 MySQL 事務原理的理解。併發
提到事務,你確定會想到 ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔離性、持久性),今天咱們就來講說其中 I,也就是「隔離性」。框架
當數據庫上有多個事務同時執行的時候,就可能出現髒讀(dirty read)、不可重複讀(non reapeatable read)、幻讀(phantom read)的問題,爲了解決這些問題,就有了「隔離級別」的概念。線程
在談隔離級別以前,你首先要知道,你隔離得越嚴實,效率就會越低。所以不少時候,咱們都要在兩者之間尋找一個平衡點。SQL 標準的事務隔離級別包括:讀未提交(read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和串行化serializable )。3d
下面我逐一爲你解釋:日誌
mysql> create table T(c int) engine=InnoDB; insert into T(c) values(1);
咱們來看看在不一樣的隔離級別下,事務 A 會有哪些不一樣的返回結果,也就是圖裏面 V一、V二、V3 的返回值分別是什麼。code
若隔離級別是「讀未提交」, 則 V1 的值就是 2。這時候事務 B 雖然尚未提交,可是結果已經被 A 看到了。所以,V二、V3 也都是 2。orm
若隔離級別是「讀提交」,則 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 語句開始執行的時候建立的。這裏須要注意的是,「讀未提交」隔離級別下直接返回記錄上的最新值,沒有視圖概念;而「串行化」隔離級別下直接用加鎖的方式來避免並行訪問。
咱們能夠看到在不一樣的隔離級別下,數據庫行爲是有所不一樣的。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 的事務啓動方式有如下幾種:
有些客戶端鏈接框架會默認鏈接成功後先執行一個 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 的事務特性。
我給你留一個問題吧。你如今知道了系統裏面應該避免長事務,若是你是業務開發負責人同時也是數據庫負責人,你會有什麼方案來避免出現或者處理這種狀況呢?