SQL的事務


一、基本概念

  1. 事務是數據庫區別於文件系統的重要特性之一,當有了事務,就可以讓數據庫始終保持一致性,同時可以通過事務的機制恢復到某個時間點,保證了提交到數據庫的修改不會因爲系統崩潰而丟失;
  2. 事務只是一個改變,是一些操作的集合,用專業術語說,就是一組邏輯操作單元。事務本身不具備四個特性,而是通過某些手段儘可能讓執行單元滿足四個特性,那麼此時稱之爲一個完整的事務。

經常在網絡上、現實中一提到事務就綁死ACID四個特性,但實際ACID不僅在事務中有體現,還可以涵蓋各個領域。


二、事務處理的原則

所有的事務都是作爲一組邏輯操作單元來執行,即使出現故障,都不能改變這種方式。當在一個事務中執行多個操作時,要麼事務提交(commit),所有修改都會永久地保存下來;要麼事務回滾(rollback),所有修改都放棄,一切迴歸最初狀態。

常見例子:

A給B轉賬,A餘額減少,B餘額增加,這是一套必須同時完成的操作,如果其中一個失敗了,則所有操作都會失敗。

在SQL中體現爲:

UPDATE account SET money = money - 100 WHERE name = 'A';
UPDATE account SET money = money + 100 WHERE name = 'B';

這兩句SQL語句就必須同時完成,或同時失敗。


三、ACID

  • 原子性(atomicity):
    指事務是不可分割的工作單位,要麼全部提交,要麼全部回滾。

    原子就是物理世界中最小的單位了,無法在繼續分下去了,表達的是一個整體性的概念。

  • 一致性(consistency):
    在事務執行前後,數據從一個合法性狀態轉換到另一個合法性狀態,這種狀態是語義上的,不同的業務場景,對於合法性狀態有不同的定義。

    可以理解爲滿足預定結果的狀態稱爲合法的狀態,這種狀態由自己定義,比如現實世界中的約束。如果滿足這種狀態,那就是滿足一致性,如果不滿足,這操作失敗,事務回滾。

    比如餘額字段設置爲無符號數值,即錢是不允許負數的,如果此時SQL語句修改餘額小於0,此時就不滿足定義的語義,所以SQL會執行失敗(顯示出來就是報錯),事務就會回滾。

  • 隔離性(isolation):

    指事務的執行不能被其它事務干擾,即一個事務內部的操作及使用的數據對併發的其它事務都是隔離的、獨立的,併發執行的各個事務之間不能互相干擾。

    比如事務A把餘額從0改成100了,此時事務A還沒有commit,而事務B進來讀取餘額了,此時結果是0還是100?這個時候就引出隔離級別這個概念了。

  • 持久性(durability):

    指事務一旦提交(commit),對數據的改變就是永久性的,之後的其它操作丟不會對其有任何影響。

    比如A給B轉賬100,A減去100,B增加100,然後事務提交,A與B的餘額就都是修改後的了,如果此時A反悔不轉錢了,是無法做到的,只能再開個事務讓B給A轉回去。

    持久性是通過事務日誌來保證的,日誌包括重做日誌(redo)回滾日誌(undo)。當通過事務對數據進行修改時,首先會將數據庫的變化信息記錄到redo日誌中,然後再對數據庫對應的行進行修改,這樣的話即使數據庫崩潰,重啓後也能找到沒有更新到數據庫系統的redo日誌重新執行,從而使事務具有持久性

總結:在ACID中,原子性是基礎,隔離性是手段,一致性是約束條件,持久性是最終的目的。

如果要一句話回答事務是什麼?那就是保證原子性一致性隔離性持久性的一個或多個數據庫操作稱爲一個事務。


四、事務的狀態

  • 活動的(active):

    事務對應的數據庫操作正在執行的過程中,稱爲事務處在活動的狀態。

  • 部分提交的(partially committed):

    當事務的最後一個操作執行完成,改動的數據的還處在內存中,並沒有刷寫到磁盤,此時稱爲事務處在部分提交的狀態。

  • 失敗的(failed):

    當事務處於活動的或者部分提交的狀態時,可能遇到某些錯誤(數據庫自身錯誤、操作系統錯誤、斷電等)而無法繼續執行,或人爲的停止事務的執行,稱爲事務處於失敗的狀態。

  • 中止的(aborted):

    如果事務執行了一部分而變爲失敗的狀態,那麼就需要將修改過的事務的操作還原(就是回滾操作),還原完成後,事務就出在了中止的狀態。

  • 提交的(committed):

    處於部分提交的狀態的事務將修改過的數據刷寫到磁盤後,稱爲事務處在提交的狀態。


五、事務的隔離級別

事務的隔離級別的出現,就是爲了解決ACID中的 I(isolation 隔離性)在併發環境下,同時被操作的數據的隔離問題,同前面所述例子:

比如事務A把餘額從0改成100了,此時事務A還沒有commit,而事務B進來讀取餘額了,此時結果是0還是100?

以MySQL爲例,MySQL是一個客戶端/服務端架構的軟件,對於同一個服務器來說,可以由若干個客戶端與之連接,每個客戶端與服務端連接上之後,稱爲一個會話(Session)。每個客戶端都可以在自己的會話中向服務器發出請求語句,而請求語句可以是某個事務的一部分。對於服務器來說可以同時處理多個事務,但又要滿足隔離性的特性,理論上在某個事務對某個數據進行訪問時,其它事務應該進行排隊,直到該事務提交之後,其它事務才能繼續訪問這個數據(類比Java多線程鎖機制),但這樣性能影響太大。如果既要保持事務的隔離性,又想讓服務器在處理訪問同一數據的多個事務的性能儘可能高,這時候就看二者如何權衡取捨了。

5.1 準備數據

創建一張表及數據

CREATE TABLE student (
    no INT,
    name VARCHAR(20),
    age INT,
    PRIMARY KEY (no)
) Engine=InnoDB;

INSERT INTO student VALUES(1, '小明', 18);

確認數據正常:

mysql> SELECT * FROM student;
+----+--------+------+
| no | name   | age  |
+----+--------+------+
|  1 | 小明   |   18 |
+----+--------+------+
1 row in set (0.00 sec)

5.2 數據併發問題

在併發情況下,如果不保證串行執行(就是一個接一個執行),那麼會出現以下幾種問題:

1. 髒寫(Dirty Write)

對於兩個事務 Session A、Session B,如果 Session A修改了 Session B 修改過還未提交的數據,就意味着發生了髒寫。

髒寫示意圖
發生時間編號 Session A Session B
BEGIN;
BEGIN;
UPDATE student SET name = '李四'
WHERE no = 1;
UPDATE student SET name = '張三'
WHERE no = 1;
COMMIT;
ROLLBACK;

已知表中僅有一條no爲1、name爲小明的記錄。

Session A 與 Session B 各自開啓自己的事務,Session B 的事務先將no爲1的記錄修改name列的值爲李四,然後這條記錄緊接着又 Session A 給改成了張三,並且Session A 還 COMMIT 了,按理來說此時張三就應該永久刷寫到磁盤了,但接着Session B 將它的事務回滾了,對於這記錄的修改全部撤回,即no爲1的記錄的name列的值爲小明。那對於 Session A 來說就有問題了,明明update且commit了,最後一看什麼變化都沒有,這是無法被容忍的問題,所以在標準SQL中的四種隔離級別中都解決了髒寫的問題(就是沒法復現了)。

2. 髒讀(Dirty Read)

對於兩個事務 Session A、Session B,如果 Session A讀取了 Session B 修改過還未提交的數據,之後 Session B 回滾,則 Session A 讀到的數據就是臨時且無效的。

髒讀示意圖
發生時間編號 Session A Session B
BEGIN;
BEGIN;
UPDATE student SET name = '李四'
WHERE no = 1;
SELECR * FROM student
WHERE no = 1;
(如果此時讀出來是李四,而不是小明,則發生了髒讀)
COMMIT;
ROLLBACK;

已知表中僅有一條no爲1、name爲小明的記錄。

Session A 與 Session B 各自開啓自己的事務,Session B 的事務先將no爲1的記錄修改name列的值爲李四,然後緊接着 Session A 查詢這條記錄,如果發現讀到的數據爲李四,即讀到了Session B 中還沒有提交的數據,之後 Session B 進行了回滾,數據還原回小明,那麼 Session A 讀到的李四就是一個不存在的數據,這種現象稱爲髒讀

在某些場合下,返回不存在的數據、假的數據給客戶,是可能會出大問題的。

3. 不可重複度(Non-Repeatable Read)

對於兩個事務 Session A、Session B,Session A 讀取了一條記錄的一個字段,然後 Session B 修改了這條記錄的同個字段,接着 Session A 再重複上一次查詢,兩次結果不同了,這就發生了不可重複讀。

不可重複讀示意圖
發生時間編號 Session A Session B
BEGIN;
SELECR * FROM student
WHERE no = 1;
(此時讀出來是小明
BEGIN;
UPDATE student SET name = '李四' WHERE no = 1;
COMMIT;
SELECR * FROM student
WHERE no = 1;
(讀出來是李四,跟上次不同,則發生了不可重複讀)
BEGIN;
UPDATE student SET name = '王五' WHERE no = 1;
COMMIT;
SELECR * FROM student
WHERE no = 1;
(讀出來是王五,又跟上次不同,則發生了不可重複讀)

已知表中僅有一條no爲1、name爲小明的記錄。

Session A 第一次查詢該記錄爲小明(注意還沒有COMMIT),接着 Session B 修改該記錄爲李四,然後 Session A 再次查詢發現變成李四了,跟上一次查詢結果不同,這種現象稱爲不可重複讀。同理 Session B 又修改值爲王五,緊接着Session A 又再次查詢發現結果又變了,變成了王五,因爲發生了不可重複讀

對於這種現象,在現實中很多場景下中我們會認爲這是合理的、可以接受的,每次查詢就應該查出當前最新的數據,但處於併發事務內的角度來看這屬於一種問題。

4. 幻讀(Phantom Read)

對於兩個事務 Session A、Session B,Session A 讀取了一條記錄的一個字段,然後 Session B 往表插入一些新的記錄,之後 Session A 再次讀取發現多出了新的記錄,這就算幻讀。

幻讀示意圖
發生時間編號 Session A Session B
BEGIN;
SELECR * FROM student
WHERE no > 0;
(此時讀出來只有小明一條記錄)
BEGIN;
INSERT INTO student VALUES(2, '李四', 19);
COMMIT;
SELECR * FROM student
WHERE no > 0;
(此時讀出來有小明李四兩條記錄,發生了幻讀)

已知表中僅有一條no爲1、name爲小明的記錄。

Session A 第一次查詢限定條件爲 no > 0,查出來一條記錄爲小明(注意還沒有COMMIT),接着 Session B 插入了一條記錄李四,然後 Session A 再次查詢結果爲小明李四兩條記錄了,多了一條,這種現象稱爲幻讀,而多出來的記錄稱爲幻影記錄

對於這種現象,還是那句話,在現實中很多場景下中我們會認爲這是合理的、可以接受的,但處於併發事務內的角度來看這屬於一種問題。

對於幻讀的注意點:

​ 幻讀強調的是多出來的記錄。按照示意圖,如果 Session B 不是插入記錄,而是刪除記錄,那 Session A 再次查詢發現記錄數量變少了,這種現象不屬於幻讀。由於第二次查變少了,嚴格歸類的話,這種現象屬於不可重複讀

5.3 SQL標準的四種隔離級別

按照併發問題嚴重程度高到低排序:髒寫 > 髒讀 > 不可重複讀 > 幻讀

由於髒寫問題是無法接受的,所以現在市面主流數據庫都不會出現這個問題,那還剩下後面3個問題。如果能把所有問題都解決那肯定最好,但也就意味着併發性能最差,如果要最高的併發性能,又會出現數據併發的問題......因此回到前面說的權衡取捨問題了,看不同的場景是要數據絕對準確性,還是要最高的併發性能,亦或犧牲部分併發性能以換取數據的相對準確

在SQL官方標準中,設立了以下四個隔離級別:

1. 讀未提交(Read Uncommitted)

在該隔離級別中,所有事務都可以看到其它未提交事務的執行結果,意味着會發生髒讀不可重複讀幻讀

2. 讀已提交(Read Committed)

讀已提交滿足了隔離的基本定義:一個事務只能看見已經提交的事務的執行結果。這是大部分數據庫系統的默認隔離級別,比如Oracle、SQL Server,可以避免髒讀,但仍會發生不可重複讀幻讀

3. 可重複讀(Repeatable Read)

事務A在讀到一條數據之後,此時事務還沒提交或回滾,接着事務B對該數據進行了修改並提交,那麼事務A再次讀取該數據,讀到的還是原來的內容。可以避免髒讀不可重複讀,但仍會發生幻讀,這是MySQL默認的隔離級別。

4. 可串行化(Serializable)

確保事務可以從一個表中讀取相同的行,在整個事務持續期間,禁止其它事務對該表相同行進行插入、更新,刪除操作。所有的問題都可以解決,但性能最爲低下。

SQL標準的四種隔離級別分別可能出現的併發問題
髒寫 髒讀 不可重複讀 幻讀 加鎖讀
Read Uncommitted × ×
Read Committed × × ×
Repeatable Read × × × ×
Serializable × × × ×

5.4 不同數據庫的隔離級別

SQL標準雖然有4種隔離級別,但不同的數據庫的支持程度不一樣。

Oracle MySQL SQL Server
Read Uncommitted ×
Read Committed √(默認) √(默認)
Repeatable Read × √(默認)
Serializable
Snapshot
Read Committed Snapshot

值得注意的是:

  1. MySQL 的實現 Repeatable Read 在一定程度上也可以解決幻讀的問題(快照讀+MVCC機制),與SQL官方標準略有區別;
  2. SQL Server 支持的隔離級別比SQL標準的還要多出兩種。

5.5 其它

MySQL中查看隔離級別:

# 5.7.20之前
SHOW VARIABLES LIKE 'tx_isolation';

# 5.7.20及之後
SHOW VARIABLES LIKE 'transaction_isolation';

# 所有版本均可用
SELECT @@transaction_isolation;

MySQL中設置隔離級別:

SET [GLOBAL|SESSION] TRANSACTION_ISOLATION = '隔離級別';

隔離級別可選項:
> READ-UNCOMMITTED
> READ-COMMITTED
> REPEATABLE-READ
> SERIALIZABLE

或者:

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL '隔離級別';

隔離級別可選項:
> READ UNCOMMITTED
> READ COMMITTED
> REPEATABLE READ
> SERIALIZABLE

GLOBAL:對後續所有新開會話有效

SESSION:只對當前會話有效

無論設置什麼級別,數據庫一旦重啓都以配置文件爲準


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章