mysql 事務就這一篇!

開篇

相信大家都用過事務以及瞭解他的特點,如:

今天想跟大家一起研究下事務內部到底是怎麼實現的,在講解前我想先拋出個問題:

事務想要做到什麼效果?

按我理解,無非是要做到可靠性以及併發處理

可靠性:數據庫要保證當 insert 或 update 操作時拋異常或者數據庫 crash 的時候需要保障數據的操作前後的一致,想要做到這個,我需要知道我修改之前和修改之後的狀態,所以就有了 undo log 和 redo log。

併發處理:也就是說當多個併發請求過來,並且其中有一個請求是對數據修改操作的時候會有影響,爲了避免讀到髒數據,所以需要對事務之間的讀寫進行隔離,至於隔離到啥程度得看業務系統的場景了,實現這個就得用 MySQL 的隔離級別。

下面我首先講實現事務功能的三個技術,分別是日誌文件 (redo log 和 undo log),鎖技術以及 MVCC,然後再講事務的實現原理,包括原子性是怎麼實現的,隔離型是怎麼實現的等等。最後再做一個總結,希望大家能夠耐心看完

redo log 與 undo log 介紹

1. redo log

什麼是 redo log ?

redo log 叫做重做日誌,是用來實現事務的持久性。該日誌文件由兩部分組成:重做日誌緩衝(redo log buffer)以及重做日誌文件(redo log), 前者是在內存中,後者在磁盤中。當事務提交之後會把所有修改信息都會存到該日誌中。假設有個表叫做 tb1(id,username) 現在要插入數據(3,ceshi)

圖片

start transaction;select balance from bank where ;// 生成 重做日誌 balance=600update bank set balance = balance - 400; // 生成 重做日誌 amount=400update finance set amount = amount + 400;commit;

redo log 有什麼作用?

mysql 爲了提升性能不會把每次的修改都實時同步到磁盤,而是會先存到 Boffer Pool(緩衝池) 裏頭,把這個當作緩存來用。然後使用後臺線程去做緩衝池和磁盤之間的同步

那麼問題來了,如果還沒來的同步的時候宕機或斷電了怎麼辦?還沒來得及執行上面圖中紅色的操作。這樣會導致丟失部分已提交事務的修改信息!

所以引入了 redo log 來記錄已成功提交事務的修改信息,並且會把 redo log 持久化到磁盤,系統重啓之後再讀取 redo log 恢復最新數據。

總結:redo log 是用來恢復數據的用於保障,已提交事務的持久化特性

2.undo log

什麼是 undo log ?

undo log 叫做回滾日誌,用於記錄數據被修改前的信息。他正好跟前面所說的重做日誌所記錄的相反,重做日誌記錄數據被修改後的信息。undo log 主要記錄的是數據的邏輯變化,爲了在發生錯誤時回滾之前的操作,需要將之前的操作都記錄下來,然後在發生錯誤時纔可以回滾。

還用上面那兩張表

每次寫入數據或者修改數據之前都會把修改前的信息記錄到 undo log。

undo log 有什麼作用?

undo log 記錄事務修改之前版本的數據信息,因此假如由於系統錯誤或者 rollback 操作而回滾的話可以根據 undo log 的信息來進行回滾到沒被修改前的狀態。

總結:undo log 是用來回滾數據的用於保障 未提交事務的原子性

mysql 鎖技術以及 MVCC 基礎

1. mysql 鎖技術

當有多個請求來讀取表中的數據時可以不採取任何操作,但是多個請求裏有讀請求,又有修改請求時必須有一種措施來進行併發控制。不然很有可能會造成不一致。

讀寫鎖

解決上述問題很簡單,只需用兩種鎖的組合來對讀寫請求進行控制即可,這兩種鎖被稱爲:

共享鎖 (shared lock), 又叫做 "讀鎖" 讀鎖是可以共享的,或者說多個讀請求可以共享一把鎖讀數據,不會造成阻塞。

排他鎖 (exclusive lock), 又叫做 "寫鎖" 寫鎖會排斥其他所有獲取鎖的請求,一直阻塞,直到寫入完成釋放鎖。

總結:通過讀寫鎖,可以做到讀讀可以並行,但是不能做到寫讀,寫寫並行 事務的隔離性就是根據讀寫鎖來實現的!!!這個後面再說。

2. MVCC 基礎

MVCC (MultiVersion Concurrency Control) 叫做多版本併發控制。

InnoDB 的 MVCC ,是通過在每行記錄的後面保存兩個隱藏的列來實現的。這兩個列,一個保存了行的創建時間,一個保存了行的過期時間,當然存儲的並不是實際的時間值,而是系統版本號。

以上片段摘自《高性能 Mysql》這本書對 MVCC 的定義。他的主要實現思想是通過數據多版本來做到讀寫分離。從而實現不加鎖讀進而做到讀寫並行。關注 Java 技術棧公衆號在後臺回覆 mysql 獲取系列教程。

MVCC 在 mysql 中的實現依賴的是 undo log 與 read view

事務的實現

前面講的重做日誌,回滾日誌以及鎖技術就是實現事務的基礎。

原子性,持久性,隔離性折騰半天的目的也是爲了保障數據的一致性!

總之,ACID 只是個概念,事務最終目的是要保障數據的可靠性,一致性。

1. 原子性的實現

什麼是原子性:

一個事務必須被視爲不可分割的最小工作單位,一個事務中的所有操作要麼全部成功提交,要麼全部失敗回滾,對於一個事務來說不可能只執行其中的部分操作,這就是事務的原子性。

上面這段話取自《高性能 MySQL》這本書對原子性的定義,原子性可以概括爲就是要實現要麼全部失敗,要麼全部成功。

以上概念相信大家夥兒都瞭解,那麼數據庫是怎麼實現的呢?就是通過回滾操作。所謂回滾操作就是當發生錯誤異常或者顯式的執行 rollback 語句時需要把數據還原到原先的模樣,所以這時候就需要用到 undo log 來進行回滾,接下來看一下 undo log 在實現事務原子性時怎麼發揮作用的

1.1 undo log 的生成

假設有兩個表 bank 和 finance,表中原始數據如圖所示,當進行插入,刪除以及更新操作時生成的 undo log 如下面圖所示:

從上圖可以瞭解到數據的變更都伴隨着回滾日誌的產生:(1) 產生了被修改前數據 (zhangsan,1000) 的回滾日誌

(2) 產生了被修改前數據 (zhangsan,0) 的回滾日誌

根據上面流程可以得出如下結論:1. 每條數據變更 (insert/update/delete) 操作都伴隨一條 undo log 的生成, 並且回滾日誌必須先於數據持久化到磁盤上 2. 所謂的回滾就是根據回滾日誌做逆向操作,比如 delete 的逆向操作爲 insert,insert 的逆向操作爲 delete,update 的逆向爲 update 等。

思考:爲什麼先寫日誌後寫數據庫?--- 稍後做解釋

1.2 根據 undo log 進行回滾

爲了做到同時成功或者失敗,當系統發生錯誤或者執行 rollback 操作時需要根據 undo log 進行回滾

回滾操作就是要還原到原來的狀態,undo log 記錄了數據被修改前的信息以及新增和被刪除的數據信息,根據 undo log 生成回滾語句,比如:

(1) 如果在回滾日誌裏有新增數據記錄,則生成刪除該條的語句

(2) 如果在回滾日誌裏有刪除數據記錄,則生成生成該條的語句

(3) 如果在回滾日誌裏有修改數據記錄,則生成修改到原先數據的語句

2. 持久性的實現

事務一旦提交,其所做的修改會永久保存到數據庫中,此時即使系統崩潰修改的數據也不會丟失。

先了解一下 MySQL 的數據存儲機制,MySQL 的表數據是存放在磁盤上的,因此想要存取的時候都要經歷磁盤 IO, 然而即使是使用 SSD 磁盤 IO 也是非常消耗性能的。爲此,爲了提升性能 InnoDB 提供了緩衝池 (Buffer Pool),Buffer Pool 中包含了磁盤數據頁的映射,可以當做緩存來使用:

讀數據:會首先從緩衝池中讀取,如果緩衝池中沒有,則從磁盤讀取在放入緩衝池;

寫數據:會首先寫入緩衝池,緩衝池中的數據會定期同步到磁盤中;

上面這種緩衝池的措施雖然在性能方面帶來了質的飛躍,但是它也帶來了新的問題,當 MySQL 系統宕機,斷電的時候可能會丟數據!!!

因爲我們的數據已經提交了,但此時是在緩衝池裏頭,還沒來得及在磁盤持久化,所以我們急需一種機制需要存一下已提交事務的數據,爲恢復數據使用。

於是 redo log 就派上用場了。下面看下 redo log 是什麼時候產生的

既然 redo log 也需要存儲,也涉及磁盤 IO 爲啥還用它?

(1)redo log 的存儲是順序存儲,而緩存同步是隨機操作。

(2)緩存同步是以數據頁爲單位的,每次傳輸的數據大小大於 redo log。

3. 隔離性實現

隔離性是事務 ACID 特性裏最複雜的一個。在 SQL 標準裏定義了四種隔離級別,每一種級別都規定一個事務中的修改,哪些是事務之間可見的,哪些是不可見的。

級別越低的隔離級別可以執行越高的併發,但同時實現複雜度以及開銷也越大。

Mysql 隔離級別有以下四種(級別由低到高):

只要徹底理解了隔離級別以及他的實現原理就相當於理解了 ACID 裏的隔離型。前面說過原子性,隔離性,持久性的目的都是爲了要做到一致性,但隔離型跟其他兩個有所區別,原子性和持久性是爲了要實現數據的可性保障靠,比如要做到宕機後的恢復,以及錯誤後的回滾。

那麼隔離性是要做到什麼呢?隔離性是要管理多個併發讀寫請求的訪問順序。 這種順序包括串行或者是並行說明一點,寫請求不僅僅是指 insert 操作,又包括 update 操作。

總之,從隔離性的實現可以看出這是一場數據的可靠性與性能之間的權衡。

READ UNCOMMITTED

在 READ UNCOMMITTED 隔離級別下,事務中的修改即使還沒提交,對其他事務是可見的。事務可以讀取未提交的數據,造成髒讀。

因爲讀不會加任何鎖,所以寫操作在讀的過程中修改數據,所以會造成髒讀。好處是可以提升併發處理性能,能做到讀寫並行

換句話說,讀的操作不能排斥寫請求。

優點:讀寫並行,性能高 缺點:造成髒讀

READ COMMITTED

一個事務的修改在他提交之前的所有修改,對其他事務都是不可見的。其他事務能讀到已提交的修改變化。在很多場景下這種邏輯是可以接受的。

InnoDB 在 READ COMMITTED,使用排它鎖, 讀取數據不加鎖而是使用了 MVCC 機制。或者換句話說他採用了讀寫分離機制。但是該級別會產生不可重讀以及幻讀問題。

什麼是不可重讀?

在一個事務內多次讀取的結果不一樣。

爲什麼會產生不可重複讀?

這跟 READ COMMITTED 級別下的 MVCC 機制有關係,在該隔離級別下每次 select 的時候新生成一個版本號,所以每次 select 的時候讀的不是一個副本而是不同的副本。分享一份完整的 MySQL 開發規範,推薦看下。

在每次 select 之間有其他事務更新了我們讀取的數據並提交了,那就出現了不可重複讀

REPEATABLE READ(Mysql 默認隔離級別)

在一個事務內的多次讀取的結果是一樣的。這種級別下可以避免,髒讀,不可重複讀等查詢問題。mysql 有兩種機制可以達到這種隔離級別的效果,分別是採用讀寫鎖以及 MVCC。

採用讀寫鎖實現

爲什麼能可重複度?只要沒釋放讀鎖,再次讀的時候還是可以讀到第一次讀的數據。

優點:實現起來簡單

缺點:無法做到讀寫並行

採用 MVCC 實現

爲什麼能可重複讀?因爲多次讀取只生成一個版本,讀到的自然是相同數據。

優點:讀寫並行

缺點:實現的複雜度高

但是在該隔離級別下仍會存在幻讀的問題,關於幻讀的解決我打算另開一篇來介紹。

SERIALIZABLE

該隔離級別理解起來最簡單,實現也最簡單。在隔離級別下除了不會造成數據不一致問題,沒其他優點。

4. 一致性的實現

數據庫總是從一個一致性的狀態轉移到另一個一致性的狀態.

下面舉個例子: zhangsan 從銀行卡轉 400 到理財賬戶

start transaction;select balance from bank where ;// 生成 重做日誌 balance=600update bank set balance = balance - 400; // 生成 重做日誌 amount=400update finance set amount = amount + 400;commit;
  1. 假如執行完 update bank set balance = balance - 400;之發生異常了,銀行卡的錢也不能平白無辜的減少,而是回滾到最初狀態。

  2. 又或者事務提交之後,緩衝池還沒同步到磁盤的時候宕機了,這也是不能接受的,應該在重啓的時候恢復並持久化。

  3. 假如有併發事務請求的時候也應該做好事務之間的可見性問題,避免造成髒讀,不可重複讀,幻讀等。在涉及併發的情況下往往在性能和一致性之間做平衡,做一定的取捨,所以隔離性也是對一致性的一種破壞。

總結

本文出發點是想講一下 Mysql 的事務的實現原理。

實現事務採取了哪些技術以及思想?

鏈接:https://www.jianshu.com/p/93dc93d2aebc

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/BSEl8ak3NC8uctGOgLD_8w