基本概念
事務是指知足ACID特性的的一組操做,能夠經過Commit提交事務,也能夠也能夠經過Rollback進行回滾。會存在中間態和一致性狀態(也是真正在數據庫表中存在的狀態)mysql
ACID
- Atomicity【原子性】:事務被視爲不可分割的最小單元,事務的全部操做要麼所有提交成功,要麼所有失敗回滾。回滾能夠用回滾日誌(undo Log)來實現,回滾日誌記錄着事務所執行的修改操做,在回滾時反向執行這些修改操做便可
undoLog:爲了知足事務的原子性,在操做任何數據以前,首先將數據備份到Undo Log,而後進行數據的修改。若是出現了錯誤或者用戶執行了ROLLBACK語句,系統能夠利用Undo Log中的備份將數據恢復到事務開始以前的狀態。與redo log不一樣的是,磁盤上不存在單獨的undo log文件,它存放在數據庫內部的一個特殊段(segment)中,這稱爲undo段(undo segment),undo段位於共享表空間內。Innodb爲每行記錄都實現了三個隱藏字段:6字節的事務ID(DB_TRX_ID)7字節的回滾指針(DB_ROLL_PTR)隱藏的IDsql
- Consistency【一致性】:數據庫在事務執行先後都保持一致性狀態,在一致性狀態下,全部事務對同一個數據的讀取結果都是相同的
- Isolation【隔離性】:一個事務所作的修改在最終提交前,對其餘事務是不可見的
- Durability【持久性】:一旦事務提交,則其所作的修改將會永遠保存到數據庫中,即便系統發生崩潰,事務執行的結果也不能丟失。系統發生崩潰能夠用redoLog進行恢復,從而實現持久性。與undoLog記錄數據的邏輯修改不一樣,redoLog記錄的是數據頁的物理修改
- 小結:
1. 只有知足一致性,事務的執行結果纔是正確的。
2. 在無併發的狀況下,事務串行執行,隔離性必定可以知足。此時只要可以知足原子性,就必定能知足一致性。
3. 在併發狀況下,多個事務並行執行,事務不只要知足原子性,還須要知足隔離性,才能知足一致性
4. 事務知足持久化是爲了可以應對系統崩潰的狀況
AutoCommit
- MySQL默認採用自動提交模式,也就是說,若是不顯示使用start transaction語句來開始一個事務,那麼每一個操做都會被當作一個事務並自動提交
事務隔離級別
- 未提交讀【read uncommitted】:事務中的修改,即便沒有提交,對其餘事務也是可見的
- 提交讀【read committed】:一個事務只能讀取已經提交的事務所作的修改,換句話說,一個事務所作的修改在提交以前對其餘事務是不可見的
- 可重複讀【repeatable read】:保證在同一個事務中屢次讀取同一數據的結果是同樣的
- 可串行化【serializable】:強制事務串行執行,這樣多個事務互不干擾,不會出現併發一致性問題,該隔離級別須要加鎖實現,由於要使用加鎖機制保證同一時間只有一個事務執行,也就是保證事務串行執行
併發一致性問題
背景
在併發環境下,事務的隔離性很難保證,所以會出現不少併發一致性問題數據庫
主要場景
- 丟失修改:丟失修改指一個事務更新操做被另一個事務的更新操做替換。例如:T1 和 T2 兩個事務都對一個數據進行修改,T1 先修改並提交生效,T2 隨後修改,T2 的修改覆蓋了 T1 的修改。
業務場景:用戶修改地址有修改地址信息和設置默認地址或者刪除地址,這三個場景都是調用的同一個update語句。提供給用戶更新地址的接口須要支持用戶可設置默認地址,而不能將更新地址信息和設置默認地址分開提供接口,若是分開提供,上層服務調用其實是一會兒調用兩個更新接口,這樣很容易會出現丟失修改的場景。
- 讀髒數據:讀髒數據指在不一樣的事務下,當前事務能夠讀取到另外事務未提交的數據,例如:T1 修改一個數據但未提交,T2 隨後讀取這個數據。若是 T1 撤銷了此次修改,那麼 T2 讀取的數據是髒數據。
- 不可重複讀:不可重複讀指在一個事務內屢次讀取同一數據集合,在這一事務還未結束前,另外一個事務也訪問了該同一數據集合並作了修改,因爲第二個事務的修改,第一次事務的兩次讀取的數據可能不一致。例如:T2 讀取一個數據,T1 對該數據作了修改。若是 T2 再次讀取這個數據,此時讀取的結果和第一次讀取的結果不一樣。
- 幻影讀:幻讀本質上也屬於不可重複讀的狀況,T1讀取某個範圍的數據,T2在這個範圍內插入新的數據,T1再次讀取這個範圍的數據,此時讀取的結果和第一次讀取的結果不一樣
- 小結:
產生併發不一致性問題的主要緣由是破壞了事務的隔離性,解決方法是經過併發控制來保證隔離性。併發控制能夠經過封鎖來實現,可是封鎖操做須要用戶本身控制,至關複雜。數據庫管理系統提供了事務的隔離級別,讓用戶以一種更輕鬆的方式處理併發一致性問題。
鎖
封鎖粒度:
- 行級鎖:只封鎖須要修改的那部分數據或那行,不是封鎖全部資源,發生鎖爭用的可能小,系統併發程度高
- 表級鎖:封鎖整個表,鎖定數據量太大,發生鎖爭用的機率大大加大,系統併發性能直線下滑
注意:加鎖就會消耗資源,鎖的各類操做【包括獲取鎖,釋放鎖,檢查鎖狀態都會增長系統開銷,所以封鎖的粒度越小,系統開銷越大】,在選擇封鎖粒度時,須要在鎖開銷和併發程度之間作一個權衡安全
封鎖類型
讀寫鎖
- 互斥鎖,簡寫爲X鎖,又稱寫鎖。
一個事務對數據對象 A 加了 X 鎖,就能夠對 A 進行讀取和更新。加鎖期間其它事務不能對 A 加任何鎖。
- 共享鎖,簡寫爲S鎖,又稱讀鎖。
一個事務對數據對象 A 加了 S 鎖,能夠對 A 進行讀取操做,可是不能進行更新操做。加鎖期間其它事務能對 A 加 S 鎖,可是不能加 X 鎖。
意向鎖
主要是表鎖,可是不會真的鎖併發
- 在存在行級鎖和表級鎖的狀況下,事務 T 想要對錶 A 加 X 鎖,就須要先檢測是否有其它事務對錶 A 或者表 A 中的任意一行加了鎖,那麼就須要對錶 A 的每一行都檢測一次,這是很是耗時的。
意向鎖在原來的 X/S 鎖之上引入了 IX/IS,IX/IS 都是表鎖,用來表示一個事務想要在表中的某個數據行上加 X 鎖或 S 鎖。
有如下兩個規定:
一個事務在得到某個數據行對象的 S 鎖以前,必須先得到表的 IS 鎖或者更強的鎖;
一個事務在得到某個數據行對象的 X 鎖以前,必須先得到表的 IX 鎖。
經過引入意向鎖,事務 T 想要對錶 A 加 X 鎖,只須要先檢測是否有其它事務對錶 A 加了 X/IX/S/IS 鎖,若是加了就表示有其它事務正在使用這個表或者表中某一行的鎖,所以事務 T 加 X 鎖失敗。
- 任意 IS/IX 鎖之間都是兼容的,由於它們只表示想要對錶加鎖,而不是真正加鎖;
這裏兼容關係針對的是表級鎖,而表級的 IX 鎖和行級的 X 鎖兼容,兩個事務能夠對兩個數據行加 X 鎖。
(事務 T1 想要對數據行 R1 加 X 鎖,事務 T2 想要對同一個表的數據行 R2 加 X 鎖,兩個事務都須要對該表加 IX 鎖,可是 IX 鎖是兼容的,而且 IX 鎖與行級的 X 鎖也是兼容的,所以兩個事務都能加鎖成功,對同一個表中的兩個數據行作修改。)
MySQL隱式與顯示鎖定
- 隱式鎖定:MySQL 的 InnoDB 存儲引擎採用兩段鎖協議,會根據隔離級別在須要的時候自動加鎖,而且全部的鎖都是在同一時刻被釋放,這被稱爲隱式鎖定
兩段鎖協議:加鎖和解鎖分爲兩個階段進行,可串行化調度是指經過併發控制,使得併發執行的事務結果與某個串行執行的事務結果相同,串行執行的事務互不干擾,不會出現併發一致性問題mvc
- 或者使用特定語句進行顯示鎖定SELECT ... LOCK In SHARE MODE;(共享鎖)SELECT ... FOR UPDATE;(排他鎖)事務完成提交自動釋放鎖
MySQL三級封鎖協議
- 一級封鎖協議:事務T要修改數據A時必須加X鎖,知道事務T結束才釋放鎖能夠解決丟失修改問題。這時候不能同時有兩個事務對同一個數據進行修改,那麼事務的修改就不會被覆蓋
- 二級封鎖協議:在一級基礎上,要求讀取數據A時必須加S鎖,讀取完立刻釋放S鎖能夠解決讀髒數據問題。由於若是有一個事務在對數據A進行修改,根據1級封鎖協議,會加X鎖,那麼就不能再加S鎖了,也就是不會讀入髒數據
- 三級封鎖協議:在二級基礎上,要求讀取數據時必須加S鎖,直到事務結束了才能釋放S鎖能夠解決不可重複讀的問題,由於讀A時,其餘事務不能對A加X鎖,從而避免了在讀期間數據發生改變
InnoDB引擎的鎖實現
MVCC
- 多版本併發控制是MySQL的innoDB存儲引擎實現隔離級別的一種具體方式,可用於實現提交讀和可重複讀這兩種隔離級別,而未提交讀隔離級別老是讀取最新的數據行,要求很低,無需使用MVCC
- 在封鎖一節中提到,加鎖能解決多個事務同時執行時出現的併發一致性問題。在實際場景中讀操做每每多於寫操做,所以又引入了讀寫鎖來避免沒必要要的加鎖操做,例如讀和讀沒有互斥關係。讀寫鎖中讀和寫操做仍然是互斥的,而 MVCC 利用了多版本的思想,寫操做更新最新的版本快照,而讀操做去讀舊版本快照,沒有互斥關係,這一點和 CopyOnWrite 相似。
- 在 MVCC 中事務的修改操做(DELETE、INSERT、UPDATE)會爲數據行新增一個版本快照。髒讀和不可重複讀最根本的緣由是事務讀取到其它事務未提交的修改。在事務進行讀取操做時,爲了解決髒讀和不可重複讀問題,MVCC 規定只能讀取已經提交的快照。固然一個事務能夠讀取自身未提交的快照,這不算是髒讀。
- 系統版本號 SYS_ID:是一個遞增的數字,每開始一個新的事務,系統版本號就會自動遞增。
- 事務版本號 TRX_ID :事務開始時的系統版本號。
- MVCC的多版本指的是多個版本的快照,快照存儲在Undo日誌中,該日誌經過回滾指針ROLL_PTR把一個數據行的全部快照鏈接起來
- INSERT、UPDATE、DELETE 操做會建立一個日誌,並將事務版本號 TRX_ID 寫入。DELETE 能夠當作是一個特殊的 UPDATE,還會額外將 DEL 字段設置爲 1
ReadView
- MVCC維護了一個ReadView結構,主要包含了當前系統未提交的事務列表,還有該列表的最小值和最大值
- 在進行 SELECT 操做時,根據數據行快照的 TRX_ID 與 TRX_ID_MIN 和 TRX_ID_MAX 之間的關係,從而判斷數據行快照是否可使用。
- TRX_ID < TRX_ID_MIN,表示該數據行快照時在當前全部未提交事務以前進行更改的,所以可使用。
- TRX_ID > TRX_ID_MAX,表示該數據行快照是在事務啓動以後被更改的,所以不可以使用。
- TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,須要根據隔離級別再進行判斷
- 提交讀:若是 TRX_ID 在 TRX_IDs 列表中,表示該數據行快照對應的事務還未提交,則該快照不可以使用。不然表示已經提交,可使用。
- 可重複讀:都不可使用。由於若是可使用的話,那麼其它事務也能夠讀到這個數據行快照並進行修改,那麼當前事務再去讀這個數據行獲得的值就會發生改變,也就是出現了不可重複讀問題。在數據行快照不可以使用的狀況下,須要沿着 Undo Log 的回滾指針 ROLL_PTR 找到下一個快照,再進行上面的判斷。
快照讀和安全讀
- 快照讀:MVCC的select操做是快照中的數據,不須要進行加鎖操做
- 當前讀:MVCC其它會對數據庫進行修改的操做就須要進行加鎖操做,從而讀取最新的數據,能夠看到MVCC並非徹底不用加鎖,而只是避免了select的加鎖操做
若是須要select進行加鎖,就能夠強制指定加鎖操做,如以前提到的共享鎖和排他鎖微服務
Next-Key Locks
- 概念:Next-Key Locks 是 MySQL 的 InnoDB 存儲引擎的一種鎖實現。MVCC 不能解決幻影讀問題,Next-Key Locks 就是爲了解決這個問題而存在的。在可重複讀(REPEATABLE READ)隔離級別下,使用 MVCC + Next-Key Locks 能夠解決幻讀問題。
- Record Locks:鎖定一個記錄上的索引,而不是記錄自己,若是表沒有設置索引,InnoDB會自動在主鍵上建立隱藏的聚簇索引
- Gap Locks:鎖定索引之間的間隙,可是不包含索引自己。例如當一個事務執行如下語句SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
- Next-Key Locks:它是 Record Locks 和 Gap Locks 的結合,不只鎖定一個記錄上的索引,也鎖定索引之間的間隙。它鎖定一個前開後閉區間,例如一個索引包含如下值:10, 11, 13, and 20,那麼就須要鎖定如下區間:(-∞, 10](10, 11](11, 13](13, 20](20, +∞)
總結
上述理論較多,可是也是這些理論支撐整個研發過程,遇到多種業務場景時,須要根據數據庫的隔離級別判斷事務會不會出現死鎖,數據不一致等等理論性問題。MySQL最厲害的地方也是在RR【可重複讀】級別下避免了幻讀,即兼顧性能又保證數據安全。
在開發微服務或者分佈使項目時。儘可能將事務寫的簡單些,讓事務不會長時間鎖住對應的行。這樣也是保證了數據的一致性性能