1. 什么是事務(wù)?
應(yīng)用在運(yùn)行時(shí)可能會(huì)發(fā)生數(shù)據(jù)庫(kù)、硬件的故障,應(yīng)用與數(shù)據(jù)庫(kù)的網(wǎng)絡(luò)連接斷開或多個(gè)客戶端端并發(fā)修改數(shù)據(jù)導(dǎo)致預(yù)期之外的數(shù)據(jù)覆蓋問題,為了提高應(yīng)用的可靠性和數(shù)據(jù)的一致性,事務(wù)應(yīng)運(yùn)而生。
從概念上講,事務(wù)是應(yīng)用程序?qū)⒍鄠€(gè)讀寫操作組合成一個(gè)邏輯單元的一種形式,這樣其中所有的讀寫操作都被視為單個(gè)操作來執(zhí)行,要么成功提交,要么失敗回滾,不存在任何部分成功和部分失敗的情況?,F(xiàn)在,幾乎所有的關(guān)系型數(shù)據(jù)庫(kù)和一些非關(guān)系型數(shù)據(jù)庫(kù)都支持事務(wù)。
1.1 ACID
事務(wù)通過ACID來保證安全的操作,它們分別是原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability)。ACID的提出旨在為數(shù)據(jù)庫(kù)容錯(cuò)機(jī)制建立精確的術(shù)語(yǔ),但是它在不同的數(shù)據(jù)庫(kù)中實(shí)現(xiàn)并不相同,我們來對(duì)其逐一的進(jìn)行解釋。
- 原子性
原子性定義的特征是:一個(gè)事務(wù)必須被視為一個(gè)不可分割的工作單元,整個(gè)事務(wù)中的所有操作要么全部提交成功,要么全部失敗回滾,不可能只執(zhí)行其中的一部分操作。
- 一致性
一致性在ACID中是“多余的”存在,它不同于原子性、隔離性和持久性,一致性是應(yīng)用程序的屬性,而其他三者是數(shù)據(jù)庫(kù)的屬性。應(yīng)用可能依賴原子性和隔離性來保證一致性,但有時(shí)候一致性的保證并不僅僅取決于數(shù)據(jù)庫(kù)。
一致性的體現(xiàn)依賴應(yīng)用程序?qū)?shù)據(jù)的約束,比如在會(huì)計(jì)系統(tǒng)中,所有賬戶的交易收支一定是平衡的。如果一個(gè)事務(wù)開始于一個(gè)平衡狀態(tài),那么在該事務(wù)執(zhí)行完成提交后,那么依然會(huì)保持平衡。從概念上來說,一致性是對(duì)數(shù)據(jù)的一組特定約束必須始終成立,這一點(diǎn)由應(yīng)用程序來保證,因?yàn)檫@些寫入和修改的邏輯都是由應(yīng)用程序決定的,數(shù)據(jù)庫(kù)只負(fù)責(zé)對(duì)這些操作進(jìn)行執(zhí)行。
- 隔離性
在實(shí)際工作中,大多數(shù)數(shù)據(jù)庫(kù)都同時(shí)被多個(gè)客戶端訪問,這就可能會(huì)發(fā)生客戶端并發(fā)修改同一條數(shù)據(jù)的情況,引發(fā)并發(fā)問題。如下圖中例子所示:
User1和User2要同時(shí)在數(shù)據(jù)庫(kù)中操作計(jì)數(shù)器增長(zhǎng),每個(gè)用戶都是先讀取值,執(zhí)行加1,然后寫入。理論上計(jì)數(shù)器的值最終應(yīng)該為44,但是由于并發(fā)問題,使得終值為43。
通常來說,隔離性讓一個(gè)事務(wù)所做的修改在最終提交以前,對(duì)其他事務(wù)不可見,它解決的是并發(fā)問題。如果一個(gè)事務(wù)進(jìn)行多次寫入,則另一個(gè)事務(wù)要么看到全部寫入結(jié)果,要么什么都看不到。所以在上述例子中,User2在修改計(jì)數(shù)器時(shí)讀取到的值應(yīng)該是User1修改完之后的結(jié)果43,之后執(zhí)行加1,使得最終結(jié)果為44。
- 持久性
持久性是一個(gè)承諾,即事務(wù)成功提交,即使發(fā)生硬件故障或者數(shù)據(jù)庫(kù)崩潰,寫入的任何數(shù)據(jù)都不會(huì)丟失,在單節(jié)點(diǎn)數(shù)據(jù)庫(kù)中,它通常意味著數(shù)據(jù)已經(jīng)被寫入硬盤或SSD;在多節(jié)點(diǎn)數(shù)據(jù)庫(kù)中,持久性可能意味著數(shù)據(jù)已經(jīng)成功復(fù)制到一些節(jié)點(diǎn)。但是,如果硬盤和備份被銷毀,那么顯然沒有任何數(shù)據(jù)庫(kù)能再找回這些數(shù)據(jù),所以完美的持久性并不存在。
2. 并發(fā)產(chǎn)生數(shù)據(jù)不一致的問題
往往由于事務(wù)之間的操作對(duì)象有競(jìng)爭(zhēng)關(guān)系,并且又因?yàn)椴l(fā)事務(wù)之間不確定的時(shí)序關(guān)系,會(huì)導(dǎo)致這些所操作的有競(jìng)爭(zhēng)關(guān)系的對(duì)象會(huì)出現(xiàn)各種奇怪的結(jié)果,下面我們就來看看這些常見的問題。
2.1 臟寫
兩個(gè)事務(wù)嘗試同時(shí)更新數(shù)據(jù)庫(kù)中相同的對(duì)象,如果先前的寫入是尚未提交事務(wù)的一部分,后面的寫入將一個(gè)尚未提交的值覆蓋掉了,這種情況被稱為臟寫。
2.2 臟讀
如果A事務(wù)已經(jīng)將一些數(shù)據(jù)寫入數(shù)據(jù)庫(kù),但是A事務(wù)還沒有提交或中止,現(xiàn)在開啟另一個(gè)B事務(wù)查詢,那么B事務(wù)能看到A事務(wù)中沒有提交的數(shù)據(jù),這就是臟讀。
我們考慮一種情況,一旦A事務(wù)發(fā)生回滾,B事務(wù)很有可能將未提交過的數(shù)據(jù)提交給數(shù)據(jù)庫(kù),因此造成的問題會(huì)讓人無從下手去排查。
2.3 不可重復(fù)讀
我們拿一個(gè)例子來說,Alice 在銀行有 1000 美元的儲(chǔ)蓄,分為兩個(gè)賬戶,每個(gè) 500 美元?,F(xiàn)在有一個(gè)事務(wù)從她的一個(gè)賬戶轉(zhuǎn)移了 100 美元到另一個(gè)賬戶。如果她在事務(wù)處理的過程中查看其賬戶余額,她可能在發(fā)出轉(zhuǎn)賬之后看到付款賬戶的余額為 400 美元,而收款賬戶的余額仍為 500 美元。對(duì) Alice 來說,現(xiàn)在她的賬戶看起來總共只有 900 美元,轉(zhuǎn)賬的 100 美元似乎憑空消失了,而再對(duì)收款賬戶進(jìn)行讀取時(shí),發(fā)現(xiàn)余額變成了 600 美元。
這種情況被稱為不可重復(fù)讀,又被稱為讀偏差,即對(duì)同一數(shù)據(jù)兩次讀取的結(jié)果不一致。
2.4 丟失更新
兩個(gè)事務(wù)同時(shí)執(zhí)行讀取-修改-寫入序列,其中一個(gè)寫操作在沒有合并另一個(gè)寫操作變更的情況下,直接覆蓋了另一個(gè)寫操作的結(jié)果,導(dǎo)致了數(shù)據(jù)的丟失,這種情況被稱為丟失更新。
比較直接的避免丟失更新的方法是不使用讀取-修改-寫入這一系列操作,而是進(jìn)行原子更新,以計(jì)數(shù)器為例,SQL如下。它的原理通常是獲取要讀取對(duì)象的排他鎖,使得事務(wù)在修改同一數(shù)據(jù)時(shí)依次執(zhí)行。
update counters set value = value + 1 where key = 1;
如果不能避免讀取-修改-寫入這一系列操作,那么可以通過顯式加鎖(FOR UPDATE)的方式來避免丟失更新,使得任何其他想要讀取同一對(duì)象的事務(wù)被阻塞,直到第一個(gè)獲取到該鎖的事務(wù)執(zhí)行完畢。
BEGIN TRANSACTION;
SELECT * FROM xxx FOR UPDATE;
-- 執(zhí)行業(yè)務(wù)邏輯
UPDATE xxx SET ...;
COMMIT;
比較并設(shè)置(CAS)是一種比較常見的樂觀的避免丟失更新的操作。當(dāng)對(duì)數(shù)據(jù)更新時(shí),會(huì)將數(shù)據(jù)表中的值和讀取值進(jìn)行對(duì)比,只有在沒有發(fā)生改變的情況下才允許更新,否則需要重試這個(gè)事務(wù)。一般在工作中會(huì)采用在數(shù)據(jù)表中添加時(shí)間戳列的方式來實(shí)現(xiàn)CAS。
2.5 幻讀和寫入偏差問題
幻讀用一句話來概括就是:一個(gè)事務(wù)的寫入改變了另一個(gè)事務(wù)的搜索查詢結(jié)果。
A事務(wù)的select
查詢出符合條件的數(shù)據(jù),并檢查是否符合業(yè)務(wù)要求,根據(jù)檢查結(jié)果決定業(yè)務(wù)是否繼續(xù)執(zhí)行。如果此時(shí)B事務(wù)對(duì)數(shù)據(jù)進(jìn)行修改,并符合A事務(wù)select
的查詢條件,那么A事務(wù)在執(zhí)行完寫入操作后,再次執(zhí)行select
查詢會(huì)發(fā)現(xiàn)不同的結(jié)果,這很可能會(huì)導(dǎo)致寫入偏差問題。
寫入偏差問題是兩個(gè)事務(wù)讀取相同的對(duì)象,然后更新其中一些對(duì)象時(shí)發(fā)生了預(yù)期之外的異常情況。它區(qū)別于臟寫和丟失更新,因?yàn)樗莾蓚€(gè)事務(wù)正在更新兩個(gè)不同的對(duì)象。如下面這個(gè)例子所示,Alice 和 Bob 是兩位值班醫(yī)生,兩人都感到不適,所以他們都決定請(qǐng)假。不幸的是,他們恰好在同一時(shí)間點(diǎn)擊按鈕下班:
這導(dǎo)致了沒有醫(yī)生值班,違反了至少有一名醫(yī)生值班的業(yè)務(wù)要求。
解決寫入偏差問題比較麻煩,因?yàn)樗婕岸鄠€(gè)對(duì)象,采用單對(duì)象原子操作的方法不能解決。通常情況下會(huì)采用更改隔離級(jí)別為可串行化或通過加鎖的方式來解決。
但是,加鎖的方式并不是在所有情況下都適用。比如,多人預(yù)定同一時(shí)段的會(huì)議室,因?yàn)樵摃r(shí)段的會(huì)議室預(yù)定記錄還沒有生成,導(dǎo)致多人讀取預(yù)定紀(jì)錄時(shí)都沒有讀到對(duì)應(yīng)的結(jié)果值,所以就無從加鎖,那么此時(shí)將會(huì)造成多人預(yù)定同一時(shí)段會(huì)議室的結(jié)果。為了解決這種情況,可以再創(chuàng)建一張數(shù)據(jù)表管理會(huì)議室的時(shí)間段,當(dāng)有人想預(yù)定某時(shí)段會(huì)議室時(shí),會(huì)將該時(shí)段的數(shù)據(jù)進(jìn)行加鎖,那么這時(shí)再有其他用戶來查詢時(shí),將會(huì)被阻塞,這種方法被稱為物化沖突。
3. 隔離級(jí)別
數(shù)據(jù)庫(kù)一直試圖通過事務(wù)隔離解決并發(fā)問題。可串行化隔離級(jí)別能保證事務(wù)串行執(zhí)行,這意味著不會(huì)發(fā)生并發(fā)問題。但是在實(shí)際生產(chǎn)中為了保證系統(tǒng)的性能,往往不會(huì)采用該隔離級(jí)別,而是會(huì)采用一些較弱的隔離級(jí)別,它們可能在某些情況下不能保證數(shù)據(jù)的一致性,但是能夠讓系統(tǒng)的性能更好。下面我們對(duì)這些隔離級(jí)別進(jìn)行介紹:
3.1 讀未提交
該隔離級(jí)別相對(duì)更弱,只能避免臟寫。
3.2 讀已提交(Read Committed)
這種隔離級(jí)別非常流行,它能夠避免臟讀和臟寫。
最常見的情況是使用行鎖來防止臟寫:當(dāng)事務(wù)想要修改同一個(gè)對(duì)象時(shí),則必須等到第一個(gè)事務(wù)提交或回滾后才能獲取該行的鎖繼續(xù)。
臟讀也可以通過加讀鎖來避免,但是這種方式會(huì)導(dǎo)致在有長(zhǎng)時(shí)間的寫入事務(wù)持有要讀數(shù)據(jù)的鎖時(shí),讀請(qǐng)求被阻塞,所以這種方式在實(shí)踐中的效果并不好。另一種避免方式是數(shù)據(jù)庫(kù)將已經(jīng)寫入的舊值記住,即使發(fā)生新的寫入事務(wù)且并沒有執(zhí)行完時(shí),讀請(qǐng)求讀取到的都是這個(gè)舊值,只有當(dāng)該寫事務(wù)提交時(shí)才能讀取到新值。
3.3 可重復(fù)讀
可重復(fù)讀能夠避免臟寫、臟讀、不可重復(fù)讀和只讀查詢中的幻讀,快照隔離是實(shí)現(xiàn)可重復(fù)讀的常見解決方案。每個(gè)事務(wù)都從數(shù)據(jù)庫(kù)的一致性快照中進(jìn)行讀取,那么這也就意味著該事務(wù)能看到事務(wù)開始時(shí)在數(shù)據(jù)庫(kù)中提交的所有數(shù)據(jù)。即使這些數(shù)據(jù)隨后被新的事務(wù)更改,該事務(wù)還仍然讀取的是在事務(wù)開始時(shí)的舊數(shù)據(jù)。這種辦法對(duì)長(zhǎng)時(shí)間運(yùn)行的只讀查詢非常有用,因?yàn)槿绻诓樵冞^程中數(shù)據(jù)不斷的變化,那么沒有辦法對(duì)數(shù)據(jù)進(jìn)行分析。
不提供快照隔離的讀已提交不能實(shí)現(xiàn)可重復(fù)讀,因?yàn)樗挥涀×藬?shù)據(jù)的兩個(gè)版本。
快照隔離也是通過寫鎖的方式來避免臟寫,而避免臟讀的方式無需加鎖,而是通過讀取數(shù)據(jù)庫(kù)中維護(hù)的對(duì)應(yīng)版本的數(shù)據(jù)對(duì)象,它的關(guān)鍵原則是讀不阻塞寫,寫不阻塞讀。這也就意味著數(shù)據(jù)庫(kù)在處理一致性快照上的長(zhǎng)時(shí)間查詢時(shí),能夠同時(shí)處理寫入,而不會(huì)發(fā)生鎖的爭(zhēng)用。
使用InnoDB引擎的MySQL對(duì)快照隔離的實(shí)現(xiàn)方法是MVCC多版本并發(fā)控制,它會(huì)同時(shí)維護(hù)單個(gè)對(duì)象的多個(gè)版本,以提供多個(gè)不同時(shí)間節(jié)點(diǎn)的數(shù)據(jù)狀態(tài),我們下面來簡(jiǎn)單地看一下它的實(shí)現(xiàn)原理。
對(duì)于InnoDB引擎的表來說,它的聚簇索引記錄中都包含兩個(gè)必要的隱藏列:trx_id
和roll_pointer
- trx_id: 事務(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è)指針,可以通過它來找到該記錄修改前的信息。我們舉個(gè)例子來理解它,假設(shè)表 hero 中只包含一條記錄:
mysql> select * from hero;
+-----+-----+
|id |name |
+-----+-----+
|1 |劉備 |
+-----+-----+
指定插入該記錄的事務(wù)ID為80,此時(shí)再開啟兩個(gè)事務(wù)對(duì)這條記錄進(jìn)行修改,每次修改都會(huì)生成一條 undo log,每條日志也都有 trx_id 屬性和 roll_pointer 屬性。通過 roll_pointer 屬性可以將多條 undo log 連接成一條鏈表,如下圖所示:
這個(gè)鏈表被稱為版本鏈,版本鏈的頭節(jié)點(diǎn)是當(dāng)前最新的記錄,利用這個(gè)記錄的版本鏈可以來控制并發(fā)事務(wù)訪問相同記錄時(shí)的行為,這種方式被稱為多版本并發(fā)控制。
事務(wù)在執(zhí)行第一次查詢的時(shí)候會(huì)生成一致性快照(Read View),通過它來判斷版本鏈中的哪個(gè)版本對(duì)當(dāng)前事務(wù)是可見的。Read View 中包含4個(gè)比較重要的內(nèi)容如下:
- m_ids: 生成 Read View 時(shí),當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)的 id 列表,它用來保證即使活躍的這些事務(wù)被提交,它們的寫入也會(huì)被當(dāng)前事務(wù)忽略
- min_trx_id: 當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)的最小事務(wù) id
- max_trx_id: 系統(tǒng)應(yīng)該分配給下一個(gè)事務(wù)的事務(wù) id
- creator_id: 生成該 Read View 的事務(wù)的事務(wù) id
有了 Read View,只需要按照下面的步驟去判斷記錄中的某個(gè)版本是否可見:
- 如果被訪問版本的 trx_id 屬性值與 Read View 中的 creator 中的 creator_trx_id 值相同,則意味著當(dāng)前事務(wù)在訪問自己修改過的內(nèi)容,該版本能夠被當(dāng)前事務(wù)訪問
- 如果被訪問的版本的 trx_id 屬性值小于 Read View 中的 min_trx_id 值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成 Read View 前已經(jīng)提交,這些版本能夠被當(dāng)前事務(wù)訪問
- 如果被訪問的版本的 trx_id 屬性大于或等于 Read View 中的 max_trx_id 值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成 Read View 之后才開啟,那么該版本不能被當(dāng)前事務(wù)訪問
- 如果被訪問的版本的 trx_id 屬性值在 Read View 的 min_trx_id 和 max_trx_id 之間,則需要判斷 trx_id 是否在 m_ids 列表中。如果在,說明創(chuàng)建該 Read View 時(shí)生成該版本的事務(wù)還是活躍的,所以該版本不可見;如果不在,說明創(chuàng)建該 Read View 時(shí)生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問
也就是說,想要滿足記錄對(duì)當(dāng)前讀事務(wù)可見,需要創(chuàng)建該記錄的事務(wù)在當(dāng)前讀事務(wù)開啟前已經(jīng)提交。
3.4 可串行化
可串行化通常被認(rèn)為是最強(qiáng)的隔離級(jí)別,能夠避免我們上訴所有數(shù)據(jù)不一致問題。它能保證即使事務(wù)可以并行執(zhí)行,但最終的結(jié)果也是一樣的,就好像它們沒有任何并發(fā)性,連續(xù)挨個(gè)執(zhí)行一樣。也就是說,數(shù)據(jù)庫(kù)可以防止所有可能的競(jìng)爭(zhēng)條件。
可串行化的實(shí)現(xiàn)技術(shù)大多采用如下3種方式之一:串行化執(zhí)行事務(wù),兩階段鎖定或可串行化快照隔離。
串行化執(zhí)行事務(wù)
使用這種技術(shù)實(shí)現(xiàn)必須要求每個(gè)事務(wù)小而快,如果其中有一個(gè)緩慢的事務(wù),那么自然會(huì)將其他事務(wù)拖慢。除此之外,這種方式限于活躍數(shù)據(jù)集可以放入內(nèi)存的情況,如果需要在事務(wù)中訪問磁盤中的數(shù)據(jù),那么系統(tǒng)也會(huì)變得非常慢。寫入吞吐量必須低到在單個(gè)CPU核上處理,如若不然,事務(wù)需要能劃分至單個(gè)分區(qū),且不需要跨分區(qū)協(xié)調(diào)。當(dāng)然跨分區(qū)事務(wù)可以實(shí)現(xiàn),但是它的執(zhí)行效率會(huì)非常低。所以,串行化執(zhí)行事務(wù)的伸縮性較差。
兩階段鎖定(2PL, two pahse locking)
兩階段提交的含義是:第一階段事務(wù)執(zhí)行時(shí)獲取鎖(共享鎖/排他鎖),第二階段在事務(wù)執(zhí)行完成時(shí)釋放鎖。它要求沒有寫入時(shí)多個(gè)事務(wù)都可以讀取同一個(gè)對(duì)象,但是只要有寫入就會(huì)獨(dú)占訪問,讀阻塞寫,寫也會(huì)阻塞讀,與快照隔離不同,因此兩階段鎖定可以避免競(jìng)爭(zhēng)條件而實(shí)現(xiàn)可串行化。
Mysql的InnoDB引擎實(shí)現(xiàn)可串行化隔離級(jí)別采用的就是2PL機(jī)制。
兩階段鎖定的性能很差,不僅是因?yàn)樗@取和釋放鎖的開銷,而且還包括并發(fā)性的降低,因?yàn)槿绻麅蓚€(gè)事務(wù)修改同一個(gè)對(duì)象時(shí),第二個(gè)事務(wù)必須要等待第一個(gè)事務(wù)執(zhí)行完為止。除此之外,2PL實(shí)現(xiàn)的可串行化隔離出現(xiàn)死鎖的情況也比較頻繁。
可串行化快照隔離(SSI, serializable snapshot isolation)
可串行化快照隔離是一種樂觀的并發(fā)控制技術(shù),它在快照隔離的基礎(chǔ)上,添加了一種算法來檢測(cè)寫入之間的串行化沖突,并確定要終止哪些事務(wù)。
樂觀意味著如果存在潛在的危險(xiǎn)也不阻止事務(wù),而是繼續(xù)執(zhí)行事務(wù),希望一切都會(huì)好起來。當(dāng)一個(gè)事務(wù)想要提交時(shí),數(shù)據(jù)庫(kù)檢查是否有什么不好的事情發(fā)生(即隔離是否被違反),如果是的話,事務(wù)將被中止,并且必須重試。在爭(zhēng)用不是很高時(shí),樂觀的并發(fā)控制往往比悲觀的并發(fā)控制性能要好。
事務(wù)從數(shù)據(jù)庫(kù)中讀取一些數(shù)據(jù),并根據(jù)這些數(shù)據(jù)進(jìn)行條件判斷執(zhí)行業(yè)務(wù)邏輯時(shí),在快照隔離的條件下,往往先前的查詢結(jié)果不是最新的,因?yàn)樵跀?shù)據(jù)查詢之后,該數(shù)據(jù)可能會(huì)被修改,所以執(zhí)行的業(yè)務(wù)邏輯可能會(huì)出現(xiàn)異常。因此在事務(wù)提交時(shí)判斷先前讀的數(shù)據(jù)是否發(fā)生改變就需要兩方面的校驗(yàn):
- 檢查是否存在讀之前未提交的寫入
- 檢查讀之后的寫入
只有通過這些校驗(yàn)后才能保證事務(wù)提交時(shí)使用的數(shù)據(jù)是新的。
可串行化快照隔離與串行執(zhí)行相比,可串行化快照隔離并不局限于單個(gè) CPU 核的吞吐量,所以它的伸縮性更好;可串行化快照隔離與兩階段鎖定相比,它的最大優(yōu)點(diǎn)是一個(gè)事務(wù)不需要阻塞等待另一個(gè)事務(wù)所持有的鎖,就像在快照隔離下一樣,讀不阻塞寫,寫也不阻塞讀,這對(duì)于讀取較多的業(yè)務(wù)場(chǎng)景非常友好。
可串行化快照隔離的性能表現(xiàn)在中止率上,如果長(zhǎng)時(shí)間的讀寫事務(wù)較多,很可能會(huì)經(jīng)常發(fā)生沖突導(dǎo)致事務(wù)中止。因此在事務(wù)比較短小的情況下,可串行化快照隔離的表現(xiàn)更好。
4. 及時(shí)性與完整性
ACID事務(wù)通常能保證強(qiáng)一致性,也就是說,寫入者會(huì)等到事務(wù)提交,而且在寫入完成后,寫入結(jié)果對(duì)所有讀取者可見。在強(qiáng)一致性這個(gè)語(yǔ)義中,包含兩個(gè)特別值得考慮的方面:
- 及時(shí)性:這意味著確保用戶觀察到系統(tǒng)的最新狀態(tài)。如果不是強(qiáng)一致性而是最終一致性的情況,那么用戶可能會(huì)讀取到陳舊的數(shù)據(jù),但這種不一致是暫時(shí)的,最終都會(huì)通過等待與簡(jiǎn)單地重試得到解決
- 完整性:完整性代表數(shù)據(jù)沒有丟失、矛盾或錯(cuò)誤,即沒有損壞。尤其是某些衍生數(shù)據(jù)集(緩存、搜索索引等),它們一定要與底層數(shù)據(jù)庫(kù)保持一致。在ACID事務(wù)中,原子性和持久性是保證完整性的重要原則
有意思的是:基于異步流處理系統(tǒng)實(shí)現(xiàn)的分布式事務(wù),它能夠?qū)⒓皶r(shí)性與完整性分開,只保證完整性,而不保證及時(shí)性,除非我們顯示地構(gòu)建一個(gè)在事務(wù)提交返回結(jié)果之前明確等待特定消息到達(dá)的消費(fèi)者。
下面我們來看一個(gè)基于流處理系統(tǒng)實(shí)現(xiàn)分布式事務(wù)的例子,來加深對(duì)及時(shí)性和完整性的理解。
4.1 使用基于日志的消息隊(duì)列保證完整性
我們以轉(zhuǎn)賬為例,比如有三個(gè)分區(qū):一個(gè)包含請(qǐng)求ID,一個(gè)包含收款人賬戶,另一個(gè)包含付款人賬戶。如果在數(shù)據(jù)庫(kù)傳統(tǒng)的方法中,執(zhí)行此事務(wù)需要跨三個(gè)分區(qū)進(jìn)行原子提交,這樣就需要協(xié)調(diào)分布式事務(wù),因此吞吐量很可能會(huì)受到影響。但事實(shí)上使用基于日志的消息隊(duì)列實(shí)現(xiàn)的流處理系統(tǒng),可以達(dá)到等價(jià)的數(shù)據(jù)完整性而不需要原子提交。例子執(zhí)行過程如下:
- 從賬戶 A 向賬戶 B 轉(zhuǎn)賬的請(qǐng)求由客戶端提供一個(gè)唯一的請(qǐng)求 ID,并按請(qǐng)求 ID 追加寫入相應(yīng)的消息隊(duì)列,并對(duì)該消息進(jìn)行持久化
- 消費(fèi)者讀取請(qǐng)求日志。對(duì)于每個(gè)請(qǐng)求消息,它向輸出流發(fā)出兩條消息:付款人的借記指令(A分區(qū)),收款人的貸記指令(B分區(qū)),發(fā)出的消息中會(huì)攜帶原始的請(qǐng)求ID
- 后續(xù)消費(fèi)者消費(fèi)借記和貸記指令,按照ID除重,并將變更應(yīng)用到賬戶的余額
為了在多分區(qū)間保證數(shù)據(jù)完整性而且還要避免對(duì)分布式事務(wù)的協(xié)調(diào)(2PC等協(xié)議),我們首先需要將這個(gè)事務(wù)所要做的事情持久化為單條記錄,然后從這條消息記錄中衍生出貸記指令和借記指令。在幾乎所有的數(shù)據(jù)系統(tǒng)中,單對(duì)象的寫入都是原子性的:即請(qǐng)求要么出現(xiàn)在日志中,要么都不出現(xiàn)。
如果流處理在步驟2崩潰,則它會(huì)從上一個(gè)存檔點(diǎn)恢復(fù)處理,這樣它就不會(huì)跳過任何消息,但可能會(huì)生成多條重復(fù)的借記/貸記指令,不過由于它是確定性的,因此它生成的只是相同的指令,在步驟3中的處理器可以通過ID值輕松地去重。
在上述例子中,我們把一個(gè)操作拆分為跨越多個(gè)階段的流處理器,消息記錄的消費(fèi)是異步的,發(fā)送者不會(huì)等其消息被消費(fèi)處理完,而且這個(gè)消息與消息的處理結(jié)果被解耦,所以我們沒有對(duì)及時(shí)性進(jìn)行保證,只是保證了完整性。
一般地,我們?cè)诮柚煽康牧魈幚硐到y(tǒng)時(shí)無需再協(xié)調(diào)分布式事務(wù)或采用其他原子提交協(xié)議就能保證完整性,其中所包含的機(jī)制如下:
- 將寫入操作的內(nèi)容表示為單條消息,這樣就保證了寫入的原子性
- 從這一消息中衍生出其他所需要的狀態(tài)變更
- 將客戶端生成的請(qǐng)求ID傳遞通過所有的處理層,從而能達(dá)到去重和保證冪等性的目的
- 保證消息不可變,并允許衍生數(shù)據(jù)能被隨時(shí)重新處理,這使從錯(cuò)誤中恢復(fù)更加容易
4.2 完整性的重要性
不論是ACID事務(wù)還是基于流處理系統(tǒng)的分布式事務(wù),它們都保證數(shù)據(jù)的完整性。因?yàn)檫`反及時(shí)性可能會(huì)令人困惑,不過這只是暫時(shí)的,但是如果違反完整性,那么它的結(jié)果可能是災(zāi)難性的。違反一致性,最終一致性;違反完整性,永無一致性,是最好的概括。
巨人的肩膀
- 《數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計(jì)》:第七章 事務(wù)、第十二章 數(shù)據(jù)系統(tǒng)的未來
- Replication(下):事務(wù),一致性與共識(shí)
- 《MySQL是怎樣運(yùn)行的》第二十一章
- 《高性能MySQL 第四版》第一章
作者:京東物流?王奕龍文章來源:http://www.zghlxwxcb.cn/news/detail-632867.html
來源:京東云開發(fā)者社區(qū)文章來源地址http://www.zghlxwxcb.cn/news/detail-632867.html
到了這里,關(guān)于事務(wù),不只ACID的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!