數據庫的事務
1、數據庫事務的基本概念
數據庫事務( transaction)是訪問并可能操作各種數據項的一個數據庫操作序列,這些操作要么全部執行,要么全部不執行,是一個不可分割的工作單位。事務由事務開始與事務結束之間執行的全部數據庫操作組成。
在執行某些SQL語句的時候,會要求該系列操作必須全部執行,而不能僅執行一部分。例如,一個轉賬操作:
# 從id=1的賬戶給id=2的賬戶轉賬100元
# 第一步:將id=1的A賬戶余額減去100
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
# 第二步:將id=2的B賬戶余額加上100
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
這兩條SQL語句必須全部執行,或者,由于某些原因,如果第一條語句成功,第二條語句失敗,就必須全部撤銷。
這種把多條語句作為一個整體進行操作的功能,被稱為數據庫事務。數據庫事務可以確保該事務范圍內的所有操作都可以全部成功或者全部失敗。如果事務失敗,那么效果就和沒有執行這些SQL一樣,不會對數據庫數據有任何改動。
2、數據庫事務的四個特性(A原子性、C一致性、I 隔離性、D持久性)
數據庫事務具有ACID這4個特性:
- A:Atomic:原子性,將所有SQL作為原子工作單元執行,要么全部執行,要么全部不執行;
- C:Consistent:一致性,事務完成后,所有數據的狀態都是一致的,即A賬戶只要減去了100,B賬戶則必定加上了100;
- I:Isolation:隔離性,如果有多個事務并發執行,每個事務作出的修改必須與其他事務隔離。但實際上,事務并發時并沒有完全隔離,互相會有影響,需要設置隔離級別。例如同時有T1和T2兩個并發事務,從T1角度來看,T2要不在T1執行之前就已經結束,要么在T1執行完成后才開始。將多個事務隔離開,每個事務都不能訪問到其他事務操作過程中的狀態。
- D:Duration:持久性,即事務完成后,對數據庫數據的修改被持久化存儲。
2.1、隱式事務和顯式事務
- 對于單條SQL語句,數據庫系統自動將其作為一個事務執行,這種事務被稱為隱式事務。
- 要手動把多條SQL語句作為一個事務執行,使用 BEGIN(MySQL中也可以用START TRANSACTION)開啟一個事務,使用
COMMIT提交一個事務,這種事務被稱為顯式事務。COMMIT是指提交事務,即試圖把事務內的所有SQL所做的修改永久保存。如果COMMIT語句執行失敗了,整個事務也會失敗。多條SQL語句要想作為一個事務執行,就必須使用顯式事務。
例如,把上述的轉賬操作作為一個顯式事務:
BEGIN; -- MySQL中也可以用START TRANSACTION UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT;
有些時候,我們希望主動讓事務失敗,這時,可以用ROLLBACK回滾事務,整個事務會失敗:
BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; ROLLBACK;
數據庫事務是由數據庫系統保證的,我們只需要根據業務邏輯使用它就可以。
(這是數據庫層面的操作,Java層面的語法操作開啟事務、結束事務等,實際底層最終也是使用了數據庫語法)
3、多用戶(事務)并發可能發生的問題(臟讀、不可重復讀、幻讀)
對于兩個并發執行的事務,如果涉及到操作同一條記錄的時候,可能會發生問題。在沒有數據庫隔離性的情況下,多用戶并發操作可能會發生以下問題,包括臟讀、不可重復讀、幻讀等。
3.1、臟讀(讀到已修改但尚未提交的數據)(禁止在生產使用)
臟讀發生在一個事務讀取了另一個事務改寫但尚未提交的數據時。如果改寫在稍后被回滾了,那么第一個事務獲取的數據就是無效的。
例如:當一個事務的操作正在修改數據時,而在該事務還未最終提交的時候,另外一個并發事務來讀取了數據,就可能會導致讀取到的數據并非是最終持久化之后的數據,因為該事務后面可能會發生回滾,所以讀到的這個數據就是臟讀的數據。
最典型的例子就是銀行轉賬,從A賬戶轉賬100到B賬戶,腳本命令為:
update account set money = money + 100 where username = 'B'; update account set money = money - 100 where username = 'A';
在這個事務執行過程中,另外一個事務讀取結果發現B賬戶中的錢已經到賬,提示B錢已到賬,B就進行了下一步的操作。但是假設最終轉賬事務失敗了,導致操作回滾。實際上B并未收到錢,但是進行了下一步的操作,造成了損失,這就是臟讀。
3.1.1、臟讀演示
我們通過 MySQL 數據庫來演示臟讀。
首先需要將數據庫的自動提交取消掉,然后修改某一條記錄:
修改前:

執行語句:
set @@autocommit = 0; UPDATE account set money = 700 WHERE username = 'aa'; SELECT * FROM account;
執行上面語句但未 commit,此時實際上并未持久性地作用到數據庫中。
此時我們新起一個連接,將隔離級別置為 Read Uncommitted (因為只有該級別會有臟讀,其他隔離級別不會有臟讀情況),查詢數據:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT * FROM account;
查詢結果:

可以看到,查詢到了第一個事務已修改但未提交的數據。此時如果第一個事務回滾 roolback,第二個事務又會查到 500 而不是 700 的記錄。所以說,產生了臟讀。
(注意,在同一事務里不會產生臟讀,即在同一事務里即使修改數據未 commit,此時查詢也是修改后的數據,臟讀等一系列問題只會發生在多事務并發的情況)
視頻例子可參考:https://www.liaoxuefeng.com/wiki/1177760294764384/1219071817284064
3.2、不可重復讀(虛讀,多次讀結果不同)(正常現象)
不可重復讀發生在一個事務內執行相同的查詢兩次或兩次以上,但是每次都得到不同的數據時,這通常是因為另一個并發事務在兩次查詢期間進行了更新。不可重復讀也可稱為虛讀。
例如:事務T1讀取完某一數據后,事務T2立馬修改了這個數據并且提交事務給數據庫,事務T1 又有SQL命令再次讀取該數據,此時就會發現跟之前的查詢結果不一致,這就是不可重復讀。
在某些情況下,不可重復讀并不是問題,比如我們多次查詢某個數據當然以最后查詢得到的結果為主。但在另一些情況下就有可能發生問題,例如對于同一個數據A和B依次查詢就可能不同,A和B就可能打起來了……
3.2.1、不可重復讀演示
將隔離級別設置為 Read committed 可以避免臟讀的情況,但仍然會有不可重復讀的情況。
表的初始情況:

事務1:
BEGIN; UPDATE account SET money = money +200 WHERE id = '1' COMMIT;
當執行完 update 后,我們并沒有執行 commit,即未提交修改。
事務2如下,我們將事務 2 的隔離級別設置為 READ COMMITTED(可以避免臟讀,但是不可避免不可重復讀)。
set session transaction isolation level READ COMMITTED; -- 將當前會話隔離級別設置為 READ COMMITTED BEGIN; SELECT * FROM account; SELECT * FROM account; COMMIT;
當我們執行到第一條 select 語句時,查詢結果如下:

因為事務 1 未提交,所以查詢不到事務1未提交的數據。
此時如果事務1提交了即commit,此時事務2執行到第二條 select 語句,此時查詢結果如下:

可以看到查詢到的是事務1提交后的數據。由此在一個事務內(即在事務2)內兩次查詢到的結果并不一致,這也就是我們說的不可重復讀,也就是在一個事務內多次查詢的結果不一致。
在大部分情況下,不可重復讀并不算是問題。但在某些特定情況下,在同一個事務中多次查詢結果需要保持一致,所以就需要避免不可重復讀的情況。MySQL數據庫默認的隔離級別已經可以避免不可重復讀的情況,也就是在同一事務內查詢同一數據,多次查詢得到的結果都是一致的。
視頻例子可查看:https://www.liaoxuefeng.com/wiki/1177760294764384/1245266514539200
3.3、幻讀(跟不可重復讀本質一樣,不過是讀到新增數據)
幻讀是事務非獨立執行時發生的一種現象,幻讀與不可重復讀有點類似。
例如:事務T1對一個表中所有的行的某個數據項做了從“1”修改為“2”的操作,這時事務T2又對這個表中插入了一行數據項,而這個數據項的數值還是為“1”并且提交給數據庫。而操作事務T1的用戶如果再查看剛剛修改的數據,會發現還有一行沒有修改,其實這行是從事務T2中添加的,就好像產生幻覺一樣,這就是發生了幻讀。
幻讀和不可重復讀都是讀取了另一條已經提交的事務(這點他們和臟讀不同,臟讀是讀取了未提交的事務),所不同的是不可重復讀查詢的都是同一個數據項,而幻讀針對的是一批數據整體(比如數據的個數)。不可重復讀的重點是修改,同樣的條件,你讀取過的數據,再次讀取出來發現值不一樣了。幻讀的重點在于新增或者刪除,同樣的條件,第1次和第2次讀出來的記錄數不一樣。
不可重復讀和幻讀本質是一樣的,只是概念有點區別,一個是跟修改有關,一個是跟增刪有關。不可重復讀是讀到了其他事務修改的數據,而幻讀讀到的是其他事務新增的數據。
4、數據庫提供的四種隔離級別
多個事務之間如果操作同一批數據,會引發一些問題,通過設置不同的隔離級別可以解決這些問題。
當多個線程都開啟事務操作數據庫中的數據時,數據庫系統要能進行隔離操作,以保證各個線程獲取數據的準確性。數據庫系統提供了隔離級別來讓我們有針對性地選擇事務的隔離級別,避免數據不一致的問題。
MySQL數據庫為我們提供了四種隔離級別:
- Read uncommitted (意譯:讀未提交):最低級別,任何情況都無法保證。
- Read committed (意譯:讀已提交,Oracle默認級別):只可避免臟讀的發生。
- Repeatable read (意譯:可重復讀,MySQL默認級別):可避免臟讀、不可重復讀的發生。
- Serializable (意譯:串行化):臟讀、不可重復讀、幻讀均可避免
4 種隔離級別分別對應可能會出現的數據不一致的情況如下:

上面四種隔離級別,從上到下安全級別越來越高,但效率也會越來越慢。
給事務定義了以上隔離級別,則該事務就可以避免一些數據不一致的問題。為什么說定義了這些隔離級別的事務就可以避免一些數據不一致的問題?可參考:https://blog.csdn.net/nevergiveup12345/article/details/24997461
4.1、查詢和設置隔離級別
我們可以通過以下語句查詢隔離級別:
SELECT @@tx_isolation; -- 查看當前會話的隔離級別 select @@global.tx_isolation; -- 查看全局的隔離級別
查詢結果如下:

可以通過命令行設置全局或會話的隔離級別,重啟數據庫或者退出會話對應的隔離級別將會失效。
設置隔離級別命令格式:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE} -- 當未指定設置的是會話還是全局時,默認是設置會話的隔離級別
具體命令:
# 設置全局隔離級別 set global transaction isolation level REPEATABLE READ; set global transaction isolation level READ COMMITTED; set global transaction isolation level READ UNCOMMITTED; set global transaction isolation level SERIALIZABLE; # 設置會話隔離級別 set session transaction isolation level REPEATABLE READ; set session transaction isolation level READ COMMITTED; set session transaction isolation level READ UNCOMMITTED; set session transaction isolation level SERIALIZABLE;
當同時設置了會話和全局的隔離級別時,當前會話的隔離級別的設置是由當前會話所設置的隔離級別決定的。
4.2、Read uncommitted級別(最低級別,無法避免數據不一致)
Read Uncommitted 是隔離級別最低的一種事務級別。在這種隔離級別下,一個事務會讀到另一個事務更新后但未提交的數據,如果那另一個事務進行了回滾,那么當前事務讀到的數據就是臟數據,這就是臟讀(Dirty Read)。
詳細例子可查看:https://www.liaoxuefeng.com/wiki/1177760294764384/1219071817284064
定義事務為 Read uncommitted 隔離級別:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; BEGIN; SELECT * FROM students WHERE id = 1; SELECT * FROM students WHERE id = 1; COMMIT;
4.3、Read committed(Oracle默認級別,可避免臟讀)
在Read Committed隔離級別下,一個事務可能會遇到不可重復讀(Non Repeatable Read)的問題。不可重復讀是指,在一個事務內,多次讀同一數據,在這個事務還沒有結束時,如果另一個事務恰好修改了這個數據,那么,在第一個事務中,兩次讀取的數據就可能不一致。
詳細例子可查看:https://www.liaoxuefeng.com/wiki/1177760294764384/1245266514539200
定義事務為 Read committed 隔離級別:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN; SELECT * FROM students WHERE id = 1; SELECT * FROM students WHERE id = 1; COMMIT;
4.4、Repeatable read(MySQL默認隔離級別,可避免臟讀和不可重復讀)
Repeatable Read 是 mysql 默認的隔離級別,也就是說在默認情況下,mysql 的同一個事務內不可能會出現臟讀和不可重復讀的情況,也就是在同一事務內,多次執行同一查詢語句,如果不是由本身的會話修改了數據,那么就不可能出現結果不一樣的情況,并且不可能會讀取到其他事務未提交的數據。
在Repeatable Read隔離級別下,一個事務可能會遇到幻讀(Phantom Read)的問題。幻讀是指,在一個事務中,第一次查詢某條記錄,發現沒有,但是,當試圖更新這條不存在的記錄時,竟然能成功,并且,再次讀取同一條記錄,它就神奇地出現了。
詳細例子可查看:https://www.liaoxuefeng.com/wiki/1177760294764384/1245268672511968
定義事務為 Repeatable read 隔離級別:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; SELECT * FROM students WHERE id = 99; SELECT * FROM students WHERE id = 99; UPDATE students SET name = 'Alice' WHERE id = 99; SELECT * FROM students WHERE id = 99; COMMIT;
如果沒有指定隔離級別,數據庫就會使用默認的隔離級別。在MySQL中,如果使用InnoDB,默認的隔離級別是Repeatable Read。
一般情況下,我們不會去修改數據庫的隔離級別。
默認隔離級別避免臟讀和不可重復讀的示例:
我們開啟兩個會話,并且設置為不自動提交事務(這樣才能模擬出在一個事務內的情況,否則事務立馬提交就看不出效果了),在 A 會話中修改數據但不提交 commit,可以看到在 A 會話中是可以看到修改后的數據的,但是在 B 會話中是無法看到 A 會話未提交的修改的。


這就是 mysql 默認隔離級別避免了出現臟讀的問題。
即使最終 A 會話將修改提交 commit 了,在 B 會話中同一事物內仍然不會看到 A 會話的修改。只有在 B 會話 commit 提交事務了,重新開啟一個新事務,在這個新事務里面才可以看到 A 會話的修改。這就是避免了不可重復讀的問題。
如下所示:


4.5、Serializable(最高級別,臟讀、不可重復讀、幻讀均可避免)
Serializable 是最嚴格的隔離級別。在Serializable隔離級別下,所有事務按照次序依次執行。如果一個事務在操作一個數據表,那么其他事務就無法再操作該數據表(包括查詢操作也無法正常執行完成),只有當前面的事務操作完成,后面的事務才能對該表的操作才能正常執行完成。因此在 Serializable 隔離級別下,臟讀、不可重復讀、幻讀都不會出現。
雖然 Serializable 隔離級別下的事務具有最高的安全性,但是,由于事務是串行執行,所以效率會大大下降,應用程序的性能會急劇降低。如果沒有特別重要的情景,一般都不會使用 Serializable 隔離級別。

浙公網安備 33010602011771號