日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

作者:京東云開發(fā)者-京東物流 張士欣

鏈接:https://my.oschina.NET/u/4090830/blog/10142911

事務(wù)的底層原理

在事務(wù)的實(shí)現(xiàn)機(jī)制上,MySQL 采用的是 WAL:Write-ahead logging,預(yù)寫式日志,機(jī)制來實(shí)現(xiàn)的。

在使用 WAL 的系統(tǒng)中,所有的修改都先被寫入到日志中,然后再被應(yīng)用到系統(tǒng)中。通常包含 redo 和 undo 兩部分信息。

為什么需要使用 WAL,然后包含 redo 和 undo 信息呢?舉個(gè)例子,如果一個(gè)系統(tǒng)直接將變更應(yīng)用到系統(tǒng)狀態(tài)中,那么在機(jī)器掉電重啟之后系統(tǒng)需要知道操作是成功了,還是只有部分成功或者是失敗了。如果使用了 WAL,那么在重啟之后系統(tǒng)可以通過比較日志和系統(tǒng)狀態(tài)來決定是繼續(xù)完成操作還是撤銷操作。

redo log 稱為重做日志,每當(dāng)有操作時(shí),在數(shù)據(jù)變更之前將操作寫入 redo log,這樣當(dāng)發(fā)生掉電之類的情況時(shí)系統(tǒng)可以在重啟后繼續(xù)操作。

undo log 稱為撤銷日志,當(dāng)一些變更執(zhí)行到一半無法完成時(shí),可以根據(jù)撤銷日志恢復(fù)到變更之間的狀態(tài)。

MySQL 中用 redo log 來在系統(tǒng) Crash 重啟之類的情況時(shí)修復(fù)數(shù)據(jù),而 undo log 來保證事務(wù)的原子性。

事務(wù) id

一個(gè)事務(wù)可以是一個(gè)只讀事務(wù),或者是一個(gè)讀寫事務(wù):可以通過 START TRANSACTION READ ONLY 語句開啟一個(gè)只讀事務(wù)。

在只讀事務(wù)中不可以對普通的表進(jìn)行增、刪、改操作,但可以對用戶臨時(shí)表做增、刪、改操作。

可以通過 START TRANSACTION READ WRITE 語句開啟一個(gè)讀寫事務(wù),或者使用 BEGIN、START TRANSACTION 語句開啟的事務(wù)默認(rèn)也算是讀寫事務(wù)。

在讀寫事務(wù)中可以對表執(zhí)行增刪改查操作。

如果某個(gè)事務(wù)執(zhí)行過程中對某個(gè)表執(zhí)行了增、刪、改操作,那么 InnoDB 存儲引擎就會(huì)給它分配一個(gè)獨(dú)一無二的事務(wù) id,針對 MySQL 5.7 分配方式如下:

  • 對于只讀事務(wù)來說,只有在它第一次對某個(gè)用戶創(chuàng)建的臨時(shí)表執(zhí)行增、刪、改操作時(shí)才會(huì)為這個(gè)事務(wù)分配一個(gè)事務(wù) id,否則的話是不分配事務(wù) id 的。
  • 對于讀寫事務(wù)來說,只有在它第一次對某個(gè)表執(zhí)行增、刪、改操作時(shí)才會(huì)為這個(gè)事務(wù)分配一個(gè)事務(wù) id,否則的話也是不分配事務(wù) id 的。
  • 有的時(shí)候雖然開啟了一個(gè)讀寫事務(wù),但是在這個(gè)事務(wù)中全是查詢語句,并沒有執(zhí)行增、刪、改的語句,那也就意味著這個(gè)事務(wù)并不會(huì)被分配一個(gè)事務(wù) id。

這個(gè)事務(wù) id 本質(zhì)上就是一個(gè)數(shù)字,它的分配策略和隱藏列 row_id 的分配策略大抵相同,具體策略如下:

  • 服務(wù)器會(huì)在內(nèi)存中維護(hù)一個(gè)全局變量,每當(dāng)需要為某個(gè)事務(wù)分配一個(gè)事務(wù) id 時(shí),就會(huì)把該變量的值當(dāng)作事務(wù) id 分配給該事務(wù),并且把該變量自增 1。
  • 每當(dāng)這個(gè)變量的值為 256 的倍數(shù)時(shí),就會(huì)將該變量的值刷新到系統(tǒng)表空間的頁號為 5 的頁面中一個(gè)稱之為 Max Trx ID 的屬性處,這個(gè)屬性占用 8 個(gè)字節(jié)的存 儲空間。
  • 當(dāng)系統(tǒng)下一次重新啟動(dòng)時(shí),會(huì)將上邊提到的 Max Trx ID 屬性加載到內(nèi)存中,將該值加上 256 之后賦值給全局變量,因?yàn)樵谏洗侮P(guān)機(jī)時(shí)該全局變量的值可能大于 Max Trx ID 屬性值。
  • 這樣就可以保證整個(gè)系統(tǒng)中分配的事務(wù) id 值是一個(gè)遞增的數(shù)字。先被分配 id 的事務(wù)得到的是較小的事務(wù) id,后被分配 id 的事務(wù)得到的是較大的事務(wù) id。
mvcc

全稱 Multi-Version Concurrency Control,即多版本并發(fā)控制,主要是為了提高數(shù)據(jù)庫的并發(fā)性能。

同一行數(shù)據(jù)平時(shí)發(fā)生讀寫請求時(shí),會(huì)上鎖阻塞住。但 MVCC 用更好的方式去處理讀寫請求,做到在發(fā)生讀寫請求沖突時(shí)不用加鎖。

這個(gè)讀是指的快照讀,而不是當(dāng)前讀,當(dāng)前讀是一種加鎖操作,是悲觀鎖。

MVCC 原理

在事務(wù)并發(fā)執(zhí)行遇到的問題如下:

  • 臟讀:如果一個(gè)事務(wù)讀到了另一個(gè)未提交事務(wù)修改過的數(shù)據(jù),那就意味著發(fā)生了臟讀;
  • 不可重復(fù)讀:如果一個(gè)事務(wù)只能讀到另一個(gè)已經(jīng)提交的事務(wù)修改過的數(shù)據(jù),并且其他事務(wù)每對該數(shù)據(jù)進(jìn)行一次修改并提交后,該事務(wù)都能查詢得到最新值,那就意味著發(fā)生了不可重復(fù)讀;
  • 幻讀:如果一個(gè)事務(wù)先根據(jù)某些條件查詢出一些記錄,之后另一個(gè)事務(wù)又向表中插入了符合這些條件的記錄,原先的事務(wù)再次按照該條件查詢時(shí),能把另一個(gè)事務(wù)插入的記錄也讀出來,那就意味著發(fā)生了幻讀,幻讀強(qiáng)調(diào)的是一個(gè)事務(wù)按照某個(gè)相同條件多次讀取記錄時(shí),后讀取時(shí)讀到了之前沒有讀到的記錄,幻讀只是重點(diǎn)強(qiáng)調(diào)了讀取到了之前讀取沒有獲取到的記錄。

MySQL 在 REPEATABLE READ 隔離級別下,是可以很大程度避免幻讀問題的發(fā)生的。

版本鏈

對于使用 InnoDB 存儲引擎的表來說,它的聚簇索引記錄中都包含兩個(gè)必要的隱藏列:

  • trx_id:每次一個(gè)事務(wù)對某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把該事務(wù)的事務(wù) id 賦值給 trx_id 隱藏列;
  • roll_pointer:每次對某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把舊的版本寫入到 undo 日志中,然后這個(gè)隱藏列就相當(dāng)于一個(gè)指針,可以通過它來找到該記錄修 改前的信息;

演示

-- 創(chuàng)建表

CREATETABLEmvcc_test (

idINT,

nameVARCHAR(100),

domAInvarchar(100),

PRIMARYKEY(id)

)Engine=InnoDBCHARSET=utf8;

-- 添加數(shù)據(jù)

INSERTINTOmvcc_test VALUES(1,'habit','演示mvcc');

假設(shè)插入該記錄的事務(wù) id=50,那么該條記錄的展示如圖:

?假設(shè)之后兩個(gè)事務(wù) id 分別為 70、90 的事務(wù)對這條記錄進(jìn)行 UPDATE 操作。

trx_id=70 trx_id=90
begin  
  begin
   
update mvcc_test set name='habit_trx_id_70_01' where id=1  
update mvcc_test set name='habit_trx_id_70_02' where id=1  
commit  
  update mvcc_test set name='habit_trx_id_90_01' where id=1
  update mvcc_test set name='habit_trx_id_90_02' where id=1
  commit

每次對記錄進(jìn)行改動(dòng),都會(huì)記錄一條 undo 日志,每條 undo 日志也都有一個(gè) roll_pointer 屬性,可以將這些 undo 日志都連起來,串成一個(gè)鏈表。

MYSQL事務(wù)的底層原理

對該記錄每次更新后,都會(huì)將舊值放到一條 undo 日志中,就算是該記錄的一個(gè)舊版本,隨著更新次數(shù)的增多,所有的版本都會(huì)被 roll_pointer 屬性連接成一個(gè)鏈表,把這個(gè)鏈表稱之為版本鏈,版本鏈的頭節(jié)點(diǎn)就是當(dāng)前記錄最新的值。另外,每個(gè)版本中還包含生成該版本時(shí)對應(yīng)的事務(wù) id。于是可以利用這個(gè)記錄的版本鏈來控制并發(fā)事務(wù)訪問相同記錄的行為,那么這種機(jī)制就被稱之為:多版本并發(fā)控制,即 MVCC。

ReadView

對于使用 READ UNCOMMITTED 隔離級別的事務(wù)來說,由于可以讀到未提交事務(wù)修改過的記錄,所以直接讀取記錄的最新版本就好了。

對于使用 SERIALIZABLE 隔離級別的事務(wù)來說,InnoDB 使用加鎖的方式來訪問記錄。

對于使用 READ COMMITTED 和 REPEATABLE READ 隔離級別的事務(wù)來說,都必須保證讀到已經(jīng)提交了的事務(wù)修改過的記錄,也就是說假如另一個(gè)事務(wù)已經(jīng)修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的,核心問題就是:READ COMMITTED 和 REPEATABLE READ 隔離級別在不可重復(fù)讀和幻讀上的區(qū)別是從哪里來的,其實(shí)結(jié)合前面的知識,這兩種隔離級別關(guān)鍵是需要判斷一下版本鏈中的哪個(gè)版本是當(dāng)前事務(wù)可見的。

為此,InnoDB 提出了一個(gè) ReadView 的概念,這個(gè) ReadView 中主要包含 4 個(gè)比較重要的內(nèi)容:

  • m_ids:表示在生成 ReadView 時(shí)當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)的事務(wù) id 列表;
  • min_trx_id:表示在生成 ReadView 時(shí)當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)中最小的事務(wù) id,也就是 m_ids 中的最小值;
  • max_trx_id:表示在生成 ReadView 時(shí)系統(tǒng)中應(yīng)該分配給下一個(gè)事務(wù)的 id 值,注:max_trx_id 并不是 m_ids 中的最大值,事務(wù) id 是遞增分配的。比方說現(xiàn)在有 id 為 1,2,3 這三個(gè)事務(wù),之后 id 為 3 的事務(wù)提交了。那么一個(gè)新的讀事務(wù)在生成 ReadView 時(shí),m_ids 就包括 1 和 2,min_trx_id 的值就是 1,max_trx_id 的值就是 4;
  • creator_trx_id:表示生成該 ReadView 的事務(wù)的事務(wù) id;

有了這個(gè) ReadView,這樣在訪問某條記錄時(shí),只需要按照下邊的步驟判斷記錄的某個(gè)版本是否可見:

  1. 如果被訪問版本的 trx_id 屬性值與 ReadView 中的 creator_trx_id 值相同,意味著當(dāng)前事務(wù)在訪問它自己修改過的記錄,所以該版本可以被當(dāng)前事務(wù)訪問;
  2. 如果被訪問版本的 trx_id 屬性值小于 ReadView 中的 min_trx_id 值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成 ReadView 前已經(jīng)提交,所以該版本可以被當(dāng)前事務(wù)訪問;
  3. 如果被訪問版本的 trx_id 屬性值大于或等于 ReadView 中的 max_trx_id 值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成 ReadView 后才開啟,所以該版本不可以被當(dāng)前事務(wù)訪問;
  4. 如果被訪問版本的 trx_id 屬性值在 ReadView 的 min_trx_id 和 max_trx_id 之間 min_trx_id < trx_id < max_trx_id,那就需要判斷一下 trx_id 屬性值是不是在 m_ids 列表中,如果在,說明創(chuàng)建 ReadView 時(shí)生成該版本的事務(wù)還是活躍的,該版本不可以被訪問;如果不在,說明創(chuàng)建 ReadView 時(shí)生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問;
  5. 如果某個(gè)版本的數(shù)據(jù)對當(dāng)前事務(wù)不可見的話,那就順著版本鏈找到下一個(gè)版本的數(shù)據(jù),繼續(xù)按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最后一個(gè)版本。如果最后一個(gè)版本也不可見的話,那么就意味著該條記錄對該事務(wù)完全不可見,查詢結(jié)果就不包含該記錄;

在 MySQL 中,READ COMMITTED 和 REPEATABLE READ 隔離級別的一個(gè)非常大的區(qū)別就是它們生成 ReadView 的時(shí)機(jī)不同。

還是以表 mvcc_test 為例,假設(shè)現(xiàn)在表 mvcc_test 中只有一條由事務(wù) id 為 50 的事務(wù)插入的一條記錄,接下來看一下 READ COMMITTED 和 REPEATABLE READ 所謂的生成 ReadView 的時(shí)機(jī)不同到底不同在哪里。

READ COMMITTED:每次讀取數(shù)據(jù)前都生成一個(gè) ReadView;

比方說現(xiàn)在系統(tǒng)里有兩個(gè)事務(wù) id 分別為 70、90 的事務(wù)在執(zhí)行:

-- T 70

UPDATEmvcc_test SETname='habit_trx_id_70_01'WHEREid=1;

UPDATEmvcc_test SETname='habit_trx_id_70_02'WHEREid=1;

此時(shí)表 mvcc_test 中 id 為 1 的記錄得到的版本鏈表如下所示:

MYSQL事務(wù)的底層原理

假設(shè)現(xiàn)在有一個(gè)使用 READ COMMITTED 隔離級別的事務(wù)開始執(zhí)行:

-- 使用 READ COMMITTED 隔離級別的事務(wù)

BEGIN;

-- SELECE1:Transaction 70、90 未提交

SELECT*FROMmvcc_test WHEREid=1;

-- 得到的列 name 的值為'habit'

這個(gè) SELECE1 的執(zhí)行過程如下:

在執(zhí)行 SELECT 語句時(shí)會(huì)先生成一個(gè) ReadView,ReadView 的 m_ids 列表的內(nèi)容就是 [70, 90],min_trx_id 為 70,max_trx_id 為 91,creator_trx_id 為 0。

然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列 name 的內(nèi)容是 habit_trx_id_70_02,該版本的 trx_id 值為 70,在 m_ids 列表內(nèi),所以不符合可見性要求第 4 條:如果被訪問版本的 trx_id 屬性值在 ReadView 的 min_trx_id 和 max_trx_id之間 min_trx_id < trx_id < max_trx_id,那就需要判斷一下trx_id 屬性值是不是在 m_ids 列表中,如果在,說明創(chuàng)建 ReadView 時(shí)生成該版本的事務(wù)還是活躍的,該版本不可以被訪問;如果不在,說明創(chuàng)建 ReadView 時(shí)生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問。根據(jù) roll_pointer 跳到下一個(gè)版本。

下一個(gè)版本的列 name 的內(nèi)容是 habit_trx_id_70_01,該版本的 trx_id 值也為 70,也在 m_ids 列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。

下一個(gè)版本的列 name 的內(nèi)容是 habit,該版本的 trx_id 值為 50,小于 ReadView 中的 min_trx_id 值,所以這個(gè)版本是符合要求的第 2 條:如果被訪問版本的 trx_id 屬性值小于 ReadView 中的 min_trx_id 值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成 ReadView 前已經(jīng)提交,所以該版本可以被當(dāng)前事務(wù)訪問。最后返回的版本就是這條列 name 為 habit 的記錄。

之后,把事務(wù) id 為 70 的事務(wù)提交一下,然后再到事務(wù) id 為 90 的事務(wù)中更新一下表 mvcc_test 中 id 為 1 的記錄:

-- T 90

UPDATEmvcc_test SETname='habit_trx_id_90_01'WHEREid=1;

UPDATEmvcc_test SETname='habit_trx_id_90_02'WHEREid=1;

此時(shí)表 mvcc 中 id 為 1 的記錄的版本鏈就長這樣:

MYSQL事務(wù)的底層原理

然后再到剛才使用 READ COMMITTED 隔離級別的事務(wù)中繼續(xù)查找這個(gè) id 為 1 的記錄,如下:

-- 使用 READ COMMITTED 隔離級別的事務(wù)

BEGIN;

-- SELECE1:Transaction 70、90 均未提交

SELECT*FROMmvcc_test WHEREid=1;-- 得到的列 name 的值為'habit'

-- SELECE2:Transaction 70 提交,Transaction 90 未提交

SELECT*FROMmvcc_test WHEREid=1;-- 得到的列 name 的值為'habit_trx_id_70_02'

這個(gè) SELECE2 的執(zhí)行過程如下:

在執(zhí)行 SELECT 語句時(shí)又會(huì)單獨(dú)生成一個(gè) ReadView,該 ReadView 的 m_ids 列表的內(nèi)容就是 [90],min_trx_id 為 90,max_trx_id 為 91,creator_trx_id 為 0。

然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列 name 的內(nèi)容是 habit_trx_id_90_02,該版本的 trx_id 值為 90,在 m_ids 列表內(nèi),所以不符合可見性要求,根據(jù) roll_pointer 跳到下一個(gè)版本。

下一個(gè)版本的列 name 的內(nèi)容是 habit_trx_id_90_01,該版本的 trx_id 值為 90,也在 m_ids 列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。

下一個(gè)版本的列 name 的內(nèi)容是 habit_trx_id_70_02,該版本的 trx_id 值為 70,小于 ReadView 中的 min_trx_id 值 90,所以這個(gè)版本是符合要求的,最后返回這個(gè)版本中列 name 為 habit_trx_id_70_02 的記錄。

以此類推,如果之后事務(wù) id 為 90 的記錄也提交了,再次在使用 READ COMMITTED 隔離級別的事務(wù)中查詢表 mvcc_test 中 id 值為 1 的記錄時(shí),得到的結(jié)果就是 habit_trx_id_90_02 了。

總結(jié):使用 READ COMMITTED 隔離級別的事務(wù)在每次查詢開始時(shí)都會(huì)生成一個(gè)獨(dú)立的 ReadView。

REPEATABLE READ:在第一次讀取數(shù)據(jù)時(shí)生成一個(gè) ReadView;

對于使用 REPEATABLE READ 隔離級別的事務(wù)來說,只會(huì)在第一次執(zhí)行查詢語句時(shí)生成一個(gè) ReadView,之后的查詢就不會(huì)重復(fù)生成了。

比方說現(xiàn)在系統(tǒng)里有兩個(gè)事務(wù) id 分別為 70、90 的事務(wù)在執(zhí)行:

-- T 70

UPDATEmvcc_test SETname='habit_trx_id_70_01'WHEREid=1;

UPDATEmvcc_test SETname='habit_trx_id_70_02'WHEREid=1;

此時(shí)表 mvcc_test 中 id 為 1 的記錄得到的版本鏈表如下所示:

MYSQL事務(wù)的底層原理

?假設(shè)現(xiàn)在有一個(gè)使用 REPEATABLE READ 隔離級別的事務(wù)開始執(zhí)行:

-- 使用 REPEATABLE READ 隔離級別的事務(wù)

BEGIN;

-- SELECE1:Transaction 70、90 未提交

SELECT*FROMmvcc_test WHEREid=1;-- 得到的列name 的值為'habit'

這個(gè) SELECE1 的執(zhí)行過程如下:

在執(zhí)行 SELECT 語句時(shí)會(huì)先生成一個(gè) ReadView,ReadView 的 m_ids 列表的內(nèi)容就是 [70, 90],min_trx_id 為 70,max_trx_id 為 91,creator_trx_id 為 0。

然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列 name 的內(nèi)容是 habit_trx_id_70_02,該版本的 trx_id 值為 70,在 m_ids 列表內(nèi),所以不符合可見性要求,根據(jù) roll_pointer 跳到下一個(gè)版本。

下一個(gè)版本的列 name 的內(nèi)容是 habit_trx_id_70_01,該版本的 trx_id 值也為 70,也在 m_ids 列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。

下一個(gè)版本的列 name 的內(nèi)容是 habit,該版本的 trx_id 值為 50,小于 ReadView 中的 min_trx_id 值,所以這個(gè)版本是符合要求的,最后返回的就是這條列 name 為 habit 的記錄。

之后,把事務(wù) id 為 70 的事務(wù)提交一下,然后再到事務(wù) id 為 90 的事務(wù)中更新一下表 mvcc_test 中 id 為 1 的記錄:

-- 使用 REPEATABLE READ 隔離級別的事務(wù)

BEGIN;

UPDATEmvcc_test SETname='habit_trx_id_90_01'WHEREid=1;

UPDATEmvcc_test SETname='habit_trx_id_90_02'WHEREid=1;

此刻,表 mvcc_test 中 id 為 1 的記錄的版本鏈就長這樣:

MYSQL事務(wù)的底層原理

然后再到剛才使用 REPEATABLE READ 隔離級別的事務(wù)中繼續(xù)查找這個(gè) id 為 1 的記錄,如下:

-- 使用 REPEATABLE READ 隔離級別的事務(wù)

BEGIN;

-- SELECE1:Transaction 70、90 均未提交

SELECT*FROMmvcc_test WHEREid=1;-- 得到的列 name 的值為'habit'

-- SELECE2:Transaction 70 提交,Transaction 90 未提交

SELECT*FROMmvcc_test WHEREid=1;-- 得到的列 name 的值為'habit'

這個(gè) SELECE2 的執(zhí)行過程如下:

因?yàn)楫?dāng)前事務(wù)的隔離級別為 REPEATABLE READ,而之前在執(zhí)行 SELECE1 時(shí)已經(jīng)生成過 ReadView 了,所以此時(shí)直接復(fù)用之前的 ReadView,之前的 ReadView 的 m_ids 列表的內(nèi)容就是 [70, 90],min_trx_id 為 70,max_trx_id 為 91, creator_trx_id 為 0。

然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列 name 的內(nèi)容是 habit_trx_id_90_02,該版本的 trx_id 值為 90,在 m_ids 列表內(nèi),所以不符合可見性要求,根據(jù) roll_pointer 跳到下一個(gè)版本。

下一個(gè)版本的列 name 的內(nèi)容是 habit_trx_id_90_01,該版本的 trx_id 值為 90,也在 m_ids 列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。

下一個(gè)版本的列 name 的內(nèi)容是 habit_trx_id_70_02,該版本的 trx_id 值為 70,而 m_ids 列表中是包含值為 70 的事務(wù) id 的,所以該版本也不符合要求,同理下一個(gè)列 name 的內(nèi)容是 habit_trx_id_70_01 的版本也不符合要求。繼續(xù)跳到下一個(gè)版本。

下一個(gè)版本的列 name 的內(nèi)容是 habit,該版本的 trx_id 值為 50,小于 ReadView 中的 min_trx_id 值 70,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列 name 為 habit 的記錄。

也就是說兩次 SELECT 查詢得到的結(jié)果是重復(fù)的,記錄的列 name 值都是 habit,這就是可重復(fù)讀的含義。如果之后再把事務(wù) id 為 90 的記錄提交了,然后再到剛才使用 REPEATABLE READ 隔離級別的事務(wù)中繼續(xù)查找這個(gè) id 為 1 的記錄,得到的結(jié)果還是 habit。

MVCC 下的幻讀解決和幻讀現(xiàn)象

REPEATABLE READ 隔離級別下 MVCC 可以解決不可重復(fù)讀問題,那么幻讀呢?MVCC 是怎么解決的?幻讀是一個(gè)事務(wù)按照某個(gè)相同條件多次讀取記錄時(shí),后讀取時(shí)讀到了之前沒有讀到的記錄,而這個(gè)記錄來自另一個(gè)事務(wù)添加的新記錄。

可以想想,在 REPEATABLE READ 隔離級別下的事務(wù) T1 先根據(jù)某個(gè)搜索條件讀取到多條記錄,然后事務(wù) T2 插入一條符合相應(yīng)搜索條件的記錄并提交,然后事務(wù) T1 再根據(jù)相同搜索條件執(zhí)行查詢。結(jié)果會(huì)是什么?按照 ReadView 中的比較規(guī)則中的第 3 條和第 4 條不管事務(wù) T2 比事務(wù) T1 是否先開啟,事務(wù) T1 都是看不到 T2 的提交的。

但是,在 REPEATABLE READ 隔離級別下 InnoDB 中的 MVCC 可以很大程度地避免幻讀現(xiàn)象,而不是完全禁止幻讀。怎么回事呢?來看下面的情況:

MYSQL事務(wù)的底層原理

?首先在事務(wù) T1 中執(zhí)行:select * from mvcc_test where id = 30; 這個(gè)時(shí)候是找不到 id = 30 的記錄的。

在事務(wù) T2 中,執(zhí)行插入語句:insert into mvcc_test values(30,'luxi','luxi');

此時(shí)回到事務(wù) T1,執(zhí)行:

updatemvcc_test setdomain='luxi_t1'whereid=30;

select*frommvcc_test whereid=30;

事務(wù) T1 很明顯出現(xiàn)了幻讀現(xiàn)象。

在 REPEATABLE READ 隔離級別下,T1 第一次執(zhí)行普通的 SELECT 語句時(shí)生成了一個(gè) ReadView,之后 T2 向 mvcc_test 表中新插入一條記錄并提交。

ReadView 并不能阻止 T1 執(zhí)行 UPDATE 或者 DELETE 語句來改動(dòng)這個(gè)新插入的記錄,由于 T2 已經(jīng)提交,因此改動(dòng)該記錄并不會(huì)造成阻塞,但是這樣一來,這條新記錄的 trx_id 隱藏列的值就變成了 T1 的事務(wù) id。之后 T1 再使用普通的 SELECT 語句去查詢這條記錄時(shí)就可以看到這條記錄了,也就可以把這條記錄返回給客戶端。因?yàn)檫@個(gè)特殊現(xiàn)象的存在,可以認(rèn)為 MVCC 并不能完全禁止幻讀。

mvcc 總結(jié)

從上邊的描述中可以看出來,所謂的 MVCC(Multi-Version Concurrency Control ,多版本并發(fā)控制)指的就是在使用 READ COMMITTD、REPEATABLE READ 這兩種隔離級別的事務(wù)在執(zhí)行普通的 SELECT 操作時(shí)訪問記錄的版本鏈的過程,這樣子可以使不同事務(wù)的讀寫、寫讀操作并發(fā)執(zhí)行,從而提升系統(tǒng)性能。

READ COMMITTD、REPEATABLE READ 這兩個(gè)隔離級別的一個(gè)很大不同就是:生成 ReadView 的時(shí)機(jī)不同,READ COMMITTD 在每一次進(jìn)行普通 SELECT 操作前都會(huì)生成一個(gè) ReadView,而 REPEATABLE READ 只在第一次進(jìn)行普通 SELECT 操作前生成一個(gè) ReadView,之后的查詢操作都重復(fù)使用這個(gè) ReadView 就好了,從而基本上可以避免幻讀現(xiàn)象。

InnoDB 的 Buffer Pool

對于使用 InnoDB 作為存儲引擎的表來說,不管是用于存儲用戶數(shù)據(jù)的索引,包括:聚簇索引和二級索引,還是各種系統(tǒng)數(shù)據(jù),都是以頁的形式存放在表空間中的,而所謂的表空間只不過是 InnoDB 對文件系統(tǒng)上一個(gè)或幾個(gè)實(shí)際文件的抽象,也就是說數(shù)據(jù)還是存儲在磁盤上的。

但是磁盤的速度慢,所以 InnoDB 存儲引擎在處理客戶端的請求時(shí),當(dāng)需要訪問某個(gè)頁的數(shù)據(jù)時(shí),就會(huì)把完整的頁的數(shù)據(jù)全部加載到內(nèi)存中,即使只需要訪問一個(gè)頁的一條記錄,那也需要先把整個(gè)頁的數(shù)據(jù)加載到內(nèi)存中。將整個(gè)頁加載到內(nèi)存中后就可以進(jìn)行讀寫訪問了,在進(jìn)行完讀寫訪問之后并不著急把該頁對應(yīng)的內(nèi)存空間釋放掉,而是將其緩存起來,這樣將來有請求再次訪問該頁面時(shí),就可以省去磁盤 IO 的開銷了。

Buffer Pool

InnoDB 為了緩存磁盤中的頁,在 MySQL 服務(wù)器啟動(dòng)的時(shí)候就向操作系統(tǒng)申請了一片連續(xù)的內(nèi)存,這塊連續(xù)內(nèi)存叫做:Buffer Pool,中文名:緩沖池。

默認(rèn)情況下 Buffer Pool 只有 128M 大小。

查看該值:show variables like 'innodb_buffer_pool_size';

可以在啟動(dòng)服務(wù)器的時(shí)候配置 innodb_buffer_pool_size 參數(shù)的值,它表示 Buffer Pool 的大小,配置如下:

[server]

innodb_buffer_pool_size= 268435456

其中,268435456 的單位是字節(jié),也就是指定 Buffer Pool 的大小為 256M,Buffer Pool 也不能太小,最小值為 5M,當(dāng)小于該值時(shí)會(huì)自動(dòng)設(shè)置成 5M。

啟動(dòng) MySQL 服務(wù)器的時(shí)候,需要完成對 Buffer Pool 的初始化過程,就是先向操作系統(tǒng)申請 Buffer Pool 的內(nèi)存空間,然后把它劃分成若干對控制塊和緩 存頁。但是此時(shí)并沒有真實(shí)的磁盤頁被緩存到 Buffer Pool 中,之后隨著程序的運(yùn)行,會(huì)不斷的有磁盤上的頁被緩存到 Buffer Pool 中。

在 Buffer Pool 中會(huì)創(chuàng)建多個(gè)緩存頁,默認(rèn)的緩存頁大小和在磁盤上默認(rèn)的頁大小是一樣的,都是 16KB。

那么怎么知道該頁在不在 Buffer Pool 中呢?

在查找數(shù)據(jù)的時(shí)候,先通過哈希表中查找 key 是否在哈希表中,如果在證明 Buffer Pool 中存在該緩存也信息,如果不存在證明不存該緩存也信息,則通過讀取磁盤加載該頁信息放到 Buffer Pool 中,哈希表中的 key 是通過表空間號 + 頁號作組成的,value 是 Buffer Pool 的緩存頁。

flush 鏈表的管理

如果修改了 Buffer Pool 中某個(gè)緩存頁的數(shù)據(jù),那它就和磁盤上的頁不一致了,這樣的緩存頁也被稱為:臟頁。最簡單的做法就是每發(fā)生一次修改就立即同步到磁盤上對應(yīng)的頁上,但是頻繁的往磁盤中寫數(shù)據(jù)會(huì)嚴(yán)重的影響程序的性能。所以每次修改緩存頁后,并不著急把修改同步到磁盤上,而是在未來的某個(gè)時(shí)間進(jìn)行同步。 但是如果不立即同步到磁盤的話,那之后再同步的時(shí)候怎么知道 Buffer Pool 中哪些頁是臟頁,哪些頁從來沒被修改過呢?總不能把所有的緩存頁都同步到磁盤上吧,如果 Buffer Pool 被設(shè)置的很大,那一次性同步會(huì)非常慢。

所以,需要再創(chuàng)建一個(gè)存儲臟頁的鏈表,凡是修改過的緩存頁對應(yīng)的控制塊都會(huì)作為一個(gè)節(jié)點(diǎn)加入到一個(gè)鏈表中,因?yàn)檫@個(gè)鏈表節(jié)點(diǎn)對應(yīng)的緩存頁都是需要被刷新到磁盤上的,所以也叫 flush 鏈表。

刷新臟頁到磁盤

后臺有專門的線程每隔一段時(shí)間負(fù)責(zé)把臟頁刷新到磁盤,這樣可以不影響用戶線程處理正常的請求。

從 flush 鏈表中刷新一部分頁面到磁盤,后臺線程也會(huì)定時(shí)從 flush 鏈表中刷新一部分頁面到磁盤,刷新的速率取決于當(dāng)時(shí)系統(tǒng)是不是很繁忙。這種刷新頁面的方式被稱之為:BUF_FLUSH_LIST。

redo 日志redo 日志的作用

InnoDB 存儲引擎是以頁為單位來管理存儲空間的,增刪改查操作其實(shí)本質(zhì)上都是在訪問頁面,包括:讀頁面、寫頁面、創(chuàng)建新頁面等操作。在真正訪問頁面之前,需要把在磁盤上的頁緩存到內(nèi)存中的 Buffer Pool 之后才可以訪問。但是在事務(wù)的時(shí)候又強(qiáng)調(diào)過一個(gè)稱之為持久性的特性,就是說對于一個(gè)已經(jīng)提交的事務(wù),在事務(wù)提交后即使系統(tǒng)發(fā)生了崩潰,這個(gè)事務(wù)對數(shù)據(jù)庫中所做的更改也不能丟失。

如果只在內(nèi)存的 Buffer Pool 中修改了頁面,假設(shè)在事務(wù)提交后突然發(fā)生了某個(gè)故障,導(dǎo)致內(nèi)存中的數(shù)據(jù)都失效了,那么這個(gè)已經(jīng)提交了的事務(wù)對數(shù)據(jù)庫中所做的更改也就跟著丟失了,這是所不能忍受的。那么如何保證這個(gè)持久性呢?一個(gè)很簡單的做法就是在事務(wù)提交完成之前把該事務(wù)所修改的所有頁面都刷新到磁盤,但是這個(gè)簡單粗暴的做法有些問題:

  1. 刷新一個(gè)完整的數(shù)據(jù)頁太浪費(fèi)了;有時(shí)候僅僅修改了某個(gè)頁面中的一個(gè)字節(jié),但是在 InnoDB 中是以頁為單位來進(jìn)行磁盤 IO 的,也就是說在該事務(wù)提交時(shí)不得不將一個(gè)完整的頁面從內(nèi)存中刷新到磁盤,一個(gè)頁面默認(rèn)是 16KB 大小,只修改一個(gè)字節(jié)就要刷新 16KB 的數(shù)據(jù)到磁盤上顯然是太浪費(fèi)了。
  2. 隨機(jī) IO 刷起來比較慢;一個(gè)事務(wù)可能包含很多語句,即使是一條語句也可能修改許多頁面,該事務(wù)修改的這些頁面可能并不相鄰,這就意味著在將某個(gè)事務(wù)修改的 Buffer Pool 中的頁面刷新到磁盤時(shí),需要進(jìn)行很多的隨機(jī) IO,隨機(jī) IO 比順序 IO 要慢,尤其對于傳統(tǒng)的機(jī)械硬盤來說。

只是想讓已經(jīng)提交了的事務(wù)對數(shù)據(jù)庫中數(shù)據(jù)所做的修改永久生效,即使后來系統(tǒng)崩潰,在重啟后也能把這種修改恢復(fù)出來。其實(shí)沒有必要在每次事務(wù)提交時(shí)就把該事務(wù)在內(nèi)存中修改過的全部頁面刷新到磁盤,只需要把修改了哪些東西記錄一下就好,比方說:某個(gè)事務(wù)將系統(tǒng)表空間中的第 5 號頁面中偏移量為 5000 處的那個(gè)字節(jié)的值 0 改成 5 只需要記錄一下:將第 5 號表空間的 5 號頁面的偏移量為 5000 處的值更新為:5。

這樣在事務(wù)提交時(shí),把上述內(nèi)容刷新到磁盤中,即使之后系統(tǒng)崩潰了,重啟之后只要按照上述內(nèi)容所記錄的步驟重新更新一下數(shù)據(jù)頁,那么該事務(wù)對數(shù)據(jù)庫中所做的修改又可以被恢復(fù)出來,也就意味著滿足持久性的要求。因?yàn)樵谙到y(tǒng)崩潰重啟時(shí)需要按照上述內(nèi)容所記錄的步驟重新更新數(shù)據(jù)頁,所以上述內(nèi)容也被稱之為:重做日志,即:redo log。與在事務(wù)提交時(shí)將所有修改過的內(nèi)存中的頁面刷新到磁盤中相比,只將該事務(wù)執(zhí)行過程中產(chǎn)生的 redo log 刷新到磁盤的好處如下:

  1. redo log 占用的空間非常小存儲表空間 ID、頁號、偏移量以及需要更新的值所需的存儲空間是很小的;
  2. redo log 是順序?qū)懭氪疟P的在執(zhí)行事務(wù)的過程中,每執(zhí)行一條語句,就可能產(chǎn)生若干條 redo log,這些日志是按照產(chǎn)生的順序?qū)懭氪疟P的,也就是使用順序 IO;
redo log 的寫入過程

InnoDB 為了更好的進(jìn)行系統(tǒng)崩潰恢復(fù),把一次原子操作生成的 redo log 都放在了大小為 512 字節(jié)的塊(block)中。

為了解決磁盤速度過慢的問題而引入了 Buffer Pool。同理,寫入 redo log 時(shí)也不能直接寫到磁盤上,實(shí)際上在服務(wù)器啟動(dòng)時(shí)就向操作系統(tǒng)申請了一大片稱之為 redo log buffer 的連續(xù)內(nèi)存空間,即:redo log 緩沖區(qū),也可以簡稱:log buffer。這片內(nèi)存空間被劃分成若干個(gè)連續(xù)的 redo log block,可以通過啟動(dòng)參數(shù) innodb_log_buffer_size 來指定 log buffer 的大小,該啟動(dòng)參數(shù)的默認(rèn)值為:16MB。

向 log buffer 中寫入 redo log 的過程是順序的,也就是先往前邊的 block 中寫,當(dāng)該 block 的空閑空間用完之后再往下一個(gè) block 中寫。

redo log 刷盤時(shí)機(jī)

log buffer 什么時(shí)候會(huì)寫入到磁盤呢?

  • log buffer 空間不足時(shí),如果不停的往這個(gè)有限大小的 log buffer 里塞入日志,很快它就會(huì)被填滿。InnoDB 認(rèn)為如果當(dāng)前寫入 log buffer 的 redo log 量已 經(jīng)占滿了 log buffer 總?cè)萘康拇蠹s一半左右,就需要把這些日志刷新到磁盤上。
  • 事務(wù)提交時(shí),必須要把修改這些頁面對應(yīng)的 redo log 刷新到磁盤。
  • 后臺有一個(gè)線程,大約每秒都會(huì)刷新一次 log buffer 中的 redo log 到磁盤。
  • 正常關(guān)閉服務(wù)器時(shí)等等。
undo 日志

事務(wù)需要保證原子性,也就是事務(wù)中的操作要么全部完成,要么什么也不做。但是偏偏有時(shí)候事務(wù)執(zhí)行到一半會(huì)出現(xiàn)一些情況,比如:

  • 情況一:事務(wù)執(zhí)行過程中可能遇到各種錯(cuò)誤,比如服務(wù)器本身的錯(cuò)誤,操作系統(tǒng)錯(cuò)誤,甚至是突然斷電導(dǎo)致的錯(cuò)誤。
  • 情況二:程序員可以在事務(wù)執(zhí)行過程中手動(dòng)輸入 ROLLBACK 語句結(jié)束當(dāng)前的事務(wù)的執(zhí)行。

這兩種情況都會(huì)導(dǎo)致事務(wù)執(zhí)行到一半就結(jié)束,但是事務(wù)執(zhí)行過程中可能已經(jīng)修改了很多東西,為了保證事務(wù)的原子性,需要把東西改回原先的樣子,這個(gè)過程就稱之為回滾,即:rollback,這樣就可以造成這個(gè)事務(wù)看起來什么都沒做,所以符合原子性要求。

每當(dāng)要對一條記錄做改動(dòng)時(shí),都需要把回滾時(shí)所需的東西都給記下來。

比方說:

  • 插入一條記錄時(shí),至少要把這條記錄的主鍵值記下來,之后回滾的時(shí)候只需要把這個(gè)主鍵值對應(yīng)的記錄刪掉。
  • 刪除了一條記錄,至少要把這條記錄中的內(nèi)容都記下來,這樣之后回滾時(shí)再把由這些內(nèi)容組成的記錄插入到表中。
  • 修改了一條記錄,至少要把修改這條記錄前的舊值都記錄下來,這樣之后回滾時(shí)再把這條記錄更新為舊值。

這些為了回滾而記錄的這些東西稱之為撤銷日志,即:undo log。這里需要注意的一點(diǎn)是,由于查詢操作并不會(huì)修改任何用戶記錄,所以在查詢操作執(zhí)行時(shí),并不需要記錄相應(yīng)的 undo log。

undo 日志的格式

為了實(shí)現(xiàn)事務(wù)的原子性,InnoDB 存儲引擎在實(shí)際進(jìn)行增、刪、改一條記錄時(shí),都需要先把對應(yīng)的 undo 日志記下來。一般每對一條記錄做一次改動(dòng),就對應(yīng)著一條 undo 日志,但在某些更新記錄的操作中,也可能會(huì)對應(yīng)著 2 條 undo 日志。

一個(gè)事務(wù)在執(zhí)行過程中可能新增、刪除、更新若干條記錄,也就是說需要記錄很多條對應(yīng)的 undo 日志,這些 undo 日志會(huì)被從 0 開始編號,也就是說根據(jù)生成的順序分別被稱為第 0 號 undo 日志、第 1 號 undo 日志、...、第 n 號 undo 日志等,這個(gè)編號也被稱之為 undo no。

這些 undo 日志是被記錄到類型為 FIL_PAGE_UNDO_LOG 的頁面中。這些頁面可以從系統(tǒng)表空間中分配,也可以從一種專門存放 undo 日志的表空間,也就是所謂的 undo tablespace 中分配。

分享到:
標(biāo)簽:MYSQL
用戶無頭像

網(wǎng)友整理

注冊時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定