本篇文章主要介紹的是 MySQL / JDBC 中的事務,為了方便讀者瀏覽,這里默認需要讀者已經掌握 SQL基礎 以及 JDBC 數據庫連接基礎。這部分的基礎也可以參考下面的鏈接進行簡單的快速入門。
1.概述
MySQL 事務主要用于處理操作量大,復雜度高的數據。比如說,在銀行轉賬系統中,A -> B 轉賬 1000 元,這時就需要將 A 賬戶余額 -1000,對應的 B 賬戶余額 +1000。這兩個操作過程必須同時執行成功才能完成此操作,這樣,這些數據庫操作語句就構成了一個事務。
- 在上面的舉例中如果 A 的賬戶余額 -1000 執行完畢后,程序被中斷了(拋出異常、服務器宕機等),而 B 賬戶沒有 +1000 元,這肯定是有問題的。
- 現在對事務應該有一個了解了吧??事務中的多個操作,或者全部執行完畢,或者全不執行,不存在只執行了一部分的情況。
2.事務的四大特性(ACID)
- 原子性(Atomicity):事務中的所有操作是不可再分割的原子單位,事務中的所有操作是一個整體,或者整體執行成功,亦或者整體執行失敗。
- 一致性(Consistency):事務執行后,數據庫狀態與其他業務規則保持一致。如轉賬業務,無論執行成功與否,參與轉賬的兩個帳號余額值和應該是不變的。
- 隔離性(Isolation):在并發操作中,不同事務之間應該隔離開來,每個并發中的事務的執行不會相互干擾。
- 持久性(Durability):一旦事務提交成功,事務中的所有數據更新必須被持久化到數據庫中,即使提交事務后,數據庫馬上崩潰,在數據庫重新啟動時,也必須能保證通過某種機制恢復數據。
3.MySQL 中的事務
在默認情況下,MySQL 每執行一條 SQL 語句,都是一個單獨的事務。如果需要在每一個事務中包含多條 SQL 語句的執行,那么就需要開啟事務和結束事務。
- 開啟事務:
START TRANSACTION
- 結束事務:
COMMIT
或ROLLBACK
- 在執行 SQL 語句之前,先執行
START TRANSACTION
,則代表開啟了一個事務,然后執行多條 SQL 語句,最后需要結束事務,COMMIT
表示提交,即事務中的多條 SQL 語句所更改的數據會持久化到數據庫中。或者ROLLBACK
表示回滾,即回滾到事務的起點,將之前所做的所有操作撤銷。
Reiminder ???♂?
ROLLBACK
可以結束事務,但不代表會將數據持久化到數據庫中,而只有COMMIT
提交才可以將數據持久化到數據庫中。
- 測試表:
# 創建 Account 表
CREATE TABLE `Account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
`balance` decimal(10,0) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `Account_id_uindex` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
# 插入數據
INSERT INTO Account (name, balance) VALUES ('A', 10000);
INSERT INTO Account (name, balance) VALUES ('B', 10000);
INSERT INTO Account (name, balance) VALUES ('C', 10000);
3.1 - COMMIT 測試
# 開啟事務
START TRANSACTION;
# 執行事務 SQL 語句
# SQL1
UPDATE Account
SET balance = balance - 1000
WHERE id = 1;
# SQL2
UPDATE Account
SET balance = balance + 1000
WHERE id = 2;
# 提交事務
COMMIT;
# 結果分析
1 A 9000
2 B 11000
3 C 10000
分析:提交事務后,更新的數據將被持久化到數據庫中。
3.2 - ROOLBACK 測試
# 開啟事務
START TRANSACTION;
# 執行事務 SQL 語句
# SQL1
UPDATE Account
SET balance = balance - 1000
WHERE id = 1;
# SQL2
UPDATE Account
SET balance = balance + 1000
WHERE id = 2;
# 回滾事務
ROLLBACK;
# 提交事務
COMMIT;
# 結果分析
1 A 10000
2 B 10000
3 C 10000
分析:事務提交前執行 ROLLBACK 回滾事務至 START TRANSACTION 時的狀態,所以持久化后數據庫中數據沒有被改變。
3.3 - 事務不提交測試
# 開啟事務
START TRANSACTION;
# 執行事務 SQL 語句
# SQL1
UPDATE Account
SET balance = balance - 1000
WHERE id = 1;
# SQL2
UPDATE Account
SET balance = balance + 1000
WHERE id = 2;
# 輸出結果
SELECT *
FROM Account;
# 控制臺打印數據
1 A 9000
2 B 11000
3 C 10000
# 結果分析(數據庫數據)
1 A 10000
2 B 10000
3 C 10000
分析:在執行了 SQL 語句后,在內存中的數據表數據已經被修改了,但是由于沒有提交事務,所以數據沒有被持久化到數據庫中。
4.并發事務問題
- 臟讀(Dirty Read):在事務的執行過程中,讀取到了其他事務的 未提交 的數據,即讀到了臟數據。
- 不可重復讀(Unrepeatable Read):在事務的執行過程中,讀到了其他事務 修改后 的數據,換句話說在該事務中的不同時間點讀取到了不一致的數據,即不可重復讀。
- 幻讀/虛讀(Phantom Read):在事務的執行過程中,讀取到了其他事務對 記錄數 修改后的數據,對同一張表的兩次查詢的
COUNT(*)
不一致。 - 不可重復讀與幻讀的區別:
- 不可重復讀:強調的是數據 內容 的不一致,主要針對
UPDATE
的修改。 - 幻讀:強調的是 記錄數 的不一致,主要針對
INSERT
/DELETE
的修改。
- 不可重復讀:強調的是數據 內容 的不一致,主要針對
5.四大隔離級別
剛剛我們介紹了事務并發時可能出現的各種問題,其實可以發現是違背了事務的 隔離性 的要求所引起的,所以我們需要通過事務的隔離來解決這個問題,下面我們就來介紹一下事務的四大隔離級別。
- 串行化(SERIALIZABLE):
- 概述:對數據串行的訪問,非并發訪問。
- 特點:不會出現任何并發問題,性能最差。
- 理解:在當前串行化事務中,如果有其他事務對數據進行了增刪改操作,當前事務讀取數據會被阻塞,需要等到其他事務結束后(ROLLBACK/COMMIT)才能執行數據讀取。
- 可重復讀(REPEATABLE READ):
- 概述:在一個事務的執行過程中,能保證讀取到數據的一致性,是 MySQL 中使用的 InnoDB 存儲引擎默認的隔離級別。
- 特點:可避免臟讀和不可重復讀,不能避免幻讀問題,并發式讀取訪問,性能比串行化好。
- 理解:在一個事務內,鎖定讀取,通過保存第一次讀取的快照(snapshot),保證每次讀取的數據一致。
- 讀已提交(READ COMMITTED):
- 概述:在一個事務內,可以讀取到其他事務已經提交的數據,性能優于可重復讀(REPEATABLE READ),Oracle 數據庫中的默認隔離級別。
- 特點:可避免臟讀,不能避免不可重復讀和幻讀問題,并發式訪問讀取,性能比可重復讀好。
- 理解:MySQL中與 Oracle 的該隔離級別,通過讀取新鮮的快照(fresh snapshot)來讀取其他事務已提交的更新內容。
- 讀未提交(READ UNCOMMITTED):
- 概述:在一個事務內,可以讀取到其他事務沒有提交的修改內容,即臟讀(Dirty Read)。
- 特點:不能避免任何并發事務的問題,性能最好。
- 理解:在 SERIALIZBLE 的事務隔離級別,InnoDB 存儲引擎會對每個 SELECT 語句后自動加上 LOCK IN SHARE MODE,即給每個讀取操作加一個共享鎖,因此在這個事務隔離級別下,讀占用鎖了,一致性的非鎖定讀不再予以支持,一般不會在本地事務中使用 SERIALIZBLE 的隔離級別,SERIALIZABLE 的事務隔離級別主要用于 InnoDB 存儲引擎的分布式事務。
隔離級別 | 臟讀 | 不可重復讀 | 幻讀 |
---|---|---|---|
串行化(SERIALIZABLE) | ?? | ?? | ?? |
可重復讀(REPEATABLE READ) | ?? | ?? | - |
讀已提交(READ COMMITTED) | ?? | ? | ? |
讀未提交(READ UNCOMMITTED) | ? | ? | ? |
6.MySQL 各隔離級別的并發事務測試
查看隔離級別:MySQL 默認隔離級別是
REPEATABLE-READ
,可以通過SELECT @@TX_ISOLATION;
查看隔離級別。設置隔離級別:
SET SESSION TRANSACTION ISOLATION LEVEL xxx;
測試表及數據
id | name | balance |
---|---|---|
1 | A | 10000 |
2 | B | 10000 |
6.1 - 串行化測試
- 測試版本:
MySQL Server 5.7
- 測試環境:
# 設置窗口 2 隔離級別為 串行化
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
# 特別的 -> 窗口 1 的隔離級別不需要特別設置。
# 我們演示是通過窗口 1 進行修改數據值,在窗口 2 來觀察結果的。
值得注意??在第 3 步,窗口 1 執行 INSERT 插入了一條數據,而后第 4 步窗口 2 執行 SELECT 操作會被阻塞(避免幻讀),直到窗口 1 事務結束(COLLBACK/COMMIT)后才會被執行。
特別的??當窗口 2 一旦執行過 SELECT 操作后,如果有其他事務對數據進行增刪改操作都將被阻塞(可重復讀的保證),直到該串行化事務結束后才會被執行。
6.2 - 可重復讀測試
與串行化類似的是,當窗口 2 執行步驟 3 讀操作后,查詢的結果將被鎖定。當其他事務要對該鎖定數據執行更改操作時都將會被阻塞,所以當窗口 1 執行步驟 4 時將會被阻塞,從而保證了可重復讀。
6.3 - 讀已提交測試
當步驟 4 修改了
balance
值時,此時還未提交,所以步驟 5 查詢到的結果并沒有改變(讀已提交),而在步驟 7 查詢到了窗口 1 改變的結果,因為此時窗口 1 的事務已經提交。
特別的??在窗口 2 事務的執行過程中,步驟 3 與步驟 7 查詢到了不同的結果,由此可以看出這是與可重復讀的重要區別。
6.4 - 讀未提交測試
步驟 4 中窗口 1 事務修改了數據,步驟 5 中窗口 2 事務讀取到了修改后的數據,此時窗口 1 事務還未提交,因此讀取到的是 臟數據,該隔離級別不能避免任何的并發事務問題。
7.JDBC 事務
剛剛我們介紹了在 MySQL 中對事務進行的操作,而 JDBC 中 也必然有與對應的方式進行事務控制,下面我們介紹一下 JDBC 中對事務的控制。
- 在 JDBC 中處理事務都是通過 Connection 完成的。
- 同一個事務中的所有的操作,都是使用同一個 Connection 對象。
7.1 - 開啟事務
- 方法:
void setAutoCommit(boolean autoCommit)
讀讀 API ??
If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either the method commit or the method rollback. By default, new connections are in auto-commit mode.
- 如果 connection 處于自動提交模式,會將每一條 SQL 語句作為一個單獨的事務提交(commit);否則,其 SQL 語句可以通過調用
commit()
方法或rollback()
方法終止事務。默認是自動提交模式。
Reminder ???♂?
Java 還特別指出:對于 DML 語句,例如插入、 更新或刪除和 DDL 語句,該語句是完整的盡快它執行完。
Select 語句,該語句完成時關閉關聯的 ResultSet。
7.2 - 提交事務
- 方法:
commit()
讀讀 API ??
Makes all changes made since the previous commit/rollback permanent and releases any database locks currently held by this Connection object. This method should be used only when auto-commit mode has been disabled.
- 提交自上次提交后的所有更改,并釋放目前此連接對象的任何 數據庫鎖。
- 只有當禁用了自動提交時此方法有效。
7.3 - 回滾事務
- 方法:
rollback()
讀讀 API ??
Undoes all changes made in the current transaction and releases any database locks currently held by this Connection object. This method should be used only when auto-commit mode has been disabled.
- 撤銷對當前事務中所做的所有更改,并釋放目前此連接對象持有的任何 數據庫鎖。
- 只有當禁用了自動提交時此方法有效。
7.4 - 設置保存點
- 方法:
Savepoint setSavepoint(String name)
讀讀 API ??
Creates a savepoint with the given name in the current transaction and returns the new Savepoint object that represents it.
if setSavepoint is invoked outside of an active transaction, a transaction will be started at this newly created savepoint.
- 在當前事務中創建一個指定名稱的保存點,并返回一個用來表示它的新的保存點對象。
- 如果該方法在一個事務外被調用時,將在這個新創建的保存點啟動事務。
7.5 - 事務回滾
- 不帶保存點的 JDBC 事務的基本格式:
try {
connection.setAutoCommit(false); // 禁用自動提交
...
...
connection.commit(); // 在 try 的末尾提交
} catch() {
connection.rollback(); // 事務執行中斷則回滾
}
- 代碼示例:
public static void transfer(boolean b) throws Throwable {
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = JdbcUtils.getConnection();
// 禁用自動提交
connection.setAutoCommit(false);
String sql = "UPDATE Account SET balance = balance + ? WHERE id = ?";
preparedStatement = connection.prepareStatement(sql);
// 操作 1
preparedStatement.setDouble(1, -10000);
preparedStatement.setInt(2, 1);
preparedStatement.executeUpdate();
// 在事務的兩個操作中拋出異常,中斷事務內務的執行
if (b) {
throw new Exception();
}
// 操作 2
preparedStatement.setDouble(1, 10000);
preparedStatement.setInt(2, 2);
preparedStatement.executeUpdate();
// 提交事務
connection.commit();
} catch (Exception e) {
try {
if (connection != null) {
connection.rollback();
}
} catch (SQLException e1) {
e1.printStackTrace();
}
throw new RuntimeException();
} finally {
JdbcUtils.release(connection, preparedStatement);
}
}
7.6 - 回滾到保存點
- 概述:保存點(savePoint) 是 JDBC 3.0 的 API,其要求數據庫支持以保存點方式的的回滾。
- 檢查方法:
boolean b = connection.getMetaData().supportsSavepoints();
- 回滾到保存點方法:
void rollback(Savepoint savepoint)
- 作用:保存點的作用是將事務回滾到指定的保存點。需要在事務中先設置好保存點,然后回滾時通過
Savepoint
回滾到指定的保存點,而不是回滾整個事務。
Reminder ???♂?
回滾到指定的保存點并沒有結束事務,只有回滾了整個事務才會結束事務。
- 代碼示例:
/*
* 李四對張三說,如果你給我轉1W,我就給你轉100W。
* ==========================================
*
* 張三給李四轉1W(張三減去1W,李四加上1W)
* 設置保存點!
* 李四給張三轉100W(李四減去100W,張三加上100W)
* 查看李四余額為負數,那么回滾到保存點。
* 提交事務
*/
private static void savepoint() throws RuntimeException {
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = JdbcUtils.getConnection();
// 禁用自動提交
connection.setAutoCommit(false);
String sql = "UPDATE Account SET balance = balance + ? WHERE name = ?";
preparedStatement = connection.prepareStatement(sql);
// 操作1(張三減去1W)
preparedStatement.setDouble(1, -10000);
preparedStatement.setString(2, "zs");
preparedStatement.executeUpdate();
// 操作2(李四加上1W)
preparedStatement.setDouble(1, 10000);
preparedStatement.setString(2, "ls");
preparedStatement.executeUpdate();
// 設置表存點
Savepoint savepoint = connection.setSavepoint();
// 操作3(李四減去100W)
preparedStatement.setDouble(1, -1000000);
preparedStatement.setString(2, "ls");
preparedStatement.executeUpdate();
// 操作4(張三加上100W)
preparedStatement.setDouble(1, 1000000);
preparedStatement.setString(2, "zs");
preparedStatement.executeUpdate();
// 操作5(查看李四余額)
sql = "SELECT balance FROM Account WHERE name = ?";
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, "ls");
ResultSet resultSet = preparedStatement.executeQuery();
double balance = 0;
if (resultSet.next()) {
balance = resultSet.getDouble("balance");
}
// 如果李四的余額為負數,那么回滾到指定保存點
if (balance < 0) {
connection.rollback(savepoint);
System.out.println("張三你上當了");
}
// 提交事務
connection.commit();
} catch (SQLException e) {
// 回滾事務
if (connection != null) {
try {
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
throw new RuntimeException();
} finally {
JdbcUtils.release(connection, preparedStatement);
}
}
悄悄話 ??
- 年后的學習節奏變得非常之快,導致最近也很久沒有與大家分享技術筆記了,最近學習了 JavaWeb 的 HTML、CSS、JS、MySQL、Tomcat、Servlet、JSP 等等內容,哪一個技術拿出來都應該可以讓我來研究一陣子了,怎奈進度太快也只能是抓大放小。最近在數據庫階段的事務控制的部分我比較感興趣并做了一些小實驗,覺得有一些意義,所以來與大家分享一下。
- 后面的 Cookie、Session 技術也是我覺得理解的比較深入的一個技術點,我會留在下次的更新中進行分享。
彩蛋 ??
-
最近在整理一些 JavaWeb 成長之路 的一些學習筆記,本篇是 Database 系列中的一篇,今后還會與大家分享 JavaWeb 中的一系列的技術,有興趣的朋友可以關注我的專題,一同學習。
如果你覺得我的分享對你有幫助的話,請在下面??隨手點個喜歡 ??,你的肯定才是我最大的動力,感謝。