本文已授權(quán)Java知音公眾號(hào)獨(dú)家發(fā)布
一丘薛、Mysql的四個(gè)隔離級(jí)別
預(yù)備工作:
- 先創(chuàng)建一個(gè)test數(shù)據(jù)庫及account表蛙酪,
create database test;
use test;
create table account(
id int not null,
balance float not null,
PRIMARY KEY ( id)
)
- 向account中插入兩條測(cè)試數(shù)據(jù)
INSERT INTO table(id,balance)
VALUES (1,1000);
INSERT INTO table(id,balance)
VALUES (2,1000);
開啟兩個(gè)控制臺(tái)窗口契沫,當(dāng)做兩個(gè)用戶(A和B)
1.1 READ UNCOMMITTED(未提交讀)
也即RU钱床,在READ UNCOMMITTED級(jí)別示罗,事務(wù)中的修改,即使沒有提交陨晶,對(duì)其他事務(wù)也都是可見的猬仁。事務(wù)可以讀取未提交的數(shù)據(jù),這也被稱為臟讀(Dirty Read)先誉。這個(gè)級(jí)別會(huì)導(dǎo)致很多問題湿刽,從性能上來說,READ UNCOMMITTED不會(huì)比其他的級(jí)別好太多褐耳,但卻缺乏其他級(jí)別的很多好處诈闺,除非真的有非常必要的理由,在實(shí)際應(yīng)用中一般很少使用铃芦。
A用戶操作如下:
set session transaction isolation level read uncommitted;
start transaction;
select * from account;
結(jié)果如下:
B用戶操作如下:
set session transaction isolation level read uncommitted;
start transaction;
update account set balance=balance+200 where id = 1;
隨后在A用戶終端中查詢數(shù)據(jù)买雾,結(jié)果如下:
可以看到B用戶并未提交事務(wù),但是A用戶卻能讀到未提交的數(shù)據(jù)杨帽,這就是臟讀。
1.2 READ COMMITTED(提交讀)
即RC嗤军,大多數(shù)數(shù)據(jù)庫系統(tǒng)的默認(rèn)隔離級(jí)別都是READ COMMTTED(但MySQL不是注盈,Mysql的默認(rèn)隔離級(jí)別是REPEATABLE READ)。READ COMMITTED滿足前面提到的隔離性的簡(jiǎn)單定義:一個(gè)事務(wù)開始時(shí)叙赚,只能”看見”已經(jīng)提交的事務(wù)所做的修改老客。換句話說僚饭,一個(gè)事務(wù)從開始直到提交之前,所做的任何修改對(duì)其他事務(wù)都是不可見的胧砰。這個(gè)級(jí)別有時(shí)候叫做不可重復(fù)讀(nonrepeatble read)鳍鸵,因?yàn)閮纱螆?zhí)行同樣的查詢,可能會(huì)得到不一樣的結(jié)果尉间。以例子說明:
我們將用戶B所在的會(huì)話當(dāng)前事務(wù)隔離級(jí)別設(shè)置為read commited偿乖。
set session transaction isolation level read committed;
在A所在的會(huì)話中執(zhí)行
update account set balance=balance-200 where id = 1;
在B用戶的會(huì)話中查詢:
select * from account;
結(jié)果如下:
發(fā)現(xiàn)數(shù)據(jù)沒有變,還是1000哲嘲,說明可以避免臟讀了贪薪。
接著A用戶會(huì)話中將事務(wù)提交:
commit;
再次在B中查詢,結(jié)果如下:
可以看到眠副,B用戶讀取到了A用戶提交的數(shù)據(jù)画切。這么做有什么問題么?那就是我們?cè)跁?huì)話B同一個(gè)事務(wù)中囱怕,讀取到兩次不同的結(jié)果霍弹。這就造成了不可重復(fù)讀,就是兩次讀取的結(jié)果不同娃弓。
1.3 REPEATABLE READ(可重復(fù)讀)
REPEATABLE READ解決了臟讀的問題典格。該隔離級(jí)別保證了在同一個(gè)事務(wù)中多次讀取同樣記錄結(jié)果是一致的。但是理論上忘闻,可重復(fù)讀隔離級(jí)別還是無法解決另外一個(gè)幻讀(Phantom Read)的問題钝计。所謂幻讀,指的是當(dāng)某個(gè)事務(wù)在讀取某個(gè)范圍內(nèi)的記錄時(shí)齐佳,另一個(gè)事務(wù)又在該范圍內(nèi)插入了新的記錄私恬,當(dāng)之前的事務(wù)再次讀取該范圍的記錄時(shí),會(huì)產(chǎn)生幻行(Phantom Row)炼吴。Mysql的RR是由“行排它鎖+MVCC”一起實(shí)現(xiàn)的本鸣。
我們將用戶B所在的會(huì)話當(dāng)前事務(wù)隔離級(jí)別設(shè)置為repeatable read。
set session transaction isolation level repeatable read;
start transaction;
接著在B中查詢數(shù)據(jù):
兩條硅蹦。
然后我們到A用戶會(huì)話中插入一條數(shù)據(jù):
insert into account(id,balance) value(3,1000);
在A中查看是否添加成功:
成功荣德,有三條數(shù)據(jù)。
回到用戶B會(huì)話中童芹,再次查詢:
發(fā)現(xiàn)沒有變還是兩條涮瞻。這時(shí),用戶B想插入一條id=3假褪,balance=1000的數(shù)據(jù):
insert into account(id,balance) value(3,1000);
會(huì)報(bào)錯(cuò):
說是主鍵重復(fù)了署咽,可是B用戶剛剛查詢并沒有id=3的記錄。這就是幻讀現(xiàn)象。
我的理解是:不可重復(fù)讀指的是update操作宁否,而幻讀指的是insert或delete操作窒升。
1.4 SERIALIZABLE(串行化)
SERIALIZABLE是最高的隔離級(jí)別。它通過強(qiáng)制事務(wù)串行執(zhí)行慕匠,避免了前面說的幻讀的問題饱须。簡(jiǎn)單來說,SERIALIZABLE會(huì)在讀取每一行數(shù)據(jù)都加鎖台谊,所以可能導(dǎo)致大量的超時(shí)和鎖爭(zhēng)用問題蓉媳。實(shí)際應(yīng)用中也很少用到這個(gè)隔離級(jí)別,只有在非常需要確保數(shù)據(jù)的一致性而且可以接受沒有并發(fā)的情況下青伤,才考慮采用該級(jí)別督怜。
二、MVCC
首先介紹一下幾個(gè)概念:
讀鎖:也叫共享鎖狠角、S鎖号杠,若事務(wù)T對(duì)數(shù)據(jù)對(duì)象A加上S鎖,則事務(wù)T可以讀A但不能修改A丰歌,其他事務(wù)只能再對(duì)A加S鎖姨蟋,而不能加X鎖,直到T釋放A上的S 鎖立帖。這保證了其他事務(wù)可以讀A眼溶,但在T釋放A上的S鎖之前不能對(duì)A做任何修改。
寫鎖:又稱排他鎖晓勇、X鎖堂飞。若事務(wù)T對(duì)數(shù)據(jù)對(duì)象A加上X鎖,事務(wù)T可以讀A也可以修改A绑咱,其他事務(wù)不能再對(duì)A加任何鎖绰筛,直到T釋放A上的鎖。這保證了其他事務(wù)在T釋放A上的鎖之前不能再讀取和修改A描融。
表鎖:操作對(duì)象是數(shù)據(jù)表铝噩。Mysql大多數(shù)鎖策略都支持,是系統(tǒng)開銷最低但并發(fā)性最低的一個(gè)鎖策略窿克。事務(wù)t對(duì)整個(gè)表加讀鎖骏庸,則其他事務(wù)可讀不可寫,若加寫鎖年叮,則其他事務(wù)增刪改都不行具被。
行級(jí)鎖:操作對(duì)象是數(shù)據(jù)表中的一行。是MVCC技術(shù)用的比較多的只损。行級(jí)鎖對(duì)系統(tǒng)開銷較大硬猫,但處理高并發(fā)較好。
MVCC使得大部分支持行鎖的事務(wù)引擎,不再單純的使用行鎖來進(jìn)行數(shù)據(jù)庫的并發(fā)控制啸蜜,取而代之的是把數(shù)據(jù)庫的行鎖與行的多個(gè)版本結(jié)合起來,只需要很小的開銷,就可以實(shí)現(xiàn)非鎖定讀辈挂,從而大大提高數(shù)據(jù)庫系統(tǒng)的并發(fā)性能衬横。
2.1 重要字段
Mysql Innodb中行記錄的存儲(chǔ)格式,除了最基本的行信息外终蒂,還會(huì)有一些額外的字段蜂林,這里主要介紹和MVCC有關(guān)的字段:DATA_TRX_ID和DATA_ROLL_PTR。
DATA_TRX_ID:用來標(biāo)識(shí)最近一次對(duì)本行記錄做修改(insert|update)的事務(wù)的標(biāo)識(shí)符, 即最后一次修改(insert|update)本行記錄的事務(wù)id拇泣。
DATA_ROLL_PTR:指寫入回滾段(rollback segment)的 undo log record (撤銷日志記錄記錄)噪叙。如果一行記錄被更新, 則 undo log record 包含 '重建該行記錄被更新之前內(nèi)容' 所必須的信息。
借圖舉例:出自<<唐成-2016PG大會(huì)-數(shù)據(jù)庫多版本實(shí)現(xiàn)內(nèi)幕.pdf>>
當(dāng)插入的是一條新數(shù)據(jù)時(shí)霉翔,記錄上對(duì)應(yīng)的回滾段指針為NULL
DB_TRX_ID記錄了行的創(chuàng)建的時(shí)間,刪除的時(shí)間在每個(gè)事件發(fā)生的時(shí)候睁蕾,每行存儲(chǔ)版本號(hào),而不是存儲(chǔ)事件實(shí)際發(fā)生的時(shí)間债朵。每次事物的開始這個(gè)版本號(hào)都會(huì)增加子眶。自記錄時(shí)間開始,每個(gè)事物都會(huì)保存記錄的系統(tǒng)版本號(hào)序芦。依照事物的版本來檢查每行的版本號(hào)臭杰。
- 在insert操作時(shí), “創(chuàng)建時(shí)間”=DB_TRX_ID谚中,這時(shí)渴杆,“刪除時(shí)間”是未定義的;
- 在update操作時(shí)宪塔,復(fù)制新增行的“創(chuàng)建時(shí)間”=DB_TRX_ID磁奖,刪除時(shí)間未定義,舊數(shù)據(jù)行“創(chuàng)建時(shí)間”不變蝌麸,刪除時(shí)間=該事務(wù)DB_TRX_ID点寥;
- 在delete操作時(shí),相應(yīng)數(shù)據(jù)行的“創(chuàng)建時(shí)間”不變来吩,刪除時(shí)間=該事務(wù)的DB_ROW_ID敢辩;
- 在select操作時(shí),對(duì)兩者都不修改弟疆,只讀相應(yīng)的數(shù)據(jù)戚长。
2.2 原理
InnoDB的MVCC,是通過在每行紀(jì)錄后面保存兩個(gè)隱藏的列來實(shí)現(xiàn)的怠苔。這兩個(gè)列同廉,一個(gè)保存了行的創(chuàng)建時(shí)間,一個(gè)保存了行的過期時(shí)間(或刪除時(shí)間),當(dāng)然存儲(chǔ)的并不是實(shí)際的時(shí)間值迫肖,而是系統(tǒng)版本號(hào)锅劝。每開始一個(gè)新的事務(wù),系統(tǒng)版本號(hào)都會(huì)自動(dòng)遞增蟆湖。事務(wù)開始時(shí)刻的系統(tǒng)版本號(hào)會(huì)作為事務(wù)的版本號(hào)故爵,用來和查詢到的每行紀(jì)錄的版本號(hào)進(jìn)行比較。在REPEATABLE READ隔離級(jí)別下隅津,MVCC具體的操作如下:
SELECT
InnoDB會(huì)根據(jù)以下兩個(gè)條件檢查每行紀(jì)錄:
- InnoDB只查找版本早于當(dāng)前事務(wù)版本的數(shù)據(jù)行诬垂,即,行的系統(tǒng)版本號(hào)小于或等于事務(wù)的系統(tǒng)版本號(hào)伦仍,這樣可以確保事務(wù)讀取的行结窘,要么是在事務(wù)開始前已經(jīng)存在的,要么是事務(wù)自身插入或者修改過的充蓝。
- 行的刪除版本隧枫,要么未定義,要么大于當(dāng)前事務(wù)版本號(hào)棺克。這樣可以確保事務(wù)讀取到的行悠垛,在事務(wù)開始之前未被刪除。
只有符合上述兩個(gè)條件的紀(jì)錄娜谊,才能作為查詢結(jié)果返回确买。
INSERT
- InnoDB為插入的每一行保存當(dāng)前系統(tǒng)版本號(hào)作為行版本號(hào)。
DELETE
- InnoDB為刪除的每一行保存當(dāng)前系統(tǒng)版本號(hào)作為行刪除標(biāo)識(shí)纱皆。
UPDATE
- InnoDB為插入一行新紀(jì)錄湾趾,保存當(dāng)前系統(tǒng)版本號(hào)作為行版本號(hào),同時(shí)派草,保存當(dāng)前系統(tǒng)版本號(hào)到原來的行作為行刪除標(biāo)識(shí)搀缠。
優(yōu)點(diǎn):
保存這兩個(gè)額外系統(tǒng)版本號(hào),使大多數(shù)讀操作都可以不用加鎖近迁。這樣設(shè)計(jì)使得讀數(shù)據(jù)操作很簡(jiǎn)單艺普,性能很好。
缺點(diǎn):
每行紀(jì)錄都需要額外的存儲(chǔ)空間鉴竭,需要做更多的行檢查工作歧譬,以及一些額外的維護(hù)工作。
讀到這里搏存,也許會(huì)有一個(gè)疑問瑰步,考慮如下執(zhí)行序列:
按照之前的Select規(guī)則,會(huì)話B 的事務(wù)是在 會(huì)話A的后面開啟的璧眠,那么B的事務(wù)版本號(hào)大于A的事務(wù)版本號(hào)缩焦。這樣在A中插入的數(shù)據(jù)在未提交的情況下读虏,B可以讀到A修改的數(shù)據(jù),這不就自相矛盾了么袁滥?其實(shí)不然盖桥,InnoDB每個(gè)事務(wù)在開始的時(shí)候,會(huì)將當(dāng)前系統(tǒng)中的活躍事務(wù)列表(trx_sys->trx_list)創(chuàng)建一個(gè)副本(read view)呻拌,然后一致性讀去比較記錄的tx id的時(shí)候葱轩,并不是根據(jù)當(dāng)前事務(wù)的tx id,而是根據(jù)read view最早一個(gè)事務(wù)的tx id(read view->up_limit_id)來做比較的藐握,這樣就能確保在事務(wù)B之前沒有提交的所有事務(wù)的變更,B事務(wù)都是看不到的垃喊。如下圖所示:
結(jié)束猾普。rm -rf / 跑
參考資料: 《唐成-2016PG大會(huì)-數(shù)據(jù)庫多版本實(shí)現(xiàn)內(nèi)幕.pdf》