mysql鎖以及事務(wù)隔離級別


事務(wù)隔離級別

隔離級別 臟讀 不可重復(fù)讀 幻讀
READ-UNCOMMITTED
READ-COMMITTED
REPEATABLE-READ
SERIALIZABLE

幻讀:事務(wù)在插入已經(jīng)檢查過不存在的記錄時漂辐,驚奇的發(fā)現(xiàn)這些數(shù)據(jù)已經(jīng)存在了嗜逻,之前檢測獲取到的數(shù)據(jù)如同鬼影一般。

不可重復(fù)讀:同樣的條件灾挨,你讀取過的數(shù)據(jù)讼溺,再次讀取出來發(fā)現(xiàn)值不一樣了吉懊。

mysql 在RR級別下拖吼,通過MVCC解決了部分幻讀問題。

準(zhǔn)備
drop table if exists city;
create table city(
    id int(11) primary key,
    name varchar(200)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;


一些命令
-- 獲取當(dāng)前的隔離級別
select @@tx_isolation;


SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET AUTOCOMMIT=0;
-- 設(shè)置鎖超時時間
set innodb_lock_wait_timeout=100;
set global innodb_lock_wait_timeout=100;


READ-UNCOMMITTED級別下

序號 T1 T2
1 BEGIN BEGIN
2 insert into city values (1,'test'); --
3 -- select * from city; >> (1,'test')
4 ROLLBACK --
5 -- select * from cityl >> empty;
6 -- COMMIT;

從上面執(zhí)行過程看以看到琳水,T1事務(wù)肆糕,在執(zhí)行2操作之后,還沒提交在孝,T2去讀的時候诚啃,就讀到了數(shù)據(jù),當(dāng)T1 回滾之后私沮,T2再次讀取始赎,這條數(shù)據(jù)就不見了。

這就是在RU級別下,產(chǎn)生的讀取到別人未提交的數(shù)據(jù)從而產(chǎn)生的臟讀极阅。

READ-COMMITTED級別下

幻讀

序號 T1 T2
1 BEGIN BEGIN
2 insert into city values (1,"test"); --
3 -- select * from city;>> empty
4 COMMIT --
5 -- select * from city;>> (1,test)

T1和T2同時開啟事務(wù)胃碾,且這個時候city表為空。

在第2步筋搏,T1插入一條數(shù)據(jù)仆百,未提交。T2這個時候讀取數(shù)據(jù)是讀取不到的(RU級別的區(qū)別)奔脐。
但是在T1 COMMIT之后俄周,T2這個時候查詢的時候就跟之前不一樣,這就是讀到了別人已經(jīng)提交過的數(shù)據(jù)髓迎。

不可重復(fù)讀

序號 T1 T2
1 BEGIN --
2 insert into city values (1,"test"); --
3 COMMIT --
4 BEGIN BEGIN
5 select * from city where id = 1; >> (1,"test")
6 update city set name='name' where id= 1
7 select * from city where id=1; >> (1,"name")
8 commit commit

在第7行峦朗,T2讀取到了T1未提交的數(shù)據(jù),5,7兩次讀取到的結(jié)果不一致排龄,產(chǎn)生了不可重復(fù)讀波势。


REPEATABLE READ 級別下

解決不可重復(fù)讀

T1 T2
BEGIN --
insert into city values (1,'test'); --
commit --
BEGIN BEGIN
-- select * from city where id = 1; >> (1,'test');
update city set name='name' where id= 1 --
-- select * from city where id=1; >> (1,'test');
commit --
-- select * from city where id=1; >> (1,'test');
-- commit;
-- select * from city where id=1;>> (1,'name');

無論T1是否提交,T2讀到的數(shù)據(jù)始終是開啟事務(wù)時查詢到的數(shù)據(jù)

解決一般性幻讀

T1 T2
BEGIN BEGIN
-- select * from city; >> empty;
insert into city values(1,"test"); --
-- select * from city; >> empty;
commit --
-- select * from city; >> empty;
-- commit;

可以看到橄维,無論T1是否提交數(shù)據(jù)尺铣,在T2事務(wù)中總是看不到最新的數(shù)據(jù),這樣就解決了幻讀争舞。
MYSQL通過MVCC解決了一般性幻讀和可重復(fù)讀凛忿;

InnoDB存儲引擎MVCC的實(shí)現(xiàn)策略

在每一行數(shù)據(jù)中額外保存兩個隱藏的列:當(dāng)前行創(chuàng)建時的版本號和刪除時的版本號(可能為空,其實(shí)還有一列稱為回滾指針竞川,用于事務(wù)回滾)店溢。這里的版本號并不是實(shí)際的時間值,而是系統(tǒng)版本號委乌。每開始新的事務(wù)床牧,系統(tǒng)版本號都會自動遞增。事務(wù)開始時刻的系統(tǒng)版本號作為事務(wù)的版本號福澡,用來和查詢每行記錄的版本號進(jìn)行比較叠赦。

MVCC下InnoDB的增刪改查是如何工作的

  • 插入數(shù)據(jù)(insert):記錄的版本號即當(dāng)前事務(wù)的版本號驹马。
    執(zhí)行一條插入的SQL語句,insert into test values(1,"test");
    假設(shè)事務(wù)ID為1革砸,那么插入后的數(shù)據(jù)行如下:
id name create version delete version
1 test 1 --
  • 在更新操作的時候,采用的是先標(biāo)記舊的那行記錄為已刪除糯累,并且刪除版本號是事務(wù)版本號算利,然后插入一行新的記錄的方式。

比如執(zhí)行更新語句:update test set name="new_test" where id=1;
事務(wù)ID=2

id name create version delete version
1 test 1 2
1 new_test 2 --
  • 刪除操作的時候泳姐,就把事務(wù)版本號作為刪除版本號效拭。
    比如:delete from test where id=1;
    事務(wù)ID=3
id name create version delete version
1 new_value 2 3
  • 查詢操作:

從上面的描述可看到,在查詢時要符合以下兩個條件的記錄才能被事務(wù)查詢出來:

  1. 刪除版本號未指定或者大于當(dāng)前事務(wù)版本號,即查詢事務(wù)開啟后確保讀取的行未被刪除缎患。(即上述事務(wù)ID=2的事務(wù)查詢時慕的,依然能讀取到事務(wù)ID=3所刪除的數(shù)據(jù)行)

  2. 創(chuàng)建版本號小于或者等于當(dāng)前事務(wù)版本號,也就是說記錄創(chuàng)建是在當(dāng)前事務(wù)中(等于的情況)或者在當(dāng)前事務(wù)啟動之前的其他事務(wù)進(jìn)行的insert

仍然存在幻讀

T1 T2
BEGIN BEGIN
-- select * from city where id=1;
insert into city values(1,"test"); --
commit; -
-- insert into city values(1,"test"); >>ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY'

T2明明沒有查詢到數(shù)據(jù)挤渔,但是插入的時候肮街,卻提示已經(jīng)存在了數(shù)據(jù),就像見鬼了一樣判导。

是什么造成了這樣一個問題呢嫉父?

在MVCC中,讀操作分為兩種:一種是快照讀眼刃,另外一種是當(dāng)前讀绕辖。

  • 快照讀即按照版本號讀取
  • 當(dāng)前讀則是讀取最新的數(shù)據(jù),操作:insert/update/delete都屬于當(dāng)前讀擂红,需要加鎖仪际。
    • select * from table where ? lock in share mode;
    • select * from table where ? for update;
    • insert into table values(...);
    • update table set ? where ?;
    • delete from table where ?;
T1 T2
BEGIN BEGIN
-- inert into city values(10,"123"); //加鎖
select * from city where id=10 lock in share mode ;>>ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction --

delete ,update ,insert ,select .. for update 都是加的X鎖即排它鎖。
select .. lock in share mode 是共享鎖

insert into city values(11,"test");
T1 T2
select * from city where id=11 lock in share mode (success); --
-- select * from city where id=11 lock in share mode; (success)
-- select * from city where id=11 for update ;>> ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
-- update city set name ="ttt" where id=11; >> ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

通過next key lock 解決幻讀

Next Key Lock

InnoDB有三種行鎖算法:

  • Record Lock:單條記錄上的鎖
  • Gap Lock:間隙鎖昵骤,鎖定一個范圍弟头,但不包括記錄本身。
  • Next Key Lock:Record Lock + Gap Lock涉茧;鎖定一個范圍赴恨,并且鎖定記錄本身。

GAP鎖的目的是防止出現(xiàn)兩次當(dāng)前讀不一致的情況

drop table if exists person;
CREATE TABLE `person` (
  `id` int(11) NOT NULL,
  `age` int(11) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `age` (`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into person values(1,10,"t1");
insert into person values(2,20,"t2");
insert into person values(3,30,"t3");
T1 T2
BEGIN; BEGIN;
update person set name="test" where age=20; --
-- insert into person values(4,25,"test5") >> ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
-- insert into person values(5,30,"test6") >> success

可以發(fā)現(xiàn)在age索引范圍內(nèi)[10,30)

使用普通索引增加X鎖

 drop table if exists book;
 create table book(
    id int ,
    key id_x(id)
) engine=innodb;

insert into book values(1),(3),(5),(8),(11);
序號 T1 T2
1 BEGIN BEGIN
2 select * from book where id=8 for update ;>> (8,6) -
3 - insert into book values(5) ; >> ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
4 - insert into book values(11) >> success

如果查詢條件中沒有使用索引且加了X鎖伴栓,那么將會鎖住整張表伦连。

create table book(
    id int ,
    price int ,
    primary key(price)
) engine=innodb;

 insert into book values (1,9),(3,5),(5,8),(8,6),(11,2);

T1 T2
BEGIN; BEGIN;
select * from book where id=8 for update ;>> (8,6) /
/ insert into book values(11,22) >> ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

GAP的范圍

drop table if exists book;
create table book(
  id int,
  key id_x(id)
) engine=innodb;
insert into book values(8),(3);
序號 T1 T2
1 BEGIN BEGIN
2 select * from book where id=8 for update>>(8) select * from book >> (3)(8)
3 - insert into book values(7) >> timeout
4 - insert into book values(9) >> timeout
5 - insert into book values(3) >> timeout
6 - insert into book values(2) >> success

從3,4,5,6操作中可以看到,能夠插入的數(shù)據(jù)只有 id<3的情況
why ??
我們先看下 2操作之后產(chǎn)生間隙
(3,8] (8 ~ ∞]
所以钳垮,id>=8都無法插入惑淳;

但是為什么3也插入不了呢?

接下來看下面的操作:

create table book (id int ,seq int primary key ,key id_x(id)) engine=innodb;

insert into book values(8,10),(3,5);
序號 T1 T2
1 BEGIN BEGIN
2 select * from book ;>>(3,5),(8,10) -
3 select * from book where id=8 for update -
4 - insert into book values(3,6);>> timeout;
5 - insert into book values(3,4);>> success;
6 - insert into book values(9,11);>> timeout;

第一個表是沒有主鍵的饺窿,所以mysql會有一個rowid作為主鍵歧焦,是遞增的。而下面的操作是有主鍵的肚医。
對比上下兩個操作绢馍,我們可以得出一個結(jié)論:
假設(shè)間隙是: (3,8](8∞)
那么 可插入的值為: id<=3 and seq <5

接著繼續(xù)插入一條數(shù)據(jù):

insert into book values(11,2);
序號 T1 T2
1 BEGIN BEGIN
2 select * from book ;>>(3,5),(8,10),(11,2) -
3 select * from book where id=8 for update -
4 - insert into book values(3,6 ) >> timeout
5 - insert into book values(3,4)>> success
6 - insert into book values(11,1)>> timeout
7 - insert into book values(11,3) >> success

3操作之后,加鎖的間隙為:
(3,8],(8,11]
那么可插入的值為:
(id <=3 and seq<5 ) or (id >=11 and seq>2)

間隙是根據(jù)什么來的呢肠套?舰涌?
索引的結(jié)構(gòu)

在B+Tree索引的葉子節(jié)點(diǎn)是一個順序的列表,那么上述的結(jié)構(gòu)的鏈表如下所示:

(3,5) -> (8,10) -> (11,2)
從這個結(jié)構(gòu)來看你稚,如果現(xiàn)在有一條數(shù)據(jù)(11,0)插入瓷耙,結(jié)果產(chǎn)生的鏈表應(yīng)該為:
(3,5) -> (8,10) -> (11,0) -> (11,2)

所以朱躺,間隙就是根據(jù)鏈表結(jié)構(gòu)來的:
以上面鏈表來看,如果 執(zhí)行的是select * from book where id=8 for update ;

那么插入的范圍應(yīng)該是:
(id <=3 and seq<4) or (id>=11 and seq>0)


當(dāng)查詢的索引含有唯一屬性的時候搁痛,Next-Key Lock會進(jìn)行優(yōu)化长搀,將其降級為Record Lock,即僅鎖住索引本身鸡典,而不是范圍盈滴。

drop table if exists book;
create table book(
     id int primary key,
     seq int,
    unique key seq_x(seq)
)engine=innodb;
insert into book values(3,3),(5,5),(8,8),(11,11),(1,1);

降級為record鎖

序號 T1 T2
1 BEGIN; BEGIN;
2 select * from book where id = 8 for update -
3 - insert into book value(9,9) >> success

從操作3可以看到,2操作并沒有鎖定間隙轿钠。

注意: 通過主鍵或者唯一索引來鎖定不存在的值巢钓,也會產(chǎn)生GAP鎖定。

序號 T1 T2
1 BEGIN; BEGIN;
2 select * from book where id =9 for update -
3 - insert into book values(10,10) >> timeout

從操作3可以看到疗垛,操作2鎖定了一條不存在的數(shù)據(jù)症汹,仍然產(chǎn)生了間隙鎖。


幻讀的理解

首先贷腕,mysql 幻讀并非是“一個事務(wù)內(nèi)進(jìn)行兩次相同操作居然得到了不一樣的結(jié)果”背镇,因?yàn)樗静豢赡馨l(fā)生在使用了read view/MVCC的RR隔離級別下,這種幻讀的定義更適合給Oracle泽裳,Oracle的事務(wù)隔離級別只有兩級瞒斩,RC和Serializable。

這里給出mysql幻讀的比較形象的場景:
users:id 主鍵

1.T1:select * from users where id = 1;
2.T2:insert into `users`(`id`,`name`) values(1,'big cat');
3.T1:insert into `users`(`id`,`name`) values(1,'big cat');

T1:主事務(wù)涮总,檢測表中是否有id為1的記錄胸囱,沒有則插入,這是我們期望的正常業(yè)務(wù)邏輯瀑梗。
T2:干擾事務(wù)烹笔,目的在于擾亂T1的正常的事務(wù)執(zhí)行。
在RR隔離級別下抛丽,1,2是會正常執(zhí)行的谤职,3則會報錯主鍵沖突,對于T1的業(yè)務(wù)來說是執(zhí)行失敗的亿鲜,這里T1就是發(fā)生了幻讀允蜈,因?yàn)門1讀取的數(shù)據(jù)狀態(tài)并不能支持他的下一步的業(yè)務(wù),見鬼了一樣蒿柳。
在Serializable隔離級別下饶套,1執(zhí)行時是會隱式的添加gap共享鎖的,從而2會被阻塞其馏,3會正常執(zhí)行凤跑,對于T1來說業(yè)務(wù)是正確的,成功的扼殺了擾亂業(yè)務(wù)的T2叛复,對于T1來說他讀取的狀態(tài)是可以拿來支持業(yè)務(wù)的。
所以mysql的幻讀并非什么讀取兩次返回結(jié)果集不同,而是事務(wù)在插入事先檢測不存在的記錄時褐奥,驚奇的發(fā)現(xiàn)這些數(shù)據(jù)已經(jīng)存在了咖耘,之前的檢測讀獲取到的數(shù)據(jù)如同鬼影一般。
這里要靈活的理解讀取的意思撬码,第一次select是讀取儿倒,第二次的insert其實(shí)也屬于隱式的讀取,只不過是在mysql的機(jī)制中讀取的呜笑,插入數(shù)據(jù)也是要先讀取一下有沒有主鍵沖突才能決定是否執(zhí)行插入夫否。
不可重復(fù)讀側(cè)重表達(dá)讀-讀,幻讀則是說讀-寫叫胁,用寫來證實(shí)讀的是鬼影凰慈。

死鎖

T1 T2
BEGIN BEGIN
update city set name="test22"where id=11 --
-- update city set name="test33" where id=13
update city set name ="test33" where id=13 --
-- update city set name="test22" where id=11 >> ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

死鎖檢測回滾策略:保留回滾代價最小的事務(wù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末驼鹅,一起剝皮案震驚了整個濱河市微谓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌输钩,老刑警劉巖豺型,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異买乃,居然都是意外死亡姻氨,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門剪验,熙熙樓的掌柜王于貴愁眉苦臉地迎上來哼绑,“玉大人,你說我怎么就攤上這事碉咆《逗” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵疫铜,是天一觀的道長茂浮。 經(jīng)常有香客問我,道長壳咕,這世上最難降的妖魔是什么席揽? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮谓厘,結(jié)果婚禮上幌羞,老公的妹妹穿的比我還像新娘。我一直安慰自己竟稳,他們只是感情好属桦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布熊痴。 她就那樣靜靜地躺著,像睡著了一般聂宾。 火紅的嫁衣襯著肌膚如雪果善。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天系谐,我揣著相機(jī)與錄音巾陕,去河邊找鬼。 笑死纪他,一個胖子當(dāng)著我的面吹牛鄙煤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播茶袒,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼梯刚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了弹谁?” 一聲冷哼從身側(cè)響起乾巧,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎预愤,沒想到半個月后沟于,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡植康,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年旷太,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片销睁。...
    茶點(diǎn)故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡供璧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出冻记,到底是詐尸還是另有隱情睡毒,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布冗栗,位于F島的核電站演顾,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏隅居。R本人自食惡果不足惜钠至,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望胎源。 院中可真熱鬧棉钧,春花似錦、人聲如沸涕蚤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至愧捕,卻和暖如春奢驯,著一層夾襖步出監(jiān)牢的瞬間申钩,已是汗流浹背次绘。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留撒遣,地道東北人邮偎。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像义黎,于是被迫代替她去往敵國和親禾进。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評論 2 354