🌇個人主頁:平凡的小蘇
📚學習格言:命運給你一個低的起點,是想看你精彩的翻盤,而不是讓你自甘墮落,腳下的路雖然難走,但我還能走,比起向陽而生,我更想嘗試逆風翻盤。
🛸Mysql專欄:Mysql內功修煉基地
> 家人們更新不易,你們的👍點贊👍和?關注?真的對我真重要,各位路 過的友友麻煩多多點贊關注。 歡迎你們的私信提問,感謝你們的轉發! 關注我,關注我,關注我,你們將會看到更多的優質內容!!
一、三種數據庫并發的場景
讀-讀
:不存在任何問題,也不需要并發控制
讀-寫
:有線程安全問題,可能會造成事務隔離性問題,可能遇到臟讀,幻讀,不可重復讀
寫-寫
:有線程安全問題,可能會存在更新丟失問題,比如第一類更新丟失,第二類更新丟失(后面補充)
二、讀寫并發
多版本并發控制( MVCC )是一種用來解決 讀-寫沖突 的無鎖并發控制為事務分配單向增長的事務ID,為每個修改保存一個版本,版本與事務ID關聯,讀操作只讀該事務開始前的數據庫的快照。 所以 MVCC 可以為數據庫解決以下問題:
-
在并發讀寫數據庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了數據庫并發讀寫的性能
-
同時還可以解決臟讀,幻讀,不可重復讀等事務隔離問題,但不能解決更新丟失問題
2.1、三個前置知識
-
每個事務都有自己的事務id,可以根據事務id的大小,來決定事務到來的先后順序
-
mysqld可能會面臨處理多個事務的情況,事務也有自己的生命周期,mysqld要對多個事務進行管理,先描述,在組織。事務在我看來,mysqld中一定是對應的一個或者一套結構體對象,事務也有自己的結構體
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變了
例如插入第一條數據的表結構
name | age | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
---|---|---|---|---|
張三 | 20 | null | null | 1 |
我們目前并不知道創建該記錄的事務ID,隱式主鍵,我們就默認設置成null,1。第一條記錄也沒有其他版本,我們設置回滾指針為null。
undo日志
我們這里理解undo log,簡單理解成,就是 MySQL 中的一段內存緩沖區,用來保存日志數據的就行。
模擬MVCC
現在有一個事務10,對student表中記錄進行修改(update):將name(張三)改成name(李四)。
事務10,因為要修改,所以要先給該記錄加行鎖。
修改前,現將改行記錄拷貝到undo log中,所以,undo log中就有了一行副本數據。(原理就是寫時拷貝)
所以現在 MySQL 中有兩行同樣的記錄。現在修改原始記錄中的name,改成 ‘李四’。并且修改原始記錄的隱藏字段 DB_TRX_ID 為當前 事務10 的ID, 我們默認從 10 開始,之后遞增。而原始記錄的回滾指針 DB_ROLL_PTR 列,里面寫入undo log中副本數據的地址,從而指向副本記錄,既表示我的上一個版本就是它。
事務10提交,釋放鎖。
備注:此時,最新的記錄是’李四‘那條記錄。
現在又有一個事務11,對student表中記錄進行修改(update):將age(20)改成age(50)。
事務11,因為也要修改,所以要先給該記錄加行鎖。(該記錄是那條?)
修改前,現將改行記錄拷貝到undo log中,所以,undo log中就又有了一行副本數據。此時,新的 副本,我們采用頭插方式,插入undo log。
現在修改原始記錄中的age,改成 50。并且修改原始記錄的隱藏字段 DB_TRX_ID 為當前 事務11 的ID。而原始記錄的回滾指針 DB_ROLL_PTR 列,里面寫入undo log中副本數據的地址,從而指向副本記錄,既表示我的上一個版本就是它。
事務11提交,釋放鎖。
這樣,我們就有了一個基于鏈表記錄的歷史版本鏈。所謂的回滾,無非就是用歷史數據,覆蓋當前數據。
上面的一個一個版本,我們可以稱之為一個一個的快照。
delete場景
如果是
delete
呢?一樣的,別忘了,刪數據不是清空,而是設置flag為刪除即可。也可以形成版本。
insert場景
因為
insert
是插入,也就是之前沒有數據,那么insert
也就沒有歷史版本。但是一般為了回滾操作,insert的數據也是要被放入undo log中,如果當前事務commit了,那么這個undolog 的歷史insert記錄就可以被清空了。
select場景
select讀取,是讀取最新的版本呢?還是讀取歷史版本?
當前讀
:讀取最新的記錄,就是當前讀。增刪改,都叫做當前讀,select也有可能當前讀,比如:selectlock in share mode(共享鎖), select for update
快照讀
:讀取歷史版本(一般而言),就叫做快照讀。我們可以看到,在多個事務同時刪改查的時候,都是當前讀,是要加鎖的。那同時有select過來,如果也要讀取最新版(當前讀),那么也就需要加鎖,這就是串行化。
但如果是快照讀,讀取歷史版本的話,是不受加鎖限制的。也就是可以并行執行!換言之,提高了效率,即MVCC的意義所在。
結論:select是當前讀還是快照讀,是由隔離級別決定的。
那么,如何保證,不同的事務,看到不同的內容呢?也就是如何如何實現隔離級別?
MVCC機制Read View
-
Read View就是事務進行 快照讀 操作的時候生產的 讀視圖 (Read View),在該事務執行的快照讀的那一刻,會生成數據庫系統當前的一個快照,記錄并維護系統當前活躍事務的ID(當每個事務開啟時,都會被分配一個ID, 這個ID是遞增的,所以最新的事務,ID值越大)
-
Read View 在MySQL 源碼中,就是一個類,本質是用來進行可見性判斷的。 即當我們某個事務執行快照讀的時候,對該記錄創建一個 Read View 讀視圖,把它比作條件,用來判斷當前事務能夠看到哪個版本的數據,既可能是當前最新的數據,也有可能是該行記錄的 undo log 里面的某個版本的數據。
下面是 ReadView 結構
class ReadView {
// 省略...
private:
/** 高水位,大于等于這個ID的事務均不可見*/
trx_id_t m_low_limit_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;
// 省略...
};m_ids; //一張列表,用來維護Read View生成時刻,系統正活躍的事務ID
up_limit_id; //記錄m_ids列表中事務ID最小的ID(沒有寫錯)
low_limit_id; //ReadView生成時刻系統尚未分配的下一個事務ID,也就是目前已出現過的事務ID的
//最大值+1(也沒有寫錯)
creator_trx_id //創建該ReadView的事務ID
注意:read view是事務可見性的一個類,不是事務創建出來,就會有read view,而是當這個事務(已經存在),首次進行快照讀的時候,mysql 形成read view!
Read View實驗
假設當前有條記錄:
name | age | DB_TRX_ID | DB_ROW_ID | DB_ROLL_PTR |
---|---|---|---|---|
王五 | 23 | null | 1 | null |
事務操作:
-
事務4:修改name(王五) 變成name(李六)
-
當 事務2 對某行數據執行了 快照讀 ,數據庫為該行數據生成一個 Read View 讀視圖
//事務2的 Read View
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:ReadView生成時刻,系統尚未分配的下一個事務ID
creator_trx_id // 2 快照讀id
-
只有事務4修改過該行記錄,并在事務2執行快照讀前,就提交了事務
-
我們的事務2在快照讀該行記錄的時候,就會拿該行記錄的 DB_TRX_ID 去跟up_limit_id,low_limit_id和活躍事務ID列表(trx_list) 進行比較,判斷當前事務2能看到該記錄的版本。
//事務2的 Read View
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:ReadView生成時刻,系統尚未分配的下一個事務ID
creator_trx_id // 2
//事務4提交的記錄對應的事務ID
DB_TRX_ID=4
//比較步驟
DB_TRX_ID(4)< up_limit_id(1) ? 不小于,下一步
DB_TRX_ID(4)>= low_limit_id(5) ? 不大于,下一步
m_ids.contains(DB_TRX_ID) ? 不包含,說明,事務4不在當前的活躍事務中。
結論:
故,事務4的更改,應該看到。所以事務2能讀到的最新數據記錄是事務4所提交的版本,而事務4提交的版本也是全局角度上最新的版本
RR與RC的本質區別
-
正是Read View生成時機的不同,從而造成RC,RR級別下快照讀的結果的不同
-
在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才會有不可重復讀問題。
測試:當前讀和快照讀在RR級別下的區別
-- 設置RR模式下測試
mysql> set global transaction isolation level REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)
-- 重啟終端
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
-- 依舊用之前的表
create table if not exists account(
id int primary key,
name varchar(50) not null default '',
blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;
-- 插入一條記錄,用來測試
mysql> insert into user (id, age, name) values (1, 15,'黃蓉');
Query OK, 1 row affected (0.00 sec)
用例一
用例二
-
用例1與用例2:唯一區別僅僅是 表1 的事務B在事務A修改age前 快照讀 過一次age數據
-
而 表2 的事務B在事務A修改age前沒有進行過快照讀。
結論:
事務中快照讀的結果是非常依賴該事務首次出現快照讀的地方,
即某個事務中首次出現快照讀,決定該事務后續快照讀結果的能力
delete同樣如此