資料庫事務概論

北冥有隻魚發表於2022-02-19
重新梳理一下對事務的理解,這個系列的文章在20年就寫了一大部分,後面轉向優先順序更高階的文章了,想拿出來續寫的時候,發現由於檔案沒有上傳網路,於是就去另一臺電腦上找,然後打不開了。這個故事告訴我們,核心資料要做容災。

前言

本系列文章的寫作思路,先提出總綱, 總領後面資料庫事務系列所有的文章, 後面再針對其他資料庫的事務,像下面這樣:

資料庫系列文章.png

事務簡介

現實世界被對映到軟體世界,有的時候就會有出現一些專屬於軟體世界的問題,舉一個簡單而又經典的例子轉賬, A向B借10元錢,假設現在還沒有網路支付,A就是從B錢包中拿10元錢給A, 就算是多個人向B借錢這也沒什麼問題,B會依次處理借錢請求,同一時間段借錢的人越多,B借錢的速度越慢,有可能還要考慮一下交情等各方面的因素。

排隊處理請求.png

但是如果我們將這個轉賬引入到軟體世界,就會引出現實世界不存在的問題,你錢包裡有五十,你就只能借五十,不可能出現你有五十,你借出去一百,然後錢包裡面出現了一個負五十,沒錯說的就是你一致性。除此之外,借錢操作一般也不會存在中間態,要麼借錢成功,要麼錢就沒到借錢人手裡。這也就是原子性。現實世界的一些操作對映到軟體世界,情況就又會變得複雜一些,A向B借錢這一個操作在資料庫操作就會被分割為若干個操作,我們來簡單的介紹一下轉賬操作在資料庫世界是怎麼樣的,在故事的開始我們先準備一張賬戶表:

CREATE TABLE `accounts`  (
  `id` bigint(20) NOT NULL COMMENT '主鍵',
  `userId` bigint(20) NOT NULL COMMENT '使用者ID',
  `money` int(255) NOT NULL COMMENT '錢款',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci;

這個建表語句是MySQL下面建表語句, 一個轉賬操作就對應下面兩條SQL語句:

UPDATE accounts SET money = money - 5  WHERE id = 1; // 1是B的ID 2是A的ID,此時的場景是A向B借5塊錢
UPDATE accounts SET money = money + 5  WHERE id = 2;  

以上兩條語句在資料庫的真實執行過程還會更復雜一點,為了說明問題,我們將轉賬模型簡化為如下操作:

​ 1. 將小B的賬戶餘額讀取到變數A中,這一步驟簡記為read(A)

​ 2 .將小B的餘額減去賬戶餘額,簡記為 A = A - 5

​ 3 . 將小B修改過後的餘額寫入磁碟裡,這一步驟簡單寫為write(A)

  1. 讀取小A賬號的餘額到變數B,這一步驟簡寫為read(B)
  2. 將小A的賬戶餘額加上小B轉賬過來的餘額, 簡單記為B = B + 5。
  3. 將小A賬戶修改過的餘額寫到磁碟裡,這一步驟簡寫為write(B)

小A向小B借錢借兩次在現實世界是沒什麼問題的,無非就是從錢包裡面掏兩次錢而已,但是在資料庫中兩次操作所對應的步驟就可能是併發執行,而不是排隊執行。為了說明問題,我們將兩次轉賬操作記為T1、T2。在併發執行的場景下, T1在read(A)之後,很快T2也執行了read(A), 我們假設在轉賬操作進行之前,小B只有10元錢,也就是T1和T2在進行轉賬操作的時候都認為小B有十元錢,然後假定T1開始執行2,3,4,5,6之後,T2接著執行,在這種情況下,小B只轉了五元錢,小A的賬戶確多出了十元,多出的五元,讓銀行補出來? 這顯然不合理。那為了避免T1,T2交替執行帶來的問題,最簡單的方法就是讓T1,T2在資料庫排隊執行,這會慢的要死。 所以對於現實世界中狀態轉換對應的某些資料庫操作來說,不僅要保證這些操作以原子性的方式來執行完成,而且要保證其他的狀態轉換不會影響到本次狀態轉換,這個規則我們稱之為隔離性。

在上面的討論中我們已經發現了,在資料庫中對某個表進行修改操作,所引出來的問題, 只是做查詢操作對於資料庫管理系統並沒有什麼影響, 我們將這些對資料庫的有限操作序列稱之為資料庫事務。

事務的狀態

現在我們已經知道,事務其實是一個抽象的概念,由一個有限的資料庫操作序列構成,對應著一個或多個資料庫操作,這些操作所執行的不同階段大致上有以下幾個狀態:

  • 活動的(active)

    事務對應的資料庫操作正在執行過程中,我們就說該事務處在活動的狀態
  • 部分提交的 (partially committed)

    當事務中的最後一個操作執行完成,但是由於操作都在記憶體中執行,所造成的影響並沒有重新整理到磁碟時,我們就說該事務處在部分提交的狀態
  • 失敗的(failed)

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

    如果事務執行了半截變為失敗的狀態,比如 我們上面嘮叨的轉賬事務,當小B的錢被扣除,小A的錢沒有增加時遇到了錯誤從而導致當前事務處在了失敗的狀態,那麼就需要將小B的賬戶餘額調整為未轉賬之前的金額,換句話說,就是要撤銷失敗事務對當前資料庫造成的影響。我們將這個撤銷的過程稱之為回滾。當回滾操作執行完畢的時候,也就是資料庫恢復到了執行事務之前的狀態,我們就說該事務處在了中指的狀態。
  • 提交的(committed)

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

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

    資料庫事務狀態.png

事務並行遇到的問題

讓T1,T2排隊執行犧牲效能這並不是我們想要的方案,我們想要的是既想保持事務的隔離性,又想讓伺服器在處理訪問同一資料的多個事務時儘量高些,捨棄一部分的隔離性來提升效能。 我們知道資料庫是一個客戶端/伺服器架構的軟體,對於同一個伺服器來說,可以有若干個客戶端與之連結,每個客戶端與伺服器連線上之後,就可以稱之為一個會話(Session)。每個客戶端都可以在自己的會話中向伺服器發出請求語句,一個請求語句可能是某個事務的一部分,也就是對於伺服器來說可能同時處理多個事務。現在讓我們來看下假設讓事務並行執行會帶來哪些問題:

  • 髒寫

​ 如果一個事務修改了另一個未提交事務修改過的資料,那就意味著發生了髒寫。如下圖所示:

髒寫.png

Session A 和 Session B 各開啟了一個事務, Session B中的事務先將userId為1改為50,緊接著Session A將userId這行資料改為80,然後提交。 Session B在執行過程中遇到了錯誤或者其他狀況執行了回滾,然後將Session A的更新也不復存在了。這種現象我們一般稱之為髒寫,這是一種十分嚴重的現象。

  • 髒讀

​ 如果一個事務讀到了另一個事務未提交事務修改過的資料,那就意味著發生了髒讀。如下圖所示:

髒讀.png

​ 如上圖所示,Session A 和 Session B各開啟了一個事務,Session B的事務先將userId為1的那一行的money列改為1,Session A查詢到了Session B 還未提交的記錄,然後 Session B執行了回滾,那麼Session A就好像讀到了一個不存在的資料一樣,這種現象我們就稱之為髒讀。

  • 不可重複讀

​ 如果一個事務只能讀到另一個已經提交的事務修改過的資料,並且其他事務每對該資料進行一次修改並提交後,該事務都能查詢得到最新值,那就意味著發生了不可重複讀。

不可重複讀.png

​ Session B中的修改語句屬於隱式事務, 隱式事務意味著語句結束之後,事務就自動提交了,這些事務都修改了userId為1的記錄列money的值。每次事務提交之後,如果Session A中都能從查詢到最新的值,這種現象就意味著發生了不可重複讀。

  • 幻讀

​ 如果一個事務先根據某些條件查詢出一些記錄,之後另一個事務又向表中插入了符合這些條件的記錄,原先的事務再次按照該條件查詢時,能把另一個事務插入的記錄也讀出來,那就意味著發生了幻讀. 示意圖如下:

幻讀

Session A 先根據條件money 大於0查到了一些記錄,Session B中插入了符合條件的記錄被Session A 中的事務再根據條件查詢時查到了Session B中插入的記錄,這種現象我們稱之為幻讀。如果我們在Session B中刪掉了 userId ,Session A再根據條件查詢時發現變少了,那麼這種情況算幻讀嗎? 不算,幻讀強調的是一個事務按照某個相同條件多次讀取記錄時,讀到了之前沒有讀到的記錄。對於之前讀到的記錄,之後讀取不到,這種應該算做不可重複讀。

由事務的隔離性引出事務的隔離級別

上面我們介紹了事務併發執行可能帶來的問題,這些問題也有輕重緩急之分,按照問題的嚴重程度我們來排序:

髒寫 > 髒讀 > 不可重複讀 > 幻讀

我們上面提到的捨棄一部分隔離性來換得效能的提升就是設立隔離級別來解決事務併發執行所帶來的問題,隔離級別登記越低,越嚴重的問題就越可能發生。SQL標準規定了以下幾個隔離級別:

  • READ UNCOMMITED: 未提交讀
  • READ COMMITED: 已提交讀
  • REPEATABLE READ: 可重複讀
  • SERIALIZABLE: 可序列化

SQL標準規定,針對不同的隔離級別,併發事務可以發生不同嚴重程度的問題,具體情況如下:

事務的隔離級別

在哪種情況下,髒寫都是超級嚴重的問題,因此在哪種隔離級別的情況下,髒寫都沒有可能發生。

不同的資料庫廠商對SQL標準規定的四種隔離級別支援不一樣,比如說Oracle就支援READ COMMITED 和 SERIALIZABLE隔離級別。 MySQL雖然支援四種隔離級別,但是MySQL在可重複讀這個隔離級別的情況下,就可以禁止幻讀問題的發生。

SQL Server 在標準之外額外支援了SNAPSHOT 這一級別,PostgreSQL內部只支援READ COMMITED、REPEATABLE READ、SERIALIZABLE這三種,PostgreSQL會將READ UNCOMMITED視為READ COMMITED。隔離級別越高,讀操作的請求鎖定就更嚴格,鎖持有的時間就越長,一致性就越高,併發效能就越低。

總結一下

現實世界的狀態修改對映到了資料庫世界我們需要保證:

  • 原子性

    對於不可分割的操作,要麼成功要麼失敗。
  • 隔離性

    對於現實世界的狀態轉換對應到某些資料庫操作來說,不僅要保證這些操作以原子性的方式來執行完成,而且要保證其它的狀態轉換不會影響到本次狀態轉換,這個規則被稱之為隔離性。
  • 一致性

    現實世界的一些約束到了軟體世界也要予以保持,比如說人民幣的最大幣值等。如果資料庫中的資料全部符合現實世界的約束,我們就說這些資料就是符合一致性的。
  • 永續性

    當現實世界的一個狀態完成後,這個轉換的結果將永久保留,這個規則我們稱之為永續性。當把現實世界的狀態轉換對映到資料庫世界,永續性意味著轉換對應的資料庫操作所修改的資料都應該在磁碟上保留下來。

由此我們引出了事務的概念,我們將需要保證原子性、隔離性、一致性和永續性的一個或多個資料庫操作稱之為一個事務。由事務的隔離性我們引出事務的隔離級別,犧牲一點隔離性來換取效能的提升。事務在資料庫中可能對應多個複雜操作由此我們引出事務的狀態。

參考資料

  • MySQL 是怎樣執行的:從根兒上理解 MySQL 小孩子4919 著
  • PostgreSQL的事務隔離級別介紹及更改

相關文章