歡迎光臨
每天分享高質量文章

事務的ACID屬性我就是傻傻的分不清。。。

本文閱讀時間大約17分鐘。

我們的希望是:讓天下沒有難學的知識,不是學生學不會,只怪老師講不對

事務的起源

對於大部分程式員來說,他們的任務就是把現實世界的業務場景對映到資料庫世界。比如銀行為了儲存人們的賬戶資訊會建立一個account表:

CREATE TABLE account (
    id INT NOT NULL AUTO_INCREMENT COMMENT '自增id',
    name VARCHAR(100) COMMENT '客戶名稱',
    balance INT COMMENT '餘額',
    PRIMARY KEY (id)
) Engine=InnoDB CHARSET=utf8;

狗哥和貓爺是一對好基友,他們都到銀行開一個賬戶,他們在現實世界中擁有的資產就會體現在資料庫世界的account表中。比如現在狗哥有11元,貓爺只有2元,那麼現實中的這個情況對映到資料庫的account表就是這樣:

+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 狗哥   |      11 |
|  2 | 貓爺   |       2 |
+----+--------+---------+

在某個特定的時刻,狗哥貓爺這些傢伙在銀行所擁有的資產是一個特定的值,這些特定的值也可以被描述為賬戶在這個特定的時刻現實世界的一個狀態。隨著時間的流逝,狗哥和貓爺可能陸續進行向賬戶中存錢、取錢或者向別人轉賬等操作,這樣他們賬戶中的餘額就可能發生變動,每一個操作都相當於現實世界中賬戶的一次狀態轉換。資料庫世界作為現實世界的一個對映,自然也要進行相應的變動。不變不知道,一變嚇一跳,現實世界中一些看似很簡單的狀態轉換,對映到資料庫世界卻不是那麼容易的。比方說有一次貓爺在賭場賭博輸了錢,急忙打電話給狗哥要借10塊錢,不然那些看場子的就會把自己剁了。現實世界中的狗哥走向了ATM機,輸入了貓爺的賬號以及10元的轉賬金額,然後按下確認,狗哥就拔卡走人了。對於資料庫世界來說,相當於執行了下邊這兩條陳述句:

UPDATE account SET balance = balance - 10 WHERE id = 1;
UPDATE account SET balance = balance + 10 WHERE id = 2;

但是這裡頭有個問題,上述兩條陳述句只執行了一條時忽然伺服器斷電了咋辦?把狗哥的錢扣了,但是沒給貓爺轉過去,那貓爺還是逃脫不了被砍死的噩運~ 即使對於單獨的一條陳述句,我們前邊嘮叨Buffer Pool時也說過,在對某個頁面進行讀寫訪問時,都會先把這個頁面載入到Buffer Pool中,之後如果修改了某個頁面,也不會立即把修改同步到磁碟,而只是把這個修改了的頁面加到Buffer Poolflush連結串列中,在之後的某個時間點才會掃清到磁碟。如果在將修改過的頁掃清到磁碟之前系統崩潰了那豈不是貓爺還是要被砍死?或者在掃清磁碟的過程中(只掃清部分資料到磁碟上)系統奔潰了貓爺也會被砍死?

怎麼才能保證讓可憐的貓爺不被砍死呢?其實再仔細想想,我們只是想讓某些資料庫運運算元合現實世界中狀態轉換的規則而已,設計資料庫的大叔們仔細盤算了盤算,現實世界中狀態轉換的規則有好幾條,待我們慢慢道來。

原子性(Atomicity)

現實世界中轉賬操作是一個不可分割的操作,也就是說要麼壓根兒就沒轉,要麼轉賬成功,不能存在中間的狀態,也就是轉了一半的這種情況。設計資料庫的大叔們把這種要麼全做,要麼全不做的規則稱之為原子性。但是在現實世界中的一個不可分割的操作卻可能對應著資料庫世界若干條不同的操作,資料庫中的一條操作也可能被分解成若干個步驟(比如先修改快取頁,之後再掃清到磁碟等),最要命的是在任何一個可能的時間都可能發生意想不到的錯誤(可能是資料庫本身的錯誤,或者是作業系統錯誤,甚至是直接斷電之類的)而使操作執行不下去,所以貓爺可能會被砍死。為了保證在資料庫世界中某些操作的原子性,設計資料庫的大叔需要費一些心機來保證如果在執行操作的過程中發生了錯誤,把已經做了的操作恢覆成沒執行之前的樣子,這也是我們後邊章節要仔細嘮叨的內容。

隔離性(Isolation)

現實世界中的兩次狀態轉換應該是互不影響的,比如說狗哥向貓爺同時進行的兩次金額為5元的轉賬(假設可以在兩個ATM機上同時操作)。那麼最後狗哥的賬戶裡肯定會少10元,貓爺的賬戶裡肯定多了10元。但是到對應的資料庫世界中,事情又變的複雜了一些。為了簡化問題,我們粗略的假設狗哥向貓爺轉賬5元的過程是由下邊幾個步驟組成的:

  • 步驟一:讀取狗哥賬戶的餘額到變數A中,這一步驟簡寫為read(A)

  • 步驟二:將狗哥賬戶的餘額減去轉賬金額,這一步驟簡寫為A = A - 5

  • 步驟三:將狗哥賬戶修改過的餘額寫到磁碟裡,這一步驟簡寫為write(A)

  • 步驟四:讀取貓爺賬戶的餘額到變數B,這一步驟簡寫為read(B)

  • 步驟五:將貓爺賬戶的餘額加上轉賬金額,這一步驟簡寫為B = B + 5

  • 步驟六:將貓爺賬戶修改過的餘額寫到磁碟裡,這一步驟簡寫為write(B)

我們將狗哥向貓爺同時進行的兩次轉賬操作分別稱為T1T2,在現實世界中T1T2是應該沒有關係的,可以先執行完T1,再執行T2,或者先執行完T2,再執行T1,對應的資料庫操作就像這樣:


但是很不幸,真實的資料庫中T1T2的操作可能交替執行,比如這樣:

如果按照上圖中的執行順序來進行兩次轉賬的話,最終狗哥的賬戶裡還剩6元錢,相當於只扣了5元錢,但是貓爺的賬戶裡卻成了12元錢,相當於多了10元錢,這銀行豈不是要虧死了?

所以對於現實世界中狀態轉換對應的某些資料庫操作來說,不僅要保證這些操作以原子性的方式執行完成,而且要保證其它的狀態轉換不會影響到本次狀態轉換,這個規則被稱之為隔離性。這時設計資料庫的大叔們就需要採取一些措施來讓訪問相同資料(上例中的A賬戶和B賬戶)的不同狀態轉換(上例中的T1T2)對應的資料庫操作的執行順序有一定規律,這也是我們後邊章節要仔細嘮叨的內容。

一致性(Consistency)

我們生活的這個世界存在著形形色色的約束,比如身份證號不能重覆,性別隻能是男或者女,高考的分數只能在0~750之間,人民幣面值最大隻能是100(現在是2019年),紅綠燈只有3種顏色,房價不能為負的,學生要聽老師話,吧啦吧啦有點兒扯遠了~ 只有符合這些約束的資料才是有效的,比如有個小孩兒跟你說他高考考了1000分,你一聽就知道他胡扯呢。資料庫世界只是現實世界的一個對映,現實世界中存在的約束當然也要在資料庫世界中有所體現。如果資料庫中的資料全部符合現實世界中的約束(all defined rules),我們說這些資料就是一致的,或者說符合一致性的。

如何保證資料庫中資料的一致性(就是符合所有現實世界的約束)呢?這其實靠兩方面的努力:

  • 資料庫本身能為我們保證一部分一致性需求(就是資料庫自身可以保證一部分現實世界的約束永遠有效)。

    我們知道MySQL資料庫可以為表建立主鍵、唯一索引、外來鍵、宣告某個列為NOT NULL來拒絕NULL值的插入。比如說當我們對某個列建立唯一索引時,如果插入某條記錄時該列的值重覆了,那麼MySQL就會報錯並且拒絕插入。除了這些我們已經非常熟悉的保證一致性的功能,MySQL還支援CHECK語法來自定義約束,比如這樣:

    CREATE TABLE account (
        id INT NOT NULL AUTO_INCREMENT COMMENT '自增id',
        name VARCHAR(100) COMMENT '客戶名稱',
        balance INT COMMENT '餘額',
        PRIMARY KEY (id),
        CHECK (balance >= 0) 
    );

    上述例子中的CHECK陳述句本意是想規定balance列不能儲存小於0的數字,對應的現實世界的意思就是銀行賬戶餘額不能小於0。但是很遺憾,MySQL僅僅支援CHECK語法,但實際上並沒有一點卵用,也就是說即使我們使用上述帶有CHECK子句的建表陳述句來建立account表,那麼在後續插入或更新記錄時,MySQL並不會去檢查CHECK子句中的約束是否成立。

    小貼士: 其它的一些資料庫,比如SQL Server或者Oracle支援的CHECK語法是有實實在在的作用的,每次進行插入或更新記錄之前都會檢查一下資料是否符合CHECK子句中指定的約束條件是否成立,如果不成立的話就會拒絕插入或更新。

    雖然CHECK子句對一致性檢查沒什麼卵用,但是我們還是可以透過定義觸發器的方式來自定義一些約束條件以保證資料庫中資料的一致性。

    小貼士: 觸發器是MySQL基礎內容中的知識,本書是一本MySQL進階的書籍,如果你不瞭解觸發器,那恐怕要找本基礎內容的書籍來看看了。

  • 更多的一致性需求需要靠寫業務程式碼的程式員自己保證。

    為建立現實世界和資料庫世界的對應關係,理論上應該把現實世界中的所有約束都反應到資料庫世界中,但是很不幸,在更改資料庫資料時進行一致性檢查是一個耗費效能的工作,比方說我們為account表建立了一個觸發器,每當插入或者更新記錄時都會校驗一下balance列的值是不是大於0,這就會影響到插入或更新的速度。僅僅是校驗一行記錄符不符合一致性需求倒也不是什麼大問題,有的一致性需求簡直變態,比方說銀行會建立一張代表賬單的表,裡邊兒記錄了每個賬戶的每筆交易,每一筆交易完成後,都需要保證整個系統的餘額等於所有賬戶的收入減去所有賬戶的支出。如果在資料庫層面實現這個一致性需求的話,每次發生交易時,都需要將所有的收入加起來減去所有的支出,再將所有的賬戶餘額加起來,看看兩個值相不相等。這不是搞笑呢麼,如果賬單表裡有幾億條記錄,光是這個校驗的過程可能就要跑好幾個小時,也就是說你在煎餅攤買個煎餅,使用銀行卡付款之後要等好幾個小時才能提示付款成功,這樣的效能代價是完全承受不起的。

    現實生活中複雜的一致性需求比比皆是,而由於效能問題把一致性需求交給資料庫去解決這是不現實的,所以這個鍋就甩給了業務端程式員。比方說我們的account表,我們也可以不建立觸發器,只要編寫業務的程式員在自己的業務程式碼裡判斷一下,當某個操作會將balance列的值更新為小於0的值時,就不執行該操作就好了嘛!

我們前邊嘮叨的原子性隔離性都會對一致性產生影響,比如我們現實世界中轉賬操作完成後,有一個一致性需求就是參與轉賬的賬戶的總的餘額是不變的。如果資料庫不遵循原子性要求,也就是轉了一半就不轉了,也就是說給狗哥扣了錢而沒給貓爺轉過去,那最後就是不符合一致性需求的;類似的,如果資料庫不遵循隔離性要求,就像我們前邊嘮叨隔離性時舉的例子中所說的,最終狗哥賬戶中扣的錢和貓爺賬戶中漲的錢可能就不一樣了,也就是說不符合一致性需求了。所以說,資料庫某些操作的原子性和隔離性都是保證一致性的一種手段,在操作執行完成後保證符合所有既定的約束則是一種結果。那滿足原子性隔離性的操作一定就滿足一致性麼?那倒也不一定,比如說狗哥要轉賬20元給貓爺,雖然在滿足原子性隔離性,但轉賬完成了之後狗哥的賬戶的餘額就成負的了,這顯然是不滿足一致性的。那不滿足原子性隔離性的操作就一定不滿足一致性麼?這也不一定,只要最後的結果符合所有現實世界中的約束,那麼就是符合一致性的。

永續性(Durability)

當現實世界的一個狀態轉換完成後,這個轉換的結果將永久的保留,這個規則被設計資料庫的大叔們稱為永續性。比方說狗哥向貓爺轉賬,當ATM機提示轉賬成功了,就意味著這次賬戶的狀態轉換完成了,狗哥就可以拔卡走人了。如果當狗哥走掉之後,銀行又把這次轉賬操作給撤銷掉,恢復到沒轉賬之前的樣子,那貓爺不就慘了,又得被砍死了,所以這個永續性是非常重要的。

當把現實世界的狀態轉換對映到資料庫世界時,永續性意味著該轉換對應的資料庫操作所修改的資料都應該在磁碟上保留下來,不論之後發生了什麼事故,本次轉換造成的影響都不應該被丟失掉(要不然貓爺還是會被砍死)。

事務的概念

為了方便大家記住我們上邊嘮叨的現實世界狀態轉換過程中需要遵守的4個特性,我們把原子性Atomicity)、隔離性Isolation)、一致性Consistency)和永續性Durability)這四個詞對應的英文單詞首字母提取出來就是AICD,稍微變換一下順序可以組成一個完整的英文單詞:ACID。想必大家都是學過初高中英語的,ACID是英文的意思,以後我們提到ACID這個詞兒,大家就應該想到原子性、一致性、隔離性、永續性這幾個規則。另外,設計資料庫的大叔為了方便起見,把需要保證原子性隔離性一致性永續性的一個或多個資料庫操作稱之為一個事務(英文名是:transaction)。

我們現在知道事務是一個抽象的概念,它其實對應著一個或多個資料庫操作,設計資料庫的大叔根據這些操作所執行的不同階段把事務大致上劃分成了這麼幾個狀態:

  • 活動的(active)

    事務對應的資料庫操作正在執行過程中時,我們就說該事務處在活動的狀態。

  • 部分提交的(partially committed)

    當事務中的最後一個操作執行完成,但由於操作都在記憶體中執行,所造成的影響並沒有掃清到磁碟時,我們就說該事務處在部分提交的狀態。

  • 失敗的(failed)

    當事務處在活動的或者部分提交的狀態時,可能遇到了某些錯誤(資料庫自身的錯誤、作業系統錯誤或者直接斷電等)而無法繼續執行,或者人為的停止當前事務的執行,我們就說該事務處在失敗的狀態。

  • 中止的(aborted)

    如果事務執行了半截而變為失敗的狀態,比如我們前邊嘮叨的狗哥向貓爺轉賬的事務,當狗哥賬戶的錢被扣除,但是貓爺賬戶的錢沒有增加時遇到了錯誤,從而當前事務處在了失敗的狀態,那麼就需要把已經修改的狗哥賬戶餘額調整為未轉賬之前的金額,換句話說,就是要撤銷失敗事務對當前資料庫造成的影響。書面一點的話,我們把這個撤銷的過程稱之為回滾。當回滾操作執行完畢時,也就是資料庫恢復到了執行事務之前的狀態,我們就說該事務處在了中止的狀態。

  • 提交的(committed)

    當一個處在部分提交的狀態的事務將修改過的資料都同步到磁碟上之後,我們就可以說該事務處在了提交的狀態。

隨著事務對應的資料庫操作執行到不同階段,事務的狀態也在不斷變化,一個基本的狀態轉換圖如下所示:

從圖中大家也可以看出了,只有當事務處於提交的或者中止的狀態時,一個事務的生命週期才算是結束了。對於已經提交的事務來說,該事務對資料庫所做的修改將永久生效,對於處於中止狀態的事務,該事務對資料庫所做的所有修改都會被回滾到沒執行該事務之前的狀態。

小貼士: 此貼士處純屬扯犢子,與正文沒啥關係,純屬吐槽。大家知道我們的計算機術語基本上全是從英文翻譯成中文的,事務的英文是transaction,英文直譯就是交易,買賣的意思,交易就是買的人付錢,賣的人交貨,不能付了錢不交貨,交了貨不付錢把,所以交易本身就是一種不可分割的操作。不知道是哪位大神把transaction翻譯成了事務(我想估計是他們也想不出什麼更好的詞兒,只能隨便找一個了),事務這個詞兒完全沒有交易、買賣的意思,所以大家理解起來也會比較困難,外國人理解transaction可能更好理解一點吧~

    贊(0)

    分享創造快樂