微信公眾號【黃小斜】大廠程序員,互聯網行業新知,終身學習踐行者。關注后回復「Java」、「Python」、「C++」、「大數據」、「機器學習」、「算法」、「AI」、「Android」、「前端」、「iOS」、「考研」、「BAT」、「校招」、「筆試」、「面試」、「面經」、「計算機基礎」、「LeetCode」 等關鍵字可以獲取對應的免費學習資料。
事務的概念
事務指邏輯上的一組操作,組成這組操作的各個單元,要不全部成功,要不全部不成功。
例如:A向B轉賬100元,對應於如下兩條sql語句:
update from account set money=money+100 where name='b'; update from account set money=money-100 where name='a';
- 1
- 2
- 1
- 2
數據庫默認事務是自動提交的,也就是發一條sql它就執行一條,如果想多條sql放在一個事務中執行,則需要使用如下語句:
start transaction … … commit
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
數據庫開啟事務命令:
start transaction
:開啟事務rollback
:回滾事務commit
:提交事務
MySQL數據庫中操作事務命令
編寫測試SQL腳本,如下:
/* 創建數據庫 */ create database day16; use day16; /* 創建賬戶表 */ create table account ( id int primary key auto_increment, name varchar(40), money float ) character set utf8 collate utf8_general_ci; /* 插入測試數據 */ insert into account(name,money) values('aaa',1000); insert into account(name,money) values('bbb',1000); insert into account(name,money) values('ccc',1000);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
下面我們在MySQL數據庫中模擬aaa向bbb轉帳這個業務場景。
-
開啟事務(start transaction)
使用”start transaction”開啟MySQL數據庫的事務,如下所示:
我們首先在數據庫中模擬轉賬失敗的場景,首先執行update語句讓aaa用戶的money減少100塊錢,如下圖所示:
現在假設程序拋出異常,也即該鏈接斷了,代碼塊沒有完成,此時數據庫會自動回滾掉此sql語句造成的影響,也就是說這條sql語句沒有執行。我們現在就來模擬這種情況,我們關閉當前操作的dos命令行窗口,這樣就導致了剛才執行的update語句的數據庫的事務沒有被提交,那么我們對aaa用戶的修改就不算是真正的修改了,下次在查詢aaa用戶的money時,依然還是之前的1000,如下圖所示: -
提交事務(commit)
下面我們在數據庫模擬aaa向bbb轉賬成功的場景。
我們手動提交(commit)數據庫事務之后,aaa向bbb轉賬100塊錢的這個業務操作算是真正成功了,aaa賬戶中少了100,bbb賬戶中多了100。 -
回滾事務(rollback)
通過手動回滾事務,讓所有的操作都失效,這樣數據就會回到最初的初始狀態!
JDBC中使用事務
當Jdbc程序向數據庫獲得一個Connection對象時,默認情況下這個Connection對象會自動向數據庫提交在它上面發送的SQL語句。若想關閉這種默認提交方式,讓多條SQL在一個事務中執行,可使用下列的JDBC控制事務語句:
- Connection.setAutoCommit(false); //開啟事務(start transaction)
- Connection.rollback(); //回滾事務(rollback)
- Connection.commit(); //提交事務(commit)
JDBC使用事務范例
在JDBC代碼中演示銀行轉帳案例,使如下轉帳操作在同一事務中執行:
update from account set money=money-100 where name=‘aaa’; update from account set money=money+100 where name=‘bbb’;
- 1
- 2
- 1
- 2
-
模擬aaa向bbb轉賬成功時的業務場景
public class Demo1 { /* * a--->b轉100元 */ public static void main(String[] args) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); // 相當於start transaction,開啟事務 String sql1 = "update account set money=money-100 where name='aaa'"; String sql2 = "update account set money=money+100 where name='bbb'"; st = conn.prepareStatement(sql1); st.executeUpdate(); st = conn.prepareStatement(sql2); st.executeUpdate(); conn.commit(); } finally { JdbcUtils.release(conn, st, rs); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
-
模擬aaa向bbb轉賬過程中出現異常導致有一部分SQL執行失敗后讓數據庫自動回滾事務
public class Demo1 { /* * a--->b轉100元 */ public static void main(String[] args) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); // 相當於start transaction,開啟事務 String sql1 = "update account set money=money-100 where name='aaa'"; String sql2 = "update account set money=money+100 where name='bbb'"; st = conn.prepareStatement(sql1); st.executeUpdate(); int x = 1/0; // 程序運行到這個地方拋異常,后面的代碼就不執行,數據庫沒有收到commit命令 st = conn.prepareStatement(sql2); st.executeUpdate(); conn.commit(); } finally { JdbcUtils.release(conn, st, rs); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
-
模擬aaa向bbb轉賬過程中出現異常導致有一部分SQL執行失敗時手動通知數據庫回滾事務
public class Demo1 { /* * a--->b轉100元 */ public static void main(String[] args) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); // 相當於start transaction,開啟事務 String sql1 = "update account set money=money-100 where name='aaa'"; String sql2 = "update account set money=money+100 where name='bbb'"; st = conn.prepareStatement(sql1); st.executeUpdate(); int x = 1/0; // 程序運行到這個地方拋異常,后面的代碼就不執行,數據庫沒有收到commit命令 st = conn.prepareStatement(sql2); st.executeUpdate(); conn.commit(); } catch (Exception e) { e.printStackTrace(); conn.rollback(); // 捕獲到異常之后手動通知數據庫執行回滾事務的操作 } finally { JdbcUtils.release(conn, st, rs); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
設置事務回滾點
在開發中,有時候可能需要手動設置事務的回滾點,在JDBC中使用如下的語句設置事務回滾點:
Savepoint sp = conn.setSavepoint(); Conn.rollback(sp); Conn.commit(); // 回滾后必須通知數據庫提交事務
- 1
- 2
- 3
- 1
- 2
- 3
設置事務回滾點范例:
public class Demo2 { // 事務回滾點概念 public static void main(String[] args) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; Savepoint sp = null; try { conn = JdbcUtils.getConnection(); // MySQL默認的隔離級別——REPEATABLE-READ,並且是嚴格遵循數據庫規范設計的,即支持4種隔離級別 // Oracle默認的隔離級別——Read committed,並且不支持這4種隔離級別,只支持這4種隔離級別中的2種,Read committed和Serializable // conn.setTransactionIsolation(); // 相當於設置CMD窗口的隔離級別 conn.setAutoCommit(false); // 相當於start transaction,開啟事務 // 不符合實際需求 String sql1 = "update account set money=money-100 where name='aaa'"; String sql2 = "update account set money=money+100 where name='bbb'"; String sql3 = "update account set money=money+100 where name='ccc'"; st = conn.prepareStatement(sql1); st.executeUpdate(); /* * 只希望回滾掉這一條sql語句,上面那條sql語句讓其執行成功 * 這時可設置事務回滾點 */ sp = conn.setSavepoint(); st = conn.prepareStatement(sql2); st.executeUpdate(); int x = 1/0; // 程序運行到這個地方拋異常,后面的代碼就不執行,數據庫沒有收到commit命令 st = conn.prepareStatement(sql3); st.executeUpdate(); conn.commit(); } catch (Exception e) { e.printStackTrace(); conn.rollback(sp); // 回滾到sp點,sp點上面的sql語句發給數據庫執行,由於數據庫沒收到commit命令,數據庫又會自動將這條sql語句的影響回滾掉,所以回滾完,一定要記得commit命令。 conn.commit(); // 手動回滾后,一定要記得提交事務 } finally { JdbcUtils.release(conn, st, rs); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
事務的四大特性(ACID)
- 原子性(Atomicity)
原子性是指事務是一個不可分割的工作單位,事務中的操作要么全部成功,要么全部失敗。比如在同一個事務中的SQL語句,要么全部執行成功,要么全部執行失敗。 - 一致性(Consistency)
官網上事務一致性的概念是:事務必須使數據庫從一個一致性狀態變換到另外一個一致性狀態。還要一種說法是事務前后數據的完整性必須保持一致。以轉賬為例子,A向B轉賬,假設轉賬之前這兩個用戶的錢加起來總共是2000,那么A向B轉賬之后,不管這兩個賬戶怎么轉,A用戶的錢和B用戶的錢加起來的總額還是2000,這個就是事務的一致性。 - 隔離性(Isolation)
事務的隔離性是多個用戶並發訪問數據庫時,數據庫為每一個用戶開啟的事務,不能被其他事務的操作數據所干擾,多個並發事務之間要相互隔離。 - 持久性(Durability)
持久性是指一個事務一旦被提交,它對數據庫中數據的改變就是永久性的,接下來即使數據庫發生故障也不應該對其有任何影響。
事務的隔離級別
事務的四大特性中最麻煩的是隔離性,下面重點介紹一下事務的隔離級別。
多個線程開啟各自事務操作數據庫中數據時,數據庫系統要負責隔離操作,以保證各個線程在獲取數據時的准確性。
事務不考慮隔離性可能會引發的問題
如果事務不考慮隔離性,可能會引發如下問題:
-
臟讀
指一個事務讀取了另外一個事務未提交的數據。
這是非常危險的,假設a向b轉帳100元,對應sql語句如下所示:1.update account set money=money+100 while name=‘b’; 2.update account set money=money-100 while name=‘a’;
- 1
- 2
- 3
- 1
- 2
- 3
當第1條sql執行完,第2條還沒執行(a未提交時),如果此時b查詢自己的帳戶,就會發現自己多了100元錢。如果a等b走后再回滾,b就會損失100元。
- 不可重復讀
在一個事務內讀取表中的某一行數據,多次讀取結果不同。(一個事務讀取到了另外一個事務提交的數據)
例如銀行想查詢a帳戶余額,第一次查詢a帳戶為200元,此時a向帳戶內存了100元並提交了,銀行接着又進行了一次查詢,此時a帳戶為300元了。銀行兩次查詢不一致,可能就會很困惑,不知道哪次查詢是准的。可將例子簡化為:讀表中某一行數據,例如a賬戶第一次讀為1000,第二次讀為1100。
不可重復讀和臟讀的區別是,臟讀是讀取前一事務未提交的臟數據,不可重復讀是重新讀取了前一事務已提交的數據。
很多人認為這種情況就對了,無須困惑,當然是以后面的結果為准了。我們可以考慮這樣一種情況,比如銀行程序需要將查詢結果分別輸出到電腦屏幕和寫到文件中,結果在一個事務中針對輸出的目的地,進行的兩次查詢不一致,導致文件和屏幕中的結果不一致,銀行工作人員就不知道以哪個為准了。 - 虛讀(幻讀)
虛讀(幻讀)是指在一個事務內讀取到了別的事務插入的數據,導致前后讀取不一致。
如丙存款100元未提交,這時銀行做報表統計account表中所有用戶的總額為500元,然后丙提交了,這時銀行再統計發現帳戶為600元了,造成虛讀同樣會使銀行不知所措,到底以哪個為准。可將例子簡化為:讀整個表,即表的行數,例如第一次讀某個表有3條記錄,第二次讀該表又有4條記錄。
數據庫共定義了四種隔離級別,應用《高性能mysql》一書中有說明:
- Serializable(串行化):可避免臟讀、不可重復讀、虛讀情況的發生。
- Repeatable read(可重復讀):可避免臟讀、不可重復讀情況的發生。
- Read committed(讀已提交):可避免臟讀情況發生。
- Read uncommitted(讀未提交):最低級別,以上情況均無法保證。
總結:在MySQL中,實現了這四種隔離級別,分別有可能產生問題如下所示:
下面說說修改事務隔離級別的方法:
-
全局修改,修改my.ini(或mysql.ini)配置文件,在最后加上
#可選參數有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE. [mysqld] transaction-isolation = REPEATABLE-READ
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
注意:MySQL默認的隔離級別為REPEATABLE-READ,並且是嚴格遵循數據庫規范設計的,即支持4種隔離級別;Oracle默認的隔離級別為Read committed,並且不支持這4種隔離級別,只支持這4種隔離級別中的2種,Read committed和Serializable。
-
對當前session修改,在登錄mysql客戶端后,執行命令:
set session transaction isolation level read uncommitted; // 設置當前事務隔離級別
- 1
- 1
注意:session是不能掉的,不然你設置不會成功,MySQL的隔離級別還是默認的隔離級別——REPEATABLE-READ,如下所示:
查詢當前事務隔離級:
select @@tx_isolation; // 查詢當前事務隔離級別
- 1
- 1
下面,將利用MySQL的客戶端程序,分別測試幾種隔離級別。測試數據庫為day16,表為account;表如下:
兩個命令行客戶端分別為a(黑色背景窗口),b(藍色背景窗口);不斷改變b的隔離級別,在a端修改數據。
- 將b的隔離級別設置為read uncommitted(未提交讀)
在a未更新數據之前,b客戶端
a更新數據,a向b轉帳100元
此時b查詢自己的帳戶,就會發現自己多了100元錢,出現了臟讀(這個事務讀取到了別的事務未提交的數據)
如果a等b走后再回滾
此時b查詢自己的帳戶,發現又少掉了100元錢,兩次讀取的數據不一樣,出現不可重復讀現象
a提交完事務,再開啟一個事務,向表account中新增一條記錄
此時b再次查詢account表,發現表account中多了一條記錄,出現幻讀現象 - 將客戶端b的事務隔離級別設置為read committed(已提交讀)
在a未更新數據之前,b客戶端
a更新數據,a向b轉帳100元
b查詢自己的帳戶,金額沒有發生任何變化,說明已提交讀隔離級別解決了臟讀的問題
a此刻提交事務
b再次查詢自己的帳戶,發現自己又多了100元錢,這時就發生不可重復讀(指這個事務讀取到了別的事務提交的數據)
a再開啟一個事務,向表account中新增一條記錄
然后b再次查詢account表,發現表account中多了一條記錄,出現幻讀現象 - 將b的隔離級別設置為repeatable read(可重復讀)
在a未更新數據之前,b客戶端
a更新數據,a向b轉帳100元
b查詢自己的帳戶,金額沒有發生任何變化,這說明repeatable read這種級別可避免臟讀
a此刻提交事務
b再次查詢自己的帳戶,金額沒有發生任何變化,這說明repeatable read這種級別還可以避免不可重復讀
a再開啟一個事務,向表account中新增一條記錄
然后b再次查詢account表,發現表中可能會多出一條ddd的記錄(也有可能不會多出一條ddd的記錄,我測試時就是這種情況),這就發生了虛讀,也就是在這個事務內讀取了別的事務插入的數據(幻影數據) - 將b的隔離級別設置為可串行化 (Serializable)
為可串行化 (Serializable)均可避免臟讀、不可重復讀、幻讀。避免臟讀和不可重復讀的情況我就不測試了,測試步驟同上,下面我重點講解可串行化 (Serializable)避免幻讀的情況。
事務b端
事務a端
因為此時事務b的隔離級別設置為serializable,開始事務后,並沒有提交,所以事務a只能等待。
事務b提交事務,事務b端
事務a端
serializable完全鎖定字段,若一個事務來查詢同一份數據就必須等待,直到前一個事務完成並解除鎖定為止,是完整的隔離級別,會鎖定對應的數據表格,因而會有效率的問題。
結論:Serializable隔離級別,雖然可避免所有問題,但性能、效率是最低的,原因是它采取的是鎖表的方式,即單線程的方式,即有一個事務來操作這個表了,另外一個事務只能等在外面進不來。
下面,將利用Java程序來測試Serializable隔離級別。
public class Demo3 { public static void main(String[] args) throws SQLException, InterruptedException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; Savepoint sp = null; try { conn = JdbcUtils.getConnection(); conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); // 相當於設置CMD窗口的隔離級別 conn.setAutoCommit(false); String sql = "select * from account"; conn.prepareStatement(sql).executeQuery(); // 故意讓程序睡眠20秒,睡眠20秒之后事務才結束,程序運行完 Thread.sleep(1000*20); conn.commit(); } finally { JdbcUtils.release(conn, st, rs); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
程序運行,同時在客戶端開啟一個事務,插入一條記錄,需要等待一段時間才能插入進去。