前言
首先從概念上進行理解什么是事務,以及事務的4大屬性,知道是什么還要知道為什么?
事務是如何進行操作的,最后在談事務的隔離性、隔離級別(最重要但是也很難理解),理解隔離級別體現在哪里? (操作層面進行理解隔離性)
理論層面理解事務的實現原理
先談場景再談事務的概念
數據庫是支持多線程下的高并發訪問的,高并發的訪問如果不進行確保原子性的話,非常容易進行出現數據不一致問題。
所以說數據庫的CURD操作要進行處理這種問題,主要通過一下策略
1. 買票的過程得是原子的吧
2. 買票互相應該不能影響吧
3. 買完票應該要永久有效吧
4. 買前,和買后都要是確定的狀態吧
CURD通過策略確保數據庫的正常操作和事務有什么關系呢?
通過事務概念就可以知道這兩者之間的關系了
什么是事務?
事務就是一組DML語句組成,各種mysql語句,他們之間具有一定的邏輯,就把這些具有邏輯的mysql的一批語句封裝起來稱為事務。
這一組DML語句要么全部成功,要么全部 失敗,是一個整體。防止出現一個事務沒有執行完,另一個事務執行造成數據不一致問題,MySQL提供一種機制,保證我們達到這樣的效果。事務還規定不同的客戶端看到的數據是不相同的
為了保證事務的正常運行,擁有四大屬性
- 原子性:一個事務(transaction)中的所有操作,要么全部完成,要么全部不完成,不會結束在中 間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個 事務從來沒有執行過一樣。
- 一致性:進行事務操作后的結果是可以預期的,在數據庫中mysql并沒有通過實現策略進行確保一致性,而是通過其他三種特性來進行維持的
- 隔離性:數據庫允許多個并發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務 并發執行時由于交叉執行而導致數據的不一致。事務隔離分為不同級別,包括讀未提交( Read uncommitted )、讀提交( read committed )、可重復讀( repeatable read )和串行化 ( Serializable )
- 持久性:事務處理結束后,對數據的修改就是永久的,即便系統故障也不會丟失。
為什么要有事務呢?
不要只站在程序員的角度去理解:
為了保證多個sql語句在執行的時候,不會出現交差執行的問題,防止數據不一致,從而破環mysql的完整性
還要站在數據庫使用者的角度去理解:
事務被MySQL編寫者設計出來,本質就是為了當應用層程序進行訪問數據庫的時候,事務能夠進行簡化我們的編程模型,上層開發者不需要再進行考慮這種潛在的錯誤和并發問題,這些都是MySQL 已經幫我們進行避免了,因此事務的本質就是為了應用層進行服務的,而不是伴隨數據庫系統天生就有的。
了解事務的提交方式
事務的版本支持
并不是所有的引擎都支持事務的
查看MySQL 系統支持的所有存儲引擎以及每個存儲引擎的相關特性和狀態。
show engines;
備注:
XA 事務是分布式事務的一部分,通常用于多個數據庫系統之間的事務管理。
保存點是事務中的檢查點,允許部分回滾某些操作,而不是完全回滾。
事務的提交方式
事務提交方式的種類
事務的常見提交方式有兩種
- 手動提交
- 自動提交
查看事務的提交方式
show variables like 'autocommit';
value值對應的是NO表示支持自動提交,OFF 表示不支持自動提交,也就是手動提交。?
更改事務的提交方式
事務的常見操作
事務正常驗證與產出結論
提前準備
mysql 不僅本地的客戶端可以進行連接,遠端的客戶端也可以進行連接
一個mysql服務器是可以通過多個客戶端進行訪問的
需要進行重啟客戶端生效?
創建測試表?
開始事務
start transaction;
或者直接使用
begin;
設置任務保存點
savepiont + 保存點
?回滾
rollback to 保存點
回退到指定的保存點。?
結束事務
回滾操作只能在事務進行提交之前進行回滾
事務異常驗證與產出結論
當我們在事務中插入數據,然后客戶端崩潰(此時崩潰的客戶端自動提交是開啟的,但是未進行手動commit 提交),我們在進行查看,發現剛剛進行插入的數據,回滾回去了?。
當我們在事務中插入數據,然后客戶端崩潰(此時崩潰的客戶端自動提交是開啟的,進行手動commit 提交),我們在進行查看,發現剛剛進行插入的數據,無法進行回滾回去了?。
ctrl + \ 客戶端直接崩潰
通過上面的現象我們可以看到,mysql中的事務保證了要么就不操作,要么就操作完,這不就是事務四大特性中的 原子性?和 持久性 嗎
單條SQL和事務的關系
當未進行打開自動提交的時候,進行插入數據后(未進行手動commit 提交),客戶端崩潰,數據庫中的數據會進行回滾。?
當打開自動提交的時候,進行插入數據后(未進行手動commit 提交),客戶端崩潰,數據庫中的數據會進行回滾,也就說明autocommit 在客戶端崩潰之前進行了自動提交。
因為事務是主動提交的,所以我們感知不到,但是可以進行證明單SQL本質就是事務。
結論
- 只要輸入begin或者start transaction,事務便必須要通過commit提交,才會持久化,與是 否設置set autocommit無關。
- 事務可以手動回滾,同時,當操作異常,MySQL會自動回滾
- 對于 InnoDB 每一條 SQL 語言都默認封裝成事務,自動提交。(select有特殊情況,因為 MySQL 有 MVCC )
- 從上面的例子,我們能看到事務本身的原子性(回滾),持久性(commit)
什么是隔離性,什么是隔離級別,為什么要有這么多隔離級別?
如何理解隔離性?
理解事務的隔離性(Isolation)通常可以通過時間軸來幫助可視化不同事務之間的數據操作順序和它們之間的相互影響。事務隔離性的目的是確保在多個事務并發執行時,事務之間互不干擾,并且每個事務都有一個獨立的執行環境。
如何理解隔離級別?
數據庫中,允許事務受不同程度的干擾,就有了一種重要特征:隔離級別
READ UNCOMMITTED(讀取未提交):最底層的隔離級別,事務可以看到其他事務的未提交數據。
READ COMMITTED(讀取已提交):事務只能讀取已提交的數據,不允許讀取未提交的數據。
REPEATABLE READ(可重復讀取):確保在同一事務中,讀取的數據是一致的,即使其他事務修改了該數據,當前事務的讀取結果不受影響。
SERIALIZABLE(可串行化):最高的隔離級別,所有事務按順序執行,就像它們是串行執行的。
READ UNCOMMITTED(讀取未提交):
在這個隔離級別下,事務可以讀取其他事務未提交的數據(臟讀)。這個級別允許事務之間的最大干擾。
- T1: 寫入數據 A=10 ?(未提交)
- T2: 讀取數據 A=10 ?(臟讀)
- T1: 提交
- T2: 繼續操作
時間軸:
-
事務
T2
讀取了事務T1
中尚未提交的變更(臟讀)。 -
事務
T1
提交后,T2
繼續操作,但T2
的數據讀取可能是無效的,因為T1
的更改最終可能被回滾。
READ COMMITTED(讀取已提交):
在這個隔離級別下,事務只能讀取其他事務已經提交的數據,避免了臟讀,但仍然可能發生不可重復讀(數據在同一事務中讀取兩次時不一致)。
- T1: 寫入數據 A=10 ?(未提交)
- T2: 讀取數據 A=10 ?(臟讀)
- T1: 提交
- T2: 繼續讀取數據 A=20 ?(不可重復讀)
時間軸:
-
事務
T2
最初讀取了事務T1
中未提交的數據。 -
當事務
T1
提交后,事務T2
讀取的數據可能發生變化,從A=10
變成A=20
,發生了不可重復讀。
這個過程還經常發生幻讀,幻讀和不可重復讀的區別如下
同一事務中,兩次讀到同一行內容不同 | ? 這是不可重復讀 |
同一事務中,兩次讀到的行數或記錄集不同 |
很多人都會對不可重復有下面這個疑問?
提交了被看到,難道不應該嗎
提交了確實應該被看到,但是不能是正在運行的事務看到,這樣是非常容易出現問題的,如下圖所示:
當我們正在進行執行發放年終獎,發放年終獎是根據薪資來進行決定的,當tom的薪資本來在3000~4000中,已將tom進行執行到這個環節,但是呢,tom薪資在另一個事務中進行發生了更改,此時由于是不可重復讀,導致呢tom這個人在4000~5000的薪資中又重新被統計了一遍,從而導致問題的出現,這也破環了事務的隔離性原則。?
REPEATABLE READ(可重復讀取):
在此級別下,事務讀取的數據在整個事務過程中保持一致,即使其他事務修改了相同的數據。該級別避免了臟讀和不可重復讀,但仍然可能出現幻讀(即讀取的記錄集在事務執行過程中發生變化)。
- T1: 讀取數據 A=10 ?(已提交)
- T2: 寫入數據 A=20 ?(未提交)
- T1: 繼續讀取數據 A=10 ?(可重復讀)
- T2: 提交
時間軸:
-
事務
T1
在整個過程中讀取的數據是一致的(不會因為其他事務的修改而改變),即使T2
修改了數據,T1
仍然讀取到A=10
。 -
事務
T1
讀取的數據是可重復的,不會出現數據的不一致。
SERIALIZABLE(可串行化):
在這個最高級別的隔離下,事務會像串行執行一樣,完全避免了臟讀、不可重復讀和幻讀。
- T1: 讀取數據 A=10 ?(已提交)
- T2: 事務等待 T1 提交
- T1: 提交
- T2: 寫入數據 A=20 ?(提交)
時間軸:
-
事務
T2
在T1
提交之前不能讀取或修改T1
正在操作的數據。 -
所有事務按順序執行,不會發生并發沖突。
事務隔離級別的設置與查看
查看全局隔離級別
select @@global.transaction_isolation;
查看會話(當前)全局隔離級別
select @@session.transaction_isolation;
注:默認當前會話都是進行拷貝的全局隔離級別進行賦值的。
設置當前會話隔離級別
set session transaction_isolation = '隔離級別';
設置全局會話隔離級別
set globle transaction_isolation = '隔離級別';
mysql提供不同隔離性的原因是
供外部用戶進行選擇。
事務的隔離級別-一致性的正確理解
事務執行的結果,必須使數據庫從一個一致性狀態,變到另一個一致性狀態。當數據庫只包含事務 成功提交的結果時,數據庫處于一致性狀態。如果系統運行發生中斷,某個事務尚未完成而被迫中 斷,而改未完成的事務對數據庫所做的修改已被寫入數據庫,此時數據庫就處于一種不正確(不一 致)的狀態。因此一致性是通過原子性來保證的。
其實一致性和用戶的業務邏輯強相關,一般MySQL提供技術支持,但是一致性還是要用戶業務邏輯 做支撐,也就是,一致性,是由用戶決定的。
事務的一致性,其實就是通過其他三種特性進行決定的,其他三種特性是因,一致性其實就是其他三種特性的必然結果。
讀提交和可重復讀這種隔離性是如何做到的???
原理是怎么做到的?通過MVCC 進行做到的,那么MVCC是什么呢?
MVCC
多版本并發控制( MVCC )是一種用來解決 讀-寫沖突 的無鎖并發控制
為事務分配單向增長的事務ID,為每個修改保存一個版本,版本與事務ID關聯,讀操作只讀該事務開始 前的數據庫的快照。 所以 MVCC 可以為數據庫解決以下問題
- 在并發讀寫數據庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了數 據庫并發讀寫的性能
- 同時還可以解決臟讀,幻讀,不可重復讀等事務隔離問題,但不能解決更新丟失問題
如何理解MVCC
前置知識
- 3個記錄隱藏字段
- undo 日志
- Read View
3個記錄隱藏字段
- DB_TRX_ID :6 byte,最近修改( 修改/插入 )事務ID,記錄創建這條記錄/最后一次修改該記錄的事 務ID
- DB_ROLL_PTR : 7 byte,回滾指針,指向這條記錄的上一個版本(簡單理解成,指向歷史版本就 行,這些數據一般在 undo log 中)
- DB_ROW_ID : 6 byte,隱含的自增ID(隱藏主鍵),如果數據表沒有主鍵, InnoDB 會自動以 DB_ROW_ID 產生一個聚簇索引
- 補充:實際還有一個刪除flag隱藏字段, 既記錄被更新或刪除并不代表真的刪除,而是刪除flag變了
如何區分每個事務的先后問題呢
- 每個事務都要有自己的事務ID,可以根據事務的ID大小進行判斷事務的先后到來順序,事務ID越小越先被執行
- mysqld可能會面臨處理多個事務的情況,事務也有自己的聲明周期,mysqld要對多個事務進行管理:先描述,在組織。mysqld使用C/C++寫的,mysqld中一定有對應的一個或者一套結構體對象,事務也要有自己的結構體。
name | age | DB_TRX_ID(創建該記錄的事 務ID) | DB_ROW_ID(隱式 主鍵) | DB_ROLL_PTR(回滾 指針) |
張三 | 28 | null | 1 | null |
?圖片中進行描述的信息其實是表格中這樣
updo 日志
寫入之前先進行拷貝,再填地址,然后進行更改
歷史的回滾采用的相反sql的方式
多版本控制就交于MVCC
事務的回滾操作也是因為 undo log
undo log 不用擔心打滿的情況,當進行commit并且滅有用戶進行訪問的時候會自動進行free
update 和 delete 可以形成版本鏈,但是insert 暫時不考慮版本鏈的原因
select讀取最新版本還是讀取歷史版本呢??
Read view理論
Read View就是事務進行 快照讀 操作的時候生產的 讀視圖 (Read View),在該事務執行的快照讀的那一 刻,會生成數據庫系統當前的一個快照,記錄并維護系統當前活躍事務的ID(當每個事務開啟時,都會被 分配一個ID, 這個ID是遞增的,所以最新的事務,ID值越大)
Read View 在 MySQL 源碼中,就是一個類,本質是用來進行可見性判斷的。 即當我們某個事務執行快照 讀的時候,對該記錄創建一個 Read View 讀視圖,把它比作條件,用來判斷當前事務能夠看到哪個版本的 數據,既可能是當前最新的數據,也有可能是該行記錄的 undo log 里面的某個版本的數據
class ReadView {// 省略...private:/** 高水位,大于等于這個ID的事務均不可見*/trx_id_t m_low_limit_id我們在實際讀取數據版本鏈的時候,是能讀取到每一個版本對應的事務ID的,即:當前記錄的
DB_TRX_ID 。
那么,我們現在手里面有的東西就有,當前快照讀的 ReadView 和 版本鏈中的某一個記錄的
DB_TRX_ID 。
所以現在的問題就是,當前快照讀,應不應該讀到當前版本記錄。一張圖,解決所有問題!/** 低水位:小于這個ID的事務均可見 */trx_id_t m_up_limit_id;/** 創建該 Read View 的事務ID*/trx_id_t m_creator_trx_id;/** 創建視圖時的活躍事務id列表*/ids_t m_ids;/** 配合purge,標識該視圖不需要小于m_low_limit_no的UNDO LOG,* 如果其他視圖也不需要,則可以刪除小于m_low_limit_no的UNDO LOG*/trx_id_t m_low_limit_no;/** 標記視圖是否被關閉*/bool m_closed;// 省略...
};
我們在實際讀取數據版本鏈的時候,是能讀取到每一個版本對應的事務ID的,即:當前記錄的 DB_TRX_ID 。 那么,我們現在手里面有的東西就有,當前快照讀的 ReadView 和 版本鏈中的某一個記錄的 DB_TRX_ID 。 所以現在的問題就是,當前快照讀,應不應該讀到當前版本記錄。一張圖,解決所有問題!
這里的版本鏈并不是undo log 中的 數據.只是類似的結構.
m_ids; ? ? ? ? ? //一張列表,用來維護Read View生成時刻,系統正活躍的事務ID
up_limit_id; ? ? //記錄m_ids列表中事務ID最小的ID(沒有寫錯)
low_limit_id; ? ?//ReadView生成時刻系統尚未分配的下一個事務ID,也就是目前已出現過的事務ID的
最大值+1(也沒有寫錯)
creator_trx_id ? //創建該ReadView的事務ID
左側:
拿著我當前事務的信息,進行遍歷版本鏈,當我們是事務的creator_trx_id 和版本鏈中的DB_TRX_ID相同時,就證明我現在進行產看的這個事務就是我自己進行創建的(增加、修改、刪除)那么我應不應該看到呢?
當然應該看到。
當我的事務中的up_limit_id ,當我進行遍歷版本鏈中的最新的提交的快照的?DB_TRX_ID 要是比我up_limit_id還要小,也就是說歷史中運行的事務ID比我正在活躍運行的事務ID還要小,說明DB_TRX_ID早就已經結束了,早就已經提交完了,所以我應該看到.我和你這個事務的執行是串行的,沒有交叉,你先跑完,我才開始跑的.
右側:
當版本鏈中的DB_TRX_ID>=當我事務中的 low_limit_id 值說明是快照之后才進行,才進行提交的事務,不應該被看到
-
DB_TRX_ID >= low_limit_id
意味著該數據版本來自于一個在當前事務開始后提交的事務,因此當前事務 不能看到 這個版本的數據,因為它只應該讀取 低于low_limit_id
的事務 提交的數據。 -
事務提交時會更新版本鏈中的快照數據,但是當前事務所使用的快照視圖是在事務開始時就已經確定的,它不會受到當前正在提交的事務的影響。
view 并不是創建事務的時候就有的,而是在查詢的時候就存在的.???
RR與RC的區別
儲備知識已經就緒,現在來分析一下RR與RC的本質區別
當前讀和快照讀在RR下的區別:
少做了一次select 為什么會出現這么大差別,說好的可重復讀呢??
在RR級別下的某個事務的對某條記錄的第一次快照讀會創建一個快照及Read View, 將當前系統活 躍的其他事務記錄起來
此后在調用快照讀的時候,還是使用的是同一個Read View,所以只要當前事務在其他事務提交更 新之前使用過快照讀,那么之后的快照讀使用的都是同一個Read View,所以對之后的修改不可 見;
即RR級別下,快照讀生成Read View時,Read View會記錄此時所有其他活動事務的快照,這些事 務的修改對于當前事務都是不可見的。而早于Read View創建的事務所做的修改均是可見?
而在RC級別下的,事務中,每次快照讀都會新生成一個快照和Read View, 這就是我們在RC級別下 的事務中可以看到別的事務提交的更新的原因
總之在RC隔離級別下,是每個快照讀都會生成并獲取最新的Read View;而在RR隔離級別下,則是 同一個事務中的第一個快照讀才會創建Read View, 之后的快照讀獲取的都是同一個Read View。
正是RC每次快照讀,都會形成Read View,所以,RC才會有不可重復讀問題。