PostgreSQL的特色之一是它的并發(fā)控制機(jī)制蹦狂,在維護(hù)一致性和完整性的同時(shí)驻售,盡量避免讀寫的堵塞。
對于傳統(tǒng)數(shù)據(jù)庫库北,為了維護(hù)一致性和完整性,避免一個(gè)事務(wù)看到其它并發(fā)事務(wù)更新而到會不一致的數(shù)據(jù)们陆,通常采用的是LOCK機(jī)制寒瓦。這樣付出的代價(jià)是,當(dāng)鎖請求無法被響應(yīng)時(shí)坪仇,待處理的請求必須進(jìn)入等候隊(duì)列杂腰,甚至等待超時(shí)不被處理。
MVCC通過避開傳統(tǒng)數(shù)據(jù)庫的LOCK機(jī)制椅文,最大限度的減少鎖競爭以允許合理的多用戶環(huán)境中的性能颈墅。
恰當(dāng)?shù)厥褂肕VCC總會提供比LOCK更好的性能。對于那些無法輕松接收MVCC行為的應(yīng)用雾袱,PostgreSQL也提供了表和行級別的LOCK機(jī)制。
PostgreSQL存儲結(jié)構(gòu)
PostgreSQL中官还,一個(gè)表對應(yīng)一個(gè)邏輯文件芹橡,一個(gè)表被分割成若干個(gè)物理段文件(relation segment),除最后一段外默認(rèn)大小40M望伦。
文件頁(磁盤塊)是物理段文件的基本儲存單位林说,也是內(nèi)存和磁盤交換的單位煎殷。文件頁大小限制了表元組的大小并影響磁盤操作效率,缺省大小8192字節(jié)腿箩,最大可設(shè)置為2^15字節(jié)(這是由磁盤塊索引是15位決定的)豪直。
一個(gè)文件頁空間被邏輯分割為三個(gè)部分:
- PageHeader:頁描述區(qū)
- 記載頁的使用情況, 如頁分布格式版本,元組數(shù)據(jù)空間和特殊空間的起始位置以及文件頁相關(guān)的事務(wù)日志記載點(diǎn)等信息
- Tuple Item space:元組數(shù)據(jù)空間
- 實(shí)際記錄元組數(shù)據(jù)的地方
- Special space:特殊空間
每個(gè)記錄的元組(Tuple)稱為一項(xiàng)珠移,每項(xiàng)由描述ID和元組數(shù)據(jù)構(gòu)成弓乙。
項(xiàng)描述ID描述了元組存儲位置,大小以及一些狀態(tài)標(biāo)識钧惧。
項(xiàng)描述ID和項(xiàng)數(shù)據(jù)分別在元組數(shù)據(jù)空間的兩頭往中間存放暇韧,最早的項(xiàng)存在最兩側(cè),越晚的數(shù)據(jù)越靠中間浓瞪。
元組的寫過程:先寫到文件頁的內(nèi)存緩沖區(qū)(Buffer)懈玻,再更新到磁盤中
- 從緩沖頁的元組數(shù)據(jù)存儲區(qū)分配空間
- 構(gòu)造元組描述ID,寫入低端處
- 把實(shí)際數(shù)據(jù)寫到高端處乾颁,并設(shè)置緩沖區(qū)的臟標(biāo)記
- 更新到磁盤
- 寫元組不會立即更新到磁盤涂乌,而是推遲到所在的緩沖區(qū)被替換(Replace)時(shí)進(jìn)行
- Replace時(shí),判斷緩沖區(qū)是否臟:
- 如果臟英岭,啟動實(shí)際磁盤IO進(jìn)行寫湾盒;
- 如果不臟,直接回收再用該Buffer巴席。
文件頁的寫過程:
- 先更新該文件頁的事務(wù)日志历涝,事務(wù)日志由頁頭部的頁描述符指出
- 把文件緩沖頁寫到指定磁盤塊
MVCC
MVCC(Multiversion Concurrency Control),多版本并發(fā)控制漾唉。
舉一個(gè)簡單的例子來理解它的機(jī)制
inset into T1(id,name) values (1,'zhangsan');
updata T1 set name = 'lisi' where id =1;
- 每個(gè)事務(wù)都會得到一個(gè)XID(稱為事務(wù)ID)荧库,當(dāng)一個(gè)新事務(wù)開始,遞增XID赵刑,然后把它賦予這個(gè)新事務(wù)分衫。
- 把一個(gè)元組(Tuple)稱作同一邏輯行的一個(gè)行版本,數(shù)據(jù)文件中存放同一邏輯行的多個(gè)行版本
- 每個(gè)行版本的頭部般此,記錄該行版本的創(chuàng)建和刪除的事務(wù)ID(分別稱為xmin和xmax)
- 每個(gè)事務(wù)的狀態(tài)(running, abort 或 commit)記錄在pg_clog文件中
- 運(yùn)用一定的規(guī)則蚪战,使每個(gè)事務(wù)只會看到一個(gè)特定的行版本(快照)
舉個(gè)例子,當(dāng)insert
一行記錄時(shí)铐懊,只有那些已提交的邀桑、并且xmin比當(dāng)前事務(wù)XID小(xmin<XID) 的行記錄 對當(dāng)前事務(wù)才是可見的科乎。
這意味著你可以創(chuàng)建一個(gè)新事務(wù)然后插入記錄壁畸,直到commit之前,這些記錄對其他事務(wù)永遠(yuǎn)都是不可見的;commit之后捏萍,其他后創(chuàng)建的新事務(wù)就可看到這行新記錄了(xmin<XID)太抓。
對于delete
和update
,機(jī)制也是類似的令杈,不同的是要用xmax值來判斷數(shù)據(jù)的可見性走敌。
隔離級別
SQL標(biāo)準(zhǔn)定義了四個(gè)級別的事務(wù)隔離。最嚴(yán)格的是可串行化逗噩,是通過標(biāo)準(zhǔn)定義掉丽,即保證并發(fā)執(zhí)行和順序執(zhí)行的結(jié)果相同;其他三個(gè)級別是通過現(xiàn)象定義的给赞。
隔離級別 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
---|---|---|---|
讀未提交(read uncommitted) | |||
讀已提交(read committed) | 避免 | ||
可重復(fù)讀(repeatable read) | 避免 | 避免 | |
可串行化(serializable) | 避免 | 避免 | 避免 |
- 幻讀:重新執(zhí)行一個(gè)查詢机打,由于最近另一個(gè)事務(wù)的提交,返回的結(jié)果(一批數(shù)據(jù))和剛才不同片迅;
- 不可重復(fù)讀: 針對同一個(gè)數(shù)據(jù)残邀,一個(gè)事務(wù)內(nèi)多次查詢,由于期間另一個(gè)事務(wù)的提交柑蛇,導(dǎo)致結(jié)果(同一個(gè)數(shù)據(jù))不同芥挣;
- 臟讀:一個(gè)事務(wù)讀取了另一個(gè)事務(wù)還未提交的改動。
PostgreSQL中:
- 默認(rèn)隔離級別是讀已提交(read committed)耻台;
- 可以請求四中級別的任意一種空免,但對于內(nèi)部其實(shí)只有讀已提交、可重復(fù)讀盆耽、可串行化三種級別蹋砚;
- 選擇讀未提交時(shí),實(shí)際上用的是讀已提交摄杂;
- 選擇重復(fù)讀時(shí)坝咐,不會發(fā)生幻讀;
SQL標(biāo)準(zhǔn)只定義了那種現(xiàn)象不能發(fā)生析恢,但是沒定義哪種現(xiàn)象一定發(fā)生墨坚。
- 讀已提交
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
- PostgreSQL里的缺省隔離級別
- 看到的是當(dāng)前查詢開始時(shí)的快照
- 當(dāng)有兩個(gè)事務(wù)同時(shí)修改同一行數(shù)據(jù)時(shí),后發(fā)生的事務(wù)在初始事務(wù)提交前就可以進(jìn)行查找映挂,然后不執(zhí)行進(jìn)入等待泽篮,待初始事務(wù)提交后retry,檢驗(yàn)查找條件是否仍然滿足柑船,如果滿足帽撑,后續(xù)操作才會被執(zhí)行;
- 例如下圖中的例子:
在事務(wù)1提交之前鞍时,通常LOCK機(jī)制會讓事務(wù)2進(jìn)入等待油狂,到事務(wù)1提交后才可以掃描查找;
而MVCC允許事務(wù)2在事務(wù)1提交之前就可以進(jìn)行掃描查找工作,當(dāng)事務(wù)1提交之后专筷,事務(wù)2retry,檢驗(yàn)where條件是否仍然滿足蒸苇;
若不滿足磷蛹,則不會執(zhí)行任何操作(保證了一致性);
若滿足溪烤,則相比LOCK機(jī)制節(jié)省了掃描查找所消耗的時(shí)間(在LOCK機(jī)制等待commit時(shí)MVCC就開始掃描查找了)味咳。
- 可重復(fù)讀
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
- 看到的是當(dāng)前事務(wù)開始時(shí)的快照
- 使用這個(gè)級別需要準(zhǔn)備好重試事務(wù),因?yàn)榇谢赡苁?/li>
- 在第二張圖的例子中檬嘀,按照一般的LOCK機(jī)制槽驶,在可重復(fù)讀的級別下,在事務(wù)B提交后鸳兽,查詢結(jié)果應(yīng)該不同(即幻讀)掂铐,但是在MVCC機(jī)制中,查詢結(jié)果是相同的(幻讀也被避免了)
- 可串行化
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
- 是嚴(yán)格意義上的可串行化
- 可重復(fù)讀級別已經(jīng)避免了幻讀揍异,達(dá)到了SQL標(biāo)準(zhǔn)約定的“可串行化標(biāo)準(zhǔn)”全陨,但能避免幻讀并不等于嚴(yán)格意義上的可串行化
- 在這種策略下,不同事務(wù)同時(shí)修改同一數(shù)據(jù)的行為會直接失敗衷掷,并返回錯(cuò)誤信息
- 需要準(zhǔn)備好重啟事務(wù)
MVCC實(shí)現(xiàn)方法
MVCC的實(shí)現(xiàn)方法有兩種:
- 寫新數(shù)據(jù)是辱姨,把舊數(shù)據(jù)移到一個(gè)專門的地方(如回滾段),其他人讀數(shù)據(jù)時(shí)戚嗅,從回滾段中把舊數(shù)據(jù)讀出來
- 寫數(shù)據(jù)時(shí)雨涛,舊數(shù)據(jù)不刪除,把新數(shù)據(jù)插入
PostgreSQL使用的是第二種方法懦胞,Oracle數(shù)據(jù)庫和MySQL innodb引擎使用一種
比較:
- 優(yōu)點(diǎn):
- 回滾可以立刻完成替久,無論進(jìn)行了多少操作
- 數(shù)據(jù)可以進(jìn)行逆很多更新,不必?fù)?dān)心需要保證回滾段不被用完
- 缺點(diǎn):
- 舊版本數(shù)據(jù)需要清理
- 舊版本數(shù)據(jù)過多導(dǎo)致查詢變慢
存在的問題及解決方法
MVCC實(shí)現(xiàn)了一種期待:讀永遠(yuǎn)不堵塞寫医瘫。但是也帶來了一些問題:
- 因?yàn)椴煌氖聞?wù)會看到不同版本的記錄侣肄,所以PostgreSQL連那些可能過期的數(shù)據(jù)也要保留著;
當(dāng)UPDATA
時(shí)醇份,真正地創(chuàng)建了一行新記錄稼锅,而DELETE
時(shí),并不會真正地刪除一行舊記錄僚纷;
最終數(shù)據(jù)庫中會存在一些對有事務(wù)永遠(yuǎn)不可見的記錄矩距,稱作dead rows。 - 事務(wù)ID只能增加怖竭,它是個(gè)32bit锥债,支持大約40億個(gè)事務(wù),達(dá)到最大值會從0重新開始;
這樣帶來一個(gè)邏輯問題:突然所有記錄都變成了發(fā)生在將來的事務(wù)所產(chǎn)生的哮肚,而所有新事物也都沒有辦法訪問這些舊記錄了登夫。
- 解決方法:
VACUUM
PostgreSQL自帶了auto_vacuum守護(hù)進(jìn)程會在一個(gè)可配置的周期內(nèi)自動執(zhí)行清理,解決了這兩個(gè)問題允趟;
使用者需要留意這個(gè)auto_vacuum恼策,以免發(fā)生不想要的結(jié)果;
vacuum命令也可以手動執(zhí)行潮剪。