為什么需要事務(wù)呢圾结?
在數(shù)據(jù)庫(kù)(二),數(shù)據(jù)庫(kù)起源里面我們提到了事務(wù)晌姚。
數(shù)據(jù)庫(kù)除了對(duì)查詢等操作進(jìn)行了抽象舀凛,另外一個(gè)重要的功能就是事務(wù)了途蒋。為什么需要事務(wù)呢号坡?因?yàn)槲覀冊(cè)诓僮鲾?shù)據(jù)的時(shí)候梯醒,可能遇到多個(gè)線程同時(shí)操作數(shù)據(jù)的問(wèn)題茸习,也可能遇到突然數(shù)據(jù)庫(kù)故障了的問(wèn)題畜隶,這些都可能造成數(shù)據(jù)的不一致。所以事務(wù)要保證的就是一致性浸遗。
保證一致性的第一重意思是鎖箱亿,這是為了應(yīng)對(duì)多個(gè)連接同時(shí)連到數(shù)據(jù)庫(kù)的時(shí)候届惋。因?yàn)槲覀兛赡転槊總€(gè)連接分配一個(gè)線程,而這些線程有可能同時(shí)操作同一塊數(shù)據(jù)郑藏,這樣將會(huì)發(fā)生不一致译秦。所以我們只好在寫的時(shí)候加上鎖击碗,也就是強(qiáng)行保證只有一個(gè)線程可以訪問(wèn)到這塊數(shù)據(jù)稍途。
另外我們還會(huì)遇到數(shù)據(jù)庫(kù)崩潰的問(wèn)題械拍,所以我們要求一個(gè)事務(wù)一定是原子的,也就是 要么全部發(fā)生甲馋, 要么根本不發(fā)生定躏。比如Bob給Smith轉(zhuǎn)100塊芹敌,要么Bob有100塊氏捞,要么Smith有100塊,不存在中間狀態(tài)逞姿。
對(duì)于單機(jī)事務(wù)而言滞造,需要保證
- 原子性
- 一致性
- 隔離性
- 持久性
也就是所謂的ACID,下面我們依次介紹他們是怎么實(shí)現(xiàn)的猎贴。
原子性
Undo日志
所謂原子性指的是要么同時(shí)成功她渴,要么同時(shí)失敗趁耗。比如Bob賬戶里面有100塊疆虚,而Smith賬戶里面有0元径簿,現(xiàn)在我們希望Bob轉(zhuǎn)100塊給Smith。
所謂原子性就是要么Bob成功轉(zhuǎn)給了Smith100塊缠捌,此時(shí)Bob有0元曼月、Smith有100塊柔昼。要么失敗了捕透,Bob仍然有100塊激率,Smith為0元勿决。不會(huì)存在Bob把錢轉(zhuǎn)出去了低缩,而Smith卻沒(méi)有拿到錢的情況曹货。
現(xiàn)在我們來(lái)想想要實(shí)現(xiàn)這個(gè)事務(wù)顶籽,應(yīng)該怎么做
鎖定Bob賬戶
鎖定Smith賬戶
查看Bob是否有100塊錢礼饱,如果有究驴,則從賬號(hào)里面減少100塊
給Smith賬戶里面增加100塊
依次解鎖Bob和Smith
但是執(zhí)行事務(wù)不會(huì)永遠(yuǎn)是一帆風(fēng)順的洒忧,可能出現(xiàn)意外熙侍,比如Bob或者Smith賬戶不存在怎么辦蛉抓?沒(méi)關(guān)系,我們可以回滾到上一個(gè)狀態(tài)减余。
但是數(shù)據(jù)庫(kù)不可能把每個(gè)狀態(tài)都記錄下來(lái)位岔,這就需要我們?cè)谵D(zhuǎn)賬之前把之前的狀態(tài)記錄下來(lái)堡牡。
比如我們看剛剛那個(gè)轉(zhuǎn)賬操作的中間狀態(tài)
- Bob:100晤柄,Smith:0
- Bob:0芥颈,Smith :0 (此時(shí)正在轉(zhuǎn)賬)
- Bob : 0 , Smith :100(轉(zhuǎn)賬成功)
我們可以在插入兩個(gè)undo段纠屋,他們記錄在日志中售担。
- Bob:100,Smith:0
- Bob:0岩四,Smith :0 (此時(shí)正在轉(zhuǎn)賬)
- 上一個(gè)狀態(tài)為:Bob:100剖煌,Smith:0
- Bob : 0 逝淹, Smith :100(轉(zhuǎn)賬成功)
- 上一個(gè)狀態(tài)為: Bob:0创橄,Smith :0
這樣如果要回滾妥畏,只需要回溯日志即可實(shí)現(xiàn)。這
另外還有一種可能就是事務(wù)并沒(méi)有進(jìn)行完燃辖,系統(tǒng)就崩潰了怎么辦黔龟?那系統(tǒng)重啟之后就得做恢復(fù)操作啊氏身。那怎么恢復(fù)了惑畴,同樣也是通過(guò)日志如贷。我們可以在進(jìn)行真正的操作之前杠袱,需要把要做的事寫下來(lái),
我們會(huì)在事務(wù)開始之前寫下:
Bob原有100元凿掂,Smith原有0元
如果事務(wù)執(zhí)行到一半就斷電缠劝,那么重啟之后我們就可以按照日志來(lái)恢復(fù)惨恭,然后仍然是** Bob有100元耙旦,Smith有0元免都。即使恢復(fù)100次绕娘,仍然是這個(gè)結(jié)果,這就叫冪等性**侨舆,所以恢復(fù)過(guò)程中也斷電了挨下,我們?nèi)匀豢梢园凑杖罩緛?lái)進(jìn)行恢復(fù)臭笆。
現(xiàn)在還有個(gè)一問(wèn)題沒(méi)有解決秤掌,那就是怎么知道一個(gè)事務(wù)沒(méi)有完成呢闻鉴?
同樣我們可以通過(guò)記錄日志的方式來(lái)完成椒拗。比如我們?cè)谟涗浀臅r(shí)候蚀苛,不但把余額記上,還把事務(wù)開始了和結(jié)束這兩個(gè)動(dòng)作打上標(biāo)記腋舌。
比如
[開始事務(wù)T1]
[事務(wù)T1:Bob原有100]
[事務(wù)T2:Smith原有0]
[提交事務(wù)T1]
這樣块饺,如果在日志中看到了提交事務(wù)T1或者回滾事務(wù)T1授艰,我們就知道這個(gè)事務(wù)已經(jīng)結(jié)束了淮腾。如果只看到開始事務(wù)T1,那就得恢復(fù)洲押。比如下面這個(gè)就得恢復(fù)
[開始事務(wù)T1]
[事務(wù)T1:Bob原有100]
[事務(wù)T2:Smith原有0]
而且杈帐,在恢復(fù)之后挑童,需要在日志文件中加上一行回滾事務(wù)T1炮沐,這樣下次恢復(fù)就不用再考慮T1這個(gè)事務(wù)呢回怜,因?yàn)楝F(xiàn)在早已經(jīng)回到上一個(gè)狀態(tài)去了呢玉雾。
Undo日志寫入文件的時(shí)機(jī)
上面的討論其實(shí)我們都故意忽略了一個(gè)問(wèn)題复旬,那就是Undo日志也需要加載到內(nèi)存中才能讀寫驹碍,但是如果日志還沒(méi)寫好就斷電了怎么辦志秃?
其實(shí)我們只要掌握好把日志寫入文件的時(shí)機(jī)就OK了。
最容易想到的就是在一開始就把日志寫入文件竟坛,就好比寫作文前把草稿打好担汤,后面只管按著草稿謄抄一遍就可以了崭歧。
然而驾荣,現(xiàn)實(shí)是普泡,一開始的時(shí)候撼班,我們都不知道程序要操作哪個(gè)字段砰嘁,怎么記錄日志呢矮湘,當(dāng)然也不能寫入文件呢缅阳。所以肯定是一邊在內(nèi)存中操作Undo日志,一邊找時(shí)機(jī)寫入磁盤中秀撇。
比如上面的轉(zhuǎn)賬操作呵燕,我們其實(shí)可以這樣來(lái)修改和寫日志再扭。
操作 | 數(shù)據(jù)緩沖區(qū) | 日志緩沖區(qū) |
---|---|---|
開始事務(wù)T1 | [開始事務(wù)T1] | |
Bob = Bob - 100 | Bob新余額為0 | [事務(wù)T1泛范,Bob原有余額為100] |
把日志寫入文件 | 注意敦跌,日志寫入文件后柠傍,緩沖區(qū)會(huì)清空 | |
把Bob余額寫入文件 | ||
Smith = Smith + 100 | [事務(wù)T1惧笛,Smith原有余額0] | |
把日志寫入文件 | 注意患整,日志寫入文件后,緩沖區(qū)會(huì)清空 | |
把Smith余額寫入文件 | Smith新余額為100 | |
提交事務(wù)T1 | [提交事務(wù)T1] | |
把日志寫入文件 | 注意,日志寫入文件后赴穗,緩沖區(qū)會(huì)清空 |
總結(jié)一下就是般眉,
當(dāng)余額發(fā)生改變的時(shí)候潜支,記錄之前的余額
在余額要寫入硬盤之前冗酿,需要把日志先寫入文件已烤,然后日志緩沖區(qū)會(huì)清空胯究。
提交事務(wù)的日志一定是在所有余額都寫入硬盤之后才寫入
也就是說(shuō)事務(wù)過(guò)程中裕循,余額發(fā)生改變剥哑,在余額正式寫入了硬盤以后株婴,相當(dāng)于木已成舟暑认,所以我們也需要把日志寫入硬盤蘸际。
當(dāng)所有余額都穩(wěn)穩(wěn)當(dāng)當(dāng)?shù)穆涞酱疟P上了粮彤,我們自然也應(yīng)該把日志落到磁盤上
那么我們可以攻防演練一下导坟。
如果Bob的余額寫到了硬盤惫周,但是Smith還沒(méi)修改闯两。此時(shí)日志中落盤的只有Bob原有的余額也就是:
[開始事務(wù)T1]
[事務(wù)T1:Bob原有100]
恢復(fù)的時(shí)候,發(fā)現(xiàn)事務(wù)沒(méi)有結(jié)束饥臂,所以還會(huì)把Bob的余額給恢復(fù)了隅熙。
同理囚戚,如果Bob和Smith的余額都落盤了驰坊,但是沒(méi)有提交事務(wù)拳芙,此時(shí)日志是
[開始事務(wù)T1]
[事務(wù)T1:Bob原有100]
[事務(wù)T2:Smith原有0]
依然可以恢復(fù)兩個(gè)賬戶的余額舟扎。
即使兩個(gè)賬戶的最新余額都落盤了睹限,也提交了事務(wù)羡疗,但是只要在日志寫入磁盤之前崩潰顺囊,則Undo日志還是
[開始事務(wù)T1]
[事務(wù)T1:Bob原有100]
[事務(wù)T2:Smith原有0]
同樣會(huì)把余額恢復(fù)成原樣特碳。
原子性做不到的地方
現(xiàn)在可算是把原子性說(shuō)完了诚亚,但是只有原子性是不夠的,為什么呢午乓?因?yàn)樗鼰o(wú)法保證多個(gè)線程訪問(wèn)數(shù)據(jù)時(shí)的一致性站宗。
比如在第2步的時(shí)候,另一個(gè)事務(wù)把把smith賬戶加到了300塊錢益愈,
- Bob:100梢灭,Smith:0
- Bob:0,Smith :0 ------------->Bob:0敏释,Smith :300(另一個(gè)事務(wù)干的)
- 上一個(gè)狀態(tài)為:Bob:100,Smith:0
- Bob : 0 摸袁, Smith :100(轉(zhuǎn)賬成功)
- 上一個(gè)狀態(tài)為: Bob:0钥顽,Smith :0
如果有另一個(gè)事務(wù)在進(jìn)行到步驟2的時(shí)候把smith賬戶加到了300塊錢,此時(shí)如果回滾靠汁,會(huì)把smith改為0蜂大,那加上的300塊就丟失了。 那么我們還需要一致性蝶怔。
一致性
上一章我們提到了如果在事務(wù)中間奶浦,有另一個(gè)事務(wù)突然插手對(duì)數(shù)據(jù)進(jìn)行修改,則如果出現(xiàn)回退踢星,將會(huì)出現(xiàn)數(shù)據(jù)不一致的問(wèn)題澳叉。
那怎么解決這個(gè)問(wèn)題呢?如果我們一個(gè)事務(wù)對(duì)數(shù)據(jù)操作完了以后斩狱,另一個(gè)事務(wù)再進(jìn)入耳高,這樣就不會(huì)發(fā)生爭(zhēng)搶和數(shù)據(jù)不一致了。所以核心就在于加鎖所踊。
比如
Lock Bob , Smith
- Bob:100泌枪,Smith:0
- Bob:0,Smith :0 ------------->Bob:0秕岛,Smith :300(另一個(gè)事務(wù)干的)
- 上一個(gè)狀態(tài)為:Bob:100碌燕,Smith:0
- Bob : 0 误证, Smith :100(轉(zhuǎn)賬成功)
- 上一個(gè)狀態(tài)為: Bob:0,Smith :0
- unLock Bob and Smith
在事務(wù)的開始和結(jié)束分別進(jìn)行加鎖和解鎖修壕。這樣愈捅,其他的事務(wù)并不可知事務(wù)內(nèi)部的事情。只有在事務(wù)單元內(nèi)部完全成功了以后才對(duì)外可見慈鸠。
到現(xiàn)在我們“仿佛”已經(jīng)解決了并發(fā)蓝谨、一致兩個(gè)大問(wèn)題了,但是新的問(wèn)題也來(lái)了青团,加鎖以后譬巫,其他的事務(wù)無(wú)法對(duì)數(shù)據(jù)進(jìn)行訪問(wèn),那么系統(tǒng)的并發(fā)度是上不來(lái)的督笆,這就是下面的隔離性要解決的問(wèn)題芦昔。
隔離性
所謂隔離性,其實(shí)是以性能作為理由娃肿,在破壞一致性咕缎。何以見得?因?yàn)槿绻WC強(qiáng)一致性料扰,最好的方法就是不管讀寫凭豪,統(tǒng)統(tǒng)排隊(duì)進(jìn)行,這樣一定不會(huì)出現(xiàn)數(shù)據(jù)不一致的情況记罚。
然而此時(shí)就做不到高的并發(fā)墅诡,性能也就上不去。所以我們只要做一些妥協(xié)桐智,比如只加寫鎖,不加讀鎖烟馅。
我們首先需要看看说庭,兩個(gè)事務(wù)單元對(duì)同一個(gè)數(shù)據(jù),有哪幾種并發(fā)模式郑趁,然后定義不同的隔離級(jí)別刊驴,看每種隔離級(jí)別可以實(shí)現(xiàn)哪些并發(fā)模式。
4種可能
同樣我們以一個(gè)例子來(lái)說(shuō)明
現(xiàn)在 T1 :Bob要給Smith 100塊寡润,然后T2 : Smith要給Joe 100塊捆憎。
這就是兩個(gè)事務(wù),如下圖所示梭纹,為了保證一致性躲惰,Smith賬戶會(huì)被兩個(gè)事務(wù)單元鎖定。也就是兩個(gè)事務(wù)有共享數(shù)據(jù)变抽,Bob在給Smith轉(zhuǎn)錢的時(shí)候础拨,另一個(gè)事務(wù)無(wú)法對(duì)Smith賬戶進(jìn)行操作了氮块,并發(fā)就上不去。
此時(shí)兩個(gè)事務(wù)單元T1诡宗,T2之間只有讀寫并發(fā)滔蝉、寫讀并發(fā)、讀讀并發(fā)塔沃、寫寫并發(fā)4種可能蝠引。
-
寫寫并行
什么時(shí)候能寫寫并行,只有當(dāng)兩個(gè)事務(wù)的數(shù)據(jù)完全沒(méi)有重疊的情況下蛀柴,比如如下的情況立肘。
因?yàn)闆](méi)有共享數(shù)據(jù),所以完全可以寫寫并行名扛,也就是寫寫都不加鎖谅年。
-
讀讀并行
也就是讀操作不加鎖,這樣讀與讀可以并行操作肮韧,因?yàn)樽x不會(huì)修改數(shù)據(jù)融蹂,所以讀讀可以放心的并行,而不用擔(dān)心一致性的問(wèn)題弄企。
-
讀寫并行
也就是讀的時(shí)候超燃,可以并發(fā)寫。我們知道拘领,寫操作會(huì)修改數(shù)據(jù)意乓,但是寫是加鎖的,所以我們無(wú)法讀到寫未提交的結(jié)果约素。所以雖然兩次讀到的數(shù)據(jù)是不一樣的届良,不可重復(fù)讀,但是每次讀到的數(shù)據(jù)都是正確的圣猎,不存在不一致士葫。
-
寫讀并行
也就是寫的時(shí)候,還可以并發(fā)讀送悔。因?yàn)閿?shù)據(jù)是在不斷改變的慢显,很可能讀到中間的狀態(tài),如果系統(tǒng)在此時(shí)崩潰了欠啤,重啟的時(shí)候會(huì)恢復(fù)到修改前的值荚藻,此時(shí)自然會(huì)出現(xiàn)錯(cuò)亂。
那么我們是否無(wú)法實(shí)現(xiàn)寫讀并行了嗎洁段?并不是应狱,可以通過(guò)Copy on Write。具體怎么做呢眉撵?每次寫操作之前都把數(shù)據(jù)復(fù)制一份到log里面侦香,在log里面進(jìn)行修改落塑。其實(shí)就是把原來(lái)的數(shù)據(jù)復(fù)制一份,然后修改罐韩。這樣讀操作作用的就是原來(lái)的數(shù)據(jù)憾赁,而寫作用的是備份的數(shù)據(jù),互不干擾散吵。
這種方法又叫(MVCC龙考,Multi Version content control,多版本內(nèi)容控制)矾睦。那么多版本是什么意思晦款。
我們知道數(shù)據(jù)被復(fù)制出去了一份以后,可能會(huì)被修改多次枚冗,那么下一次讀應(yīng)該讀修改后哪個(gè)版本的數(shù)據(jù)呢缓溅?這個(gè)時(shí)候,我們可以在日志里面加上版本號(hào)赁温。比如說(shuō)坛怪,現(xiàn)在寫入的數(shù)據(jù)版本號(hào)是10,如果要讀取版本號(hào)為5的數(shù)據(jù)股囊,則可以往前一直找袜匿,直到找到對(duì)應(yīng)的位置。
所以如果讀發(fā)生在寫操作之后稚疹,讀的版本號(hào)一定要大于寫的版本號(hào)居灯。這樣就可以保證讀到想要的數(shù)據(jù)。
四種隔離級(jí)別
上面講了兩個(gè)事務(wù)單元針對(duì)一塊數(shù)據(jù)其實(shí)有4種并發(fā)的可能内狗,接下來(lái)我們繼續(xù)討論隔離級(jí)別怪嫌。不同的隔離級(jí)別可以實(shí)現(xiàn)讀寫并行、寫讀并行其屏、讀讀并行喇勋、寫寫并行的一種或者幾種。
-
串行化:
就是讀的時(shí)候不允許寫偎行,寫的時(shí)候不允許讀,這樣可以保證數(shù)據(jù)強(qiáng)一致贰拿,但是性能最低蛤袒。SQLite默認(rèn)采用這種方式。
-
可重復(fù)讀膨更,也就是只能實(shí)現(xiàn)讀讀并行妙真,讀寫、寫讀荚守、寫寫等不能實(shí)現(xiàn)珍德。
所以在兩個(gè)都是讀的時(shí)候练般,不加讀鎖,其他情況均需要加鎖锈候。
MySQL默認(rèn)是這種方式薄料。
-
讀已提交(Read Committed):
此時(shí)當(dāng)數(shù)據(jù)被加上讀鎖了以后,一個(gè)寫進(jìn)來(lái)泵琳,寫鎖替換掉讀鎖摄职,也就是可以將讀鎖升級(jí)為寫鎖。那么如果事務(wù)T1讀取了數(shù)據(jù)获列,然后事務(wù)T2把這個(gè)數(shù)據(jù)修改了谷市,因?yàn)槭聞?wù)T2也是加鎖的,所以它會(huì)提交击孩,那么事務(wù)T1再讀取這個(gè)數(shù)據(jù)時(shí)迫悠,原來(lái)的數(shù)據(jù)已經(jīng)發(fā)生變化了。這就是不可重復(fù)讀巩梢。
此時(shí)可以做到讀寫并行创泄、讀讀并行,做不了寫讀并行
Oracle , PostgreSQL, SQL Server都是使用的這種模式且改。
-
讀未提交:顧名思義验烧,就是可以讀到未提交的內(nèi)容
最低級(jí)別的隔離,此時(shí)只加上寫和讀是不加鎖的又跛。因?yàn)閿?shù)據(jù)是在不斷改變的碍拆,很可能讀到中間的狀態(tài),如果系統(tǒng)在此時(shí)崩潰了慨蓝,重啟的時(shí)候會(huì)恢復(fù)到修改前的值感混,此時(shí)自然會(huì)出現(xiàn)錯(cuò)亂。
要解決寫讀并行的問(wèn)題礼烈,可以使用上面說(shuō)過(guò)的Copy on write弧满,這種方法最大的好處在于可以保證寫讀并行,同時(shí)隔離級(jí)別還很高
持久性
現(xiàn)在我們來(lái)討論最后ACID的持久性此熬,也就是只要事務(wù)提交了庭呜,不管是崩潰還是出錯(cuò),數(shù)據(jù)一定要寫到磁盤上
那么數(shù)據(jù)什么情況下會(huì)丟失呢犀忱?
- 首先是磁盤損壞募谎。所以我們可以使用RAID冗余磁盤陣列來(lái)保證可靠性。詳見【大話存儲(chǔ)】學(xué)習(xí)筆記(4阴汇,5章)数冬,RAID
-
還有就是內(nèi)存如果掉電,里面的數(shù)據(jù)就必然丟失搀庶,持久性得不到保證拐纱。但是如果每一次提交操作完成以后铜异,都將內(nèi)存中的數(shù)據(jù)同步到硬盤上,則會(huì)造成頻繁寫硬盤秸架,性能將下降揍庄。所以持久性和延遲無(wú)法兼得
我們只要進(jìn)行折中,比如只要把數(shù)據(jù)提交到內(nèi)存咕宿,就立刻返回成功币绩,然后將一段時(shí)間的請(qǐng)求打包送到磁盤上。這樣就避免了每次提交都寫磁盤
參考
- 慕課網(wǎng)
- 如果有人問(wèn)你數(shù)據(jù)庫(kù)的原理府阀,叫他看這篇文章如果有人問(wèn)你數(shù)據(jù)庫(kù)的原理缆镣,叫他看這篇文章