自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

MySQL事務(wù)的隔離性是如何實(shí)現(xiàn)的?

數(shù)據(jù)庫(kù) MySQL
「隔離的本質(zhì)就是控制并發(fā)」,如果SQL語(yǔ)句就是串行執(zhí)行的。那么數(shù)據(jù)庫(kù)的四大特性中就不會(huì)有隔離性這個(gè)概念了,也就不會(huì)有臟讀,不可重復(fù)讀,幻讀等各種問(wèn)題了。

[[422210]]

并發(fā)場(chǎng)景

最近做了一些分布式事務(wù)的項(xiàng)目,對(duì)事務(wù)的隔離性有了更深的認(rèn)識(shí),后續(xù)寫文章聊分布式事務(wù)。今天就復(fù)盤一下單機(jī)事務(wù)的隔離性是如何實(shí)現(xiàn)的?

「隔離的本質(zhì)就是控制并發(fā)」,如果SQL語(yǔ)句就是串行執(zhí)行的。那么數(shù)據(jù)庫(kù)的四大特性中就不會(huì)有隔離性這個(gè)概念了,也就不會(huì)有臟讀,不可重復(fù)讀,幻讀等各種問(wèn)題了

「對(duì)數(shù)據(jù)庫(kù)的各種并發(fā)操作,只有如下四種,寫寫,讀讀,讀寫和寫讀」

寫-寫

事務(wù)A更新一條記錄的時(shí)候,事務(wù)B能同時(shí)更新同一條記錄嗎?

答案肯定是不能的,不然就會(huì)造成「臟寫」問(wèn)題,那如何避免臟寫呢?答案就是「加鎖」

讀-讀

MySQL讀操作默認(rèn)情況下不會(huì)加鎖,所以可以并行的讀

讀-寫 和 寫-讀

「基于各種場(chǎng)景對(duì)并發(fā)操作容忍程度不同,MySQL就搞了個(gè)隔離性的概念」。你自己根據(jù)業(yè)務(wù)場(chǎng)景選擇隔離級(jí)別。

隔離級(jí)別 臟讀 不可重復(fù)讀 幻讀
read uncommitted(未提交讀)
read committed(提交讀) ×
repeatable read(可重復(fù)讀) × ×
serializable (可串行化) × × ×

「所以你看,MySQL是通過(guò)鎖和隔離級(jí)別對(duì)MySQL進(jìn)行并發(fā)控制的」

MySQL中的鎖

行級(jí)鎖

InnoDB存儲(chǔ)引擎中有如下兩種類型的行級(jí)鎖

「共享鎖」(Shared Lock,簡(jiǎn)稱S鎖),在事務(wù)需要讀取一條記錄時(shí),需要先獲取改記錄的S鎖

「排他鎖」(Exclusive Lock,簡(jiǎn)稱X鎖),在事務(wù)要改動(dòng)一條記錄時(shí),需要先獲取該記錄的X鎖

如果事務(wù)T1獲取了一條記錄的S鎖之后,事務(wù)T2也要訪問(wèn)這條記錄。如果事務(wù)T2想再獲取這個(gè)記錄的S鎖,可以成功,這種情況稱為鎖兼容,如果事務(wù)T2想再獲取這個(gè)記錄的X鎖,那么此操作會(huì)被阻塞,直到事務(wù)T1提交之后將S鎖釋放掉

如果事務(wù)T1獲取了一條記錄的X鎖之后,那么不管事務(wù)T2接著想獲取該記錄的S鎖還是X鎖都會(huì)被阻塞,直到事務(wù)1提交,這種情況稱為鎖不兼容。

「多個(gè)事務(wù)可以同時(shí)讀取記錄,即共享鎖之間不互斥,但共享鎖會(huì)阻塞排他鎖。排他鎖之間互斥」

S鎖和X鎖之間的兼容關(guān)系如下

兼容性 X鎖 S鎖
X鎖 互斥 互斥
S鎖 互斥 兼容

「update,delete,insert 都會(huì)自動(dòng)給涉及到的數(shù)據(jù)加上排他鎖,select 語(yǔ)句默認(rèn)不會(huì)加任何鎖」

那什么情況下會(huì)對(duì)讀操作加鎖呢?

  1. select .. lock in share mode,對(duì)讀取的記錄加S鎖
  2. select ... for update ,對(duì)讀取的記錄加X(jué)鎖
  3. 在事務(wù)中讀取記錄,對(duì)讀取的記錄加S鎖
  4. 事務(wù)隔離級(jí)別在 SERIALIZABLE 下,對(duì)讀取的記錄加S鎖

「InnoDB中有如下三種鎖」

Record Lock:對(duì)單個(gè)記錄加鎖

Gap Lock:間隙鎖,鎖住記錄前面的間隙,不允許插入記錄

Next-key Lock:同時(shí)鎖住數(shù)據(jù)和數(shù)據(jù)前面的間隙,即數(shù)據(jù)和數(shù)據(jù)前面的間隙都不允許插入記錄

寫個(gè)Demo演示一下

  1. CREATE TABLE `girl` ( 
  2.   `id` int(11) NOT NULL
  3.   `namevarchar(255), 
  4.   `age` int(11), 
  5.   PRIMARY KEY (`id`) 
  6. ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 
  1. insert into girl values 
  2. (1, '西施', 20), 
  3. (5, '王昭君', 23), 
  4. (8, '貂蟬', 25), 
  5. (10, '楊玉環(huán)', 26), 
  6. (12, '陳圓圓', 20); 

Record Lock

「對(duì)單個(gè)記錄加鎖」

如把id值為8的數(shù)據(jù)加一個(gè)Record Lock,示意圖如下

Record Lock也是有S鎖和X鎖之分的,兼容性和之前描述的一樣。

SQL執(zhí)行加什么樣的鎖受很多條件的制約,比如事務(wù)的隔離級(jí)別,執(zhí)行時(shí)使用的索引(如,聚集索引,非聚集索引等),因此就不詳細(xì)分析了,舉幾個(gè)簡(jiǎn)單的例子。

  1. -- READ UNCOMMITTED/READ COMMITTED/REPEATABLE READ 利用主鍵進(jìn)行等值查詢 
  2. -- 對(duì)id=8的記錄加S型Record Lock 
  3. select * from girl where id = 8 lock in share mode; 
  4.  
  5. -- READ UNCOMMITTED/READ COMMITTED/REPEATABLE READ 利用主鍵進(jìn)行等值查詢 
  6. -- 對(duì)id=8的記錄加X(jué)型Record Lock 
  7. select * from girl where id = 8 for update

Gap Lock

「鎖住記錄前面的間隙,不允許插入記錄」

「MySQL在可重復(fù)讀隔離級(jí)別下可以通過(guò)MVCC和加鎖來(lái)解決幻讀問(wèn)題」

當(dāng)前讀:加鎖

快照讀:MVCC

但是該如何加鎖呢?因?yàn)榈谝淮螆?zhí)行讀取操作的時(shí)候,這些幻影記錄并不存在,我們沒(méi)有辦法加Record Lock,此時(shí)可以通過(guò)加Gap Lock解決,即對(duì)間隙加鎖。

如一個(gè)事務(wù)對(duì)id=8的記錄加間隙鎖,則意味著不允許別的事務(wù)在id=8的記錄前面的間隙插入新記錄,即id值在(5, 8)這個(gè)區(qū)間內(nèi)的記錄是不允許立即插入的。直到加間隙鎖的事務(wù)提交后,id值在(5, 8)這個(gè)區(qū)間中的記錄才可以被提交

我們來(lái)看如下一個(gè)SQL的加鎖過(guò)程

  1. -- REPEATABLE READ 利用主鍵進(jìn)行等值查詢 
  2. -- 但是主鍵值并不存在 
  3. -- 對(duì)id=8的聚集索引記錄加Gap Lock 
  4. SELECT * FROM girl WHERE id = 7 LOCK IN SHARE MODE; 

由于id=7的記錄不存在,為了禁止幻讀現(xiàn)象(避免在同一事務(wù)下執(zhí)行相同的語(yǔ)句得到的結(jié)果集中有id=7的記錄),所以在當(dāng)前事務(wù)提交前我們要預(yù)防別的事務(wù)插入id=7的記錄,此時(shí)在id=8的記錄上加一個(gè)Gap Lock即可,即不允許別的事務(wù)插入id值在(5, 8)這個(gè)區(qū)間的新記錄

「給大家提一個(gè)問(wèn)題,Gap Lock只能鎖定記錄前面的間隙,那么最后一條記錄后面的間隙該怎么鎖定?」

其實(shí)mysql數(shù)據(jù)是存在頁(yè)中的,每個(gè)頁(yè)有2個(gè)偽記錄

  • Infimum記錄,表示該頁(yè)面中最小的記錄
  • upremum記錄,表示該頁(yè)面中最大的記錄

為了防止其它事務(wù)插入id值在(12, +∞)這個(gè)區(qū)間的記錄,我們可以給id=12記錄所在頁(yè)面的Supremum記錄加上一個(gè)gap鎖,此時(shí)就可以阻止其他事務(wù)插入id值在(12, +∞)這個(gè)區(qū)間的新記錄

Next-key Lock

「同時(shí)鎖住數(shù)據(jù)和數(shù)據(jù)前面的間隙,即數(shù)據(jù)和數(shù)據(jù)前面的間隙都不允許插入記錄」所以你可以這樣理解Next-key Lock=Record Lock+Gap Lock

  1. -- REPEATABLE READ 利用主鍵進(jìn)行范圍查詢 
  2. -- 對(duì)id=8的聚集索引記錄加S型Record Lock 
  3. -- 對(duì)id>8的所有聚集索引記錄加S型Next-key Lock(包括Supremum偽記錄) 
  4. SELECT * FROM girl WHERE id >= 8 LOCK IN SHARE MODE; 

因?yàn)橐鉀Q幻讀的問(wèn)題,所以需要禁別的事務(wù)插入id>=8的記錄,所以

對(duì)id=8的聚集索引記錄加S型Record Lock

對(duì)id>8的所有聚集索引記錄加S型Next-key Lock(包括Supremum偽記錄)

表級(jí)鎖

「表鎖也有S鎖和X鎖之分」

在對(duì)某個(gè)表執(zhí)行select,insert,update,delete語(yǔ)句時(shí),innodb存儲(chǔ)引擎是不會(huì)為這個(gè)表添加表級(jí)別的S鎖或者X鎖。

在對(duì)表執(zhí)行一些諸如ALTER TABLE,DROP TABLE這類的DDL語(yǔ)句時(shí),會(huì)對(duì)這個(gè)表加X(jué)鎖,因此其他事務(wù)對(duì)這個(gè)表執(zhí)行諸如SELECT INSERT UPDATE DELETE的語(yǔ)句會(huì)發(fā)生阻塞

在系統(tǒng)變量autocommit=0,innodb_table_locks = 1時(shí),手動(dòng)獲取InnoDB存儲(chǔ)引擎提供的表t的S鎖或者X鎖,可以這么寫

對(duì)表t加表級(jí)別的S鎖

  1. lock tables t read 

對(duì)表t加表級(jí)別的X鎖

  1. lock tables t write 

「如果一個(gè)事務(wù)給表加了S鎖,那么」

  • 別的事務(wù)可以繼續(xù)獲得該表的S鎖
  • 別的事務(wù)可以繼續(xù)獲得表中某些記錄的S鎖
  • 別的事務(wù)不可以繼續(xù)獲得該表的X鎖
  • 別的事務(wù)不可以繼續(xù)獲得表中某些記錄的X鎖

「如果一個(gè)事務(wù)給表加了X鎖,那么」

  • 別的事務(wù)不可以繼續(xù)獲得該表的S鎖
  • 別的事務(wù)不可以繼續(xù)獲得表中某些記錄的S鎖
  • 別的事務(wù)不可以繼續(xù)獲得該表的X鎖
  • 別的事務(wù)不可以繼續(xù)獲得表中某些記錄的X鎖

「所以修改線上的表時(shí)一定要小心,因?yàn)闀?huì)使大量事務(wù)阻塞」,目前有很多成熟的修改線上表的方法,不再贅述

隔離級(jí)別

讀未提交:每次讀取最新的記錄,沒(méi)有做特殊處理 串行化:事務(wù)串行執(zhí)行,不會(huì)產(chǎn)生并發(fā)

所以我們重點(diǎn)關(guān)注「讀已提交」和「可重復(fù)讀」的隔離實(shí)現(xiàn)!

「這兩種隔離級(jí)別是通過(guò)MVCC(多版本并發(fā)控制)來(lái)實(shí)現(xiàn)的,本質(zhì)就是MySQL通過(guò)undolog存儲(chǔ)了多個(gè)版本的歷史數(shù)據(jù),根據(jù)規(guī)則讀取某一歷史版本的數(shù)據(jù),這樣就可以在無(wú)鎖的情況下實(shí)現(xiàn)讀寫并行,提高數(shù)據(jù)庫(kù)性能」

「那么undolog是如何存儲(chǔ)修改前的記錄?」

「對(duì)于使用InnoDB存儲(chǔ)引擎的表來(lái)說(shuō),聚集索引記錄中都包含下面2個(gè)必要的隱藏列」

「trx_id」:一個(gè)事務(wù)每次對(duì)某條聚集索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把該事務(wù)的事務(wù)id賦值給trx_id隱藏列

「roll_pointer」:每次對(duì)某條聚集索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把舊的版本寫入undo日志中。這個(gè)隱藏列就相當(dāng)于一個(gè)指針,通過(guò)他找到該記錄修改前的信息

如果一個(gè)記錄的name從貂蟬被依次改為王昭君,西施,會(huì)有如下的記錄,多個(gè)記錄構(gòu)成了一個(gè)版本鏈

「為了判斷版本鏈中哪個(gè)版本對(duì)當(dāng)前事務(wù)是可見(jiàn)的,MySQL設(shè)計(jì)出了ReadView的概念」。4個(gè)重要的內(nèi)容如下

「m_ids」:在生成ReadView時(shí),當(dāng)前系統(tǒng)中活躍的事務(wù)id列表「min_trx_id」:在生成ReadView時(shí),當(dāng)前系統(tǒng)中活躍的最小的事務(wù)id,也就是m_ids中的最小值「max_trx_id」:在生成ReadView時(shí),系統(tǒng)應(yīng)該分配給下一個(gè)事務(wù)的事務(wù)id值「creator_trx_id」:生成該ReadView的事務(wù)的事務(wù)id

當(dāng)對(duì)表中的記錄進(jìn)行改動(dòng)時(shí),執(zhí)行insert,delete,update這些語(yǔ)句時(shí),才會(huì)為事務(wù)分配唯一的事務(wù)id,否則一個(gè)事務(wù)的事務(wù)id值默認(rèn)為0。

max_trx_id并不是m_ids中的最大值,事務(wù)id是遞增分配的。比如現(xiàn)在有事務(wù)id為1,2,3這三個(gè)事務(wù),之后事務(wù)id為3的事務(wù)提交了,當(dāng)有一個(gè)新的事務(wù)生成ReadView時(shí),m_ids的值就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4

執(zhí)行過(guò)程如下:

  1. 如果被訪問(wèn)版本的trx_id=creator_id,意味著當(dāng)前事務(wù)在訪問(wèn)它自己修改過(guò)的記錄,所以該版本可以被當(dāng)前事務(wù)訪問(wèn)
  2. 如果被訪問(wèn)版本的trx_id
  3. 被訪問(wèn)版本的trx_id>=max_trx_id,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView后才開(kāi)啟,該版本不可以被當(dāng)前事務(wù)訪問(wèn)
  4. 被訪問(wèn)版本的trx_id是否在m_ids列表中 4.1 是,創(chuàng)建ReadView時(shí),該版本還是活躍的,該版本不可以被訪問(wèn)。順著版本鏈找下一個(gè)版本的數(shù)據(jù),繼續(xù)執(zhí)行上面的步驟判斷可見(jiàn)性,如果最后一個(gè)版本還不可見(jiàn),意味著記錄對(duì)當(dāng)前事務(wù)完全不可見(jiàn) 4.2 否,創(chuàng)建ReadView時(shí),生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問(wèn)

「好了,我們知道了版本可見(jiàn)性的獲取規(guī)則,那么是怎么實(shí)現(xiàn)讀已提交和可重復(fù)讀的呢?」

其實(shí)很簡(jiǎn)單,就是生成ReadView的時(shí)機(jī)不同

舉個(gè)例子,先建立如下表

  1. CREATE TABLE `girl` ( 
  2.   `id` int(11) NOT NULL
  3.   `namevarchar(255), 
  4.   `age` int(11), 
  5.   PRIMARY KEY (`id`) 
  6. ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 

Read Committed

「Read Committed(讀已提交),每次讀取數(shù)據(jù)前都生成一個(gè)ReadView」

下面是3個(gè)事務(wù)執(zhí)行的過(guò)程,一行代表一個(gè)時(shí)間點(diǎn)

「先分析一下5這個(gè)時(shí)間點(diǎn)select的執(zhí)行過(guò)程」

  1. 系統(tǒng)中有兩個(gè)事務(wù)id分別為100,200的事務(wù)正在執(zhí)行
  2. 執(zhí)行select語(yǔ)句時(shí)生成一個(gè)ReadView,mids=[100,200],min_trx_id=100,max_trx_id=201,creator_trx_id=0(select這個(gè)事務(wù)沒(méi)有執(zhí)行更改操作,事務(wù)id默認(rèn)為0)
  3. 最新版本的name列為西施,該版本trx_id值為100,在mids列表中,不符合可見(jiàn)性要求,根據(jù)roll_pointer跳到下一個(gè)版本
  4. 下一個(gè)版本的name列王昭君,該版本的trx_id值為100,也在mids列表內(nèi),因此也不符合要求,繼續(xù)跳到下一個(gè)版本
  5. 下一個(gè)版本的name列為貂蟬,該版本的trx_id值為10,小于min_trx_id,因此最后返回的name值為貂蟬

「再分析一下8這個(gè)時(shí)間點(diǎn)select的執(zhí)行過(guò)程」

  1. 系統(tǒng)中有一個(gè)事務(wù)id為200的事務(wù)正在執(zhí)行(事務(wù)id為100的事務(wù)已經(jīng)提交)
  2. 執(zhí)行select語(yǔ)句時(shí)生成一個(gè)ReadView,mids=[200],min_trx_id=200,max_trx_id=201,creator_trx_id=0
  3. 最新版本的name列為楊玉環(huán),該版本trx_id值為200,在mids列表中,不符合可見(jiàn)性要求,根據(jù)roll_pointer跳到下一個(gè)版本
  4. 下一個(gè)版本的name列為西施,該版本的trx_id值為100,小于min_trx_id,因此最后返回的name值為西施

當(dāng)事務(wù)id為200的事務(wù)提交時(shí),查詢得到的name列為楊玉環(huán)。

Repeatable Read

「Repeatable Read(可重復(fù)讀),在第一次讀取數(shù)據(jù)時(shí)生成一個(gè)ReadView」圖片可重復(fù)讀因?yàn)橹辉诘谝淮巫x取數(shù)據(jù)的時(shí)候生成ReadView,所以每次讀到的是相同的版本,即name值一直為貂蟬,具體的過(guò)程上面已經(jīng)演示了兩遍了,我這里就不重復(fù)演示了,相信你一定會(huì)自己分析了。

本文轉(zhuǎn)載自微信公眾號(hào)「Java識(shí)堂」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系Java識(shí)堂公眾號(hào)。

 

責(zé)任編輯:武曉燕 來(lái)源: Java識(shí)堂
相關(guān)推薦

2020-10-13 10:32:24

MySQL事務(wù)MVCC

2021-07-26 10:28:13

MySQL事務(wù)隔離

2021-08-04 13:19:42

MySQL 事務(wù)隔離

2018-07-20 11:10:21

數(shù)據(jù)庫(kù)事務(wù)隔離性

2025-03-03 08:20:00

MySQL事務(wù)隔離數(shù)據(jù)庫(kù)

2023-09-22 08:27:39

2018-12-19 16:46:38

MySQL事務(wù)隔離數(shù)據(jù)庫(kù)

2024-12-30 13:58:14

2025-04-28 09:27:26

2025-01-13 13:12:54

2024-04-26 09:17:20

MySQL事務(wù)隔離

2024-05-10 09:31:22

務(wù)隔離級(jí)別Python

2021-06-02 16:30:33

PolarDB原子性數(shù)據(jù)庫(kù)

2021-09-17 12:50:10

MySQL數(shù)據(jù)庫(kù)ACID

2021-06-03 14:00:35

PolarDB

2024-12-02 08:37:04

2020-09-21 18:44:35

MySQL

2019-10-15 10:23:13

服務(wù)器MySQL 數(shù)據(jù)

2017-08-09 14:34:12

MysqlJavaPython

2024-05-24 09:29:28

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)