1. 程式人生 > >說說資料庫事務

說說資料庫事務

多條 SQL 語句,要麼全部執行成功,要麼全部執行失敗。

1 特性

資料庫事務必須同時滿足 4 個特性 ( ACID )。

特性 說明
原子性 Atomic 表示組成一個事務的多次資料庫操作是一個不可分割的原子單元,只有所有的操作都執行成功,才提交整個事務 。 事務中的任何一次資料庫操作失敗,已經執行操作都必須回滾,讓資料庫返回到操作前的狀態 。
一致性 Consistency 事務操作後,資料庫所處的狀態和它的業務規則是一致的 。比如 A 賬戶轉賬到 B 賬戶,不管操作是否異常, A 賬戶與 B 賬戶的總額是不變的。
隔離性 Isolation 在併發操作資料時,不同的事務擁有各自的資料空間,它們的操作既可能地不對對方產生干擾。資料庫規定了多種事務隔離級別,不同的隔離級別對應不同的干擾程度 。 隔離級別越高,資料一致性越好,但併發性越差。
永續性 Durability 一旦事務提交成功,事務中所有的資料都必須被持久化到資料庫中 。 即使在提交事務後資料庫發生崩潰,那麼當資料庫重啟時,也必須保證能夠根據日誌恢復資料 。

在這些事務特性中,資料的 “ 一致性 ” 是最終目標, 其他特性都是為了達到這個目標而採取的措施或要求。

資料庫管理系統採用資料庫鎖來保證事物的隔離性,當多個事務試圖對相同的資料執行操作時,只有持有鎖的事務才能真正操作資料。

Oracle 採用了資料版本機制,在回滾階段為資料的每一種變化都保留了一個版本,修改資料不會影響讀取資料 。

2 併發問題

資料庫中的相同資料,可能同時被多個事務所訪問。所以,如果沒有采取必要的隔離措施,就會導致各種併發問題,從而破壞資料的完整性 。

併發問題可以歸結為 5 類,包括 3 類資料讀問題(髒讀 、 不可重複度 、 幻讀)和 2 類資料更新問題(第一類丟失更新和第二類丟失更新)。

2.1 髒讀(dirty read)

A 事務讀取了 B 事務尚未提交的更改資料,並在此資料的基礎上進行操作 。 如果此時 B 事務回滾,那麼 A 事務之前讀到的資料就是髒資料。

時間序列 事務 A 事務 B
1 開始事務 開始事務
2 - 查詢賬戶餘額(100 元)
3 - 取出 50 元
4 查詢賬戶餘額(50 元)【髒讀】 -
5 - 回滾事務(賬戶餘額:100 元)
6 存入 100 元 -
7 提交事務(賬戶餘額:150 元) -

這裡因為發生髒讀,導致賬戶損失了 50 元(事務 A 存款 100 元,事務 B 無影響,再加上原來的賬戶餘額 100 元,最後的賬戶餘額應該是 200 元才是)。

2.2 不可重複讀(unrepeatable read)

不可重複讀指的是事務在不同的時間點,讀取到的資料不同。

時間序列 事務 A 事務 B
1 開始事務 開始事務
2 - 查詢賬戶餘額(100 元)
3 查詢賬戶餘額(100 元) -
4 - 取款 10 元
5 - 提交事務(賬戶餘額:90 元)
6 查詢賬戶餘額(90 元) -

在時間序列 6,與在時間序列 3 時查詢到的餘額不同,發生不可重複讀現象。

2.3 幻讀(phantom read)

幻象讀一般發生在計算統計資料的事務中 。 A 事務讀取了 B 事務提交的新增資料,這時 A 事務將出現幻象讀的問題 。

假設在同一個事務中,兩次統計名某銀行支行所有賬戶的總金額,在兩次統計過程中,剛好新增了一個存款賬戶 。那麼,這兩次統計的總金額肯定會不一致 。

時間序列 事務 A 事務 B
1 開始事務 開始事務
2 統計(總金額:10 w) -
3 - 新增存款賬戶(金額:1 w)
4 - 提交事務(總金額:11 w)
5 統計(總金額:11 w)幻讀 -

2.4 不可重複讀與幻讀比較

比較 不可重複讀 幻讀
讀取物件 讀到其它事務已經提交的修改或刪除資料。 讀到其它事務已經提交的新增資料。
採取措施 對所要操作的資料新增級鎖,避免這些資料發生變化。 對所要操作的資料所在表新增級鎖,即將整張表鎖定(在 Oracle 中,是以多版本資料的方式實現的)。

2.5 第一類丟失更新

A 事務回滾時,把 B 事務中已經提交的更新資料給覆蓋咯 。

時間序列 事務 A 事務 B
1 開始事務 開始事務
2 查詢賬戶餘額(100 元) -
3 - 查詢賬戶餘額(100 元)
4 - 取款 10 元
5 - 提交事務(賬戶餘額:90 元)
6 存入 10 元 -
7 提交事務(賬戶餘額:110 元) -

這個問題影響很大。這個例子中,賬戶餘額應該還是 100 元(取款 10 元,存入 10 元,實際對賬戶無影響),但因為存在第一類丟失更新,導致銀行損失 10 元。如果事務 A 先提交,那麼賬戶將損失 10 元。

2.6 第二類丟失更新

A 事務提交後覆蓋了 B 事務已經提交的資料,導致 B 事務所做操作丟失。

時間序列 事務 A 事務 B
1 開始事務 開始事務
2 - 查詢賬戶餘額:100 元
3 查詢賬戶餘額:100 元 -
4 - 取款 10 元
5 - 提交事務(賬戶餘額:90 元)
6 存款 10 元 -
7 提交事務(賬戶餘額:110 元) -

上述示例,直接導致銀行損失 10 元。如果 A 事務先提交,那麼將導致賬戶損失 10 元。

3 鎖機制

分類方式 類別
鎖定物件 表鎖定(整張表)、行鎖定(特定行)
併發事務鎖定關係 共享鎖定(執行其它的共享鎖定,但防止獨佔鎖定)、獨佔鎖定(防止任何鎖定)

oracle 資料庫中常見的鎖定:

鎖定 說明 防止 允許
行共享鎖定 可通過 select for update 語句隱式獲得該鎖定,或者通過 LOCK TABLE IN ROW SHARE MODE 語句顯式獲取 。 表獨佔鎖定 行共享鎖定、行獨佔鎖定、表共享行獨佔鎖定
行獨佔鎖定 可通過 insert、update、delete 語句隱式獲取,或者通過 LOCK TABLE IN ROW EXCLUSIVE MODE 語句顯式獲取 。 行或表共享鎖定、行或表獨佔鎖定 -
表共享鎖定 可通過 LOCK TABLE IN SHARE MODE 語句顯式獲取。該鎖定可以讓會話具有對錶事務級的一致性訪問,因為其他會話在使用者提交或者回滾該事務並釋放對該表的鎖定之前,不能更改這張表 。 表共享行獨佔鎖定、表獨佔鎖定 行共享鎖定、表共享鎖定
表共享行獨佔鎖定 可通過 LOCK TABLE IN SHARE ROW EXCLUSIVE MODE 語句顯式獲取。 表共享行獨佔鎖定、行獨佔鎖定、表獨佔鎖定 其它行的共享鎖定
表獨佔鎖定 可通過 LOCK TABLE IN EXCLUSIVE MODE 顯式獲取。 所有鎖定 -

上式表中的防止與允許列都是針對其它會話而言的。

4 事務的隔離級別

因為直接使用鎖比較麻煩,所以資料庫為我們設定了事務的隔離級別,這些級別實現了自動鎖機制 。 設定好事務的隔離級別後,資料庫就會分析事務中的 SQL 語句,然後自動為事務所操作的資料加上適合的鎖 。 而且,資料庫還會維護這些鎖,當一個資源上的鎖數目太多時,就會自動升級,從而提高系統的執行效能。這些過程對我們來說是完全透明的。

ANSI/ISO SQL 92 定義了 4 個等級的隔離級別:

隔離級別 髒讀 不可重複讀 幻讀 第一類丟失更新 第二類丟失更新
READ UNCOMMITTED 允許 允許 允許 不允許 允許
READ COMMITTED 不允許 允許 允許 不允許 允許
REPEATABLE_READ 不允許 不允許 允許 不允許 不允許
SERIALIZABLE 不允許 不允許 不允許 不允許 不允許

隔離級別與併發性是對立的,READ UNCOMMITTED 併發性最高,而 SERIALIZABLE 的併發性最低。

因為 Oracle 通過多版本機制,徹底解決了髒讀問題,所以它的 READ COMMITTED 已經達到 SQL 92 定義的 REPEATABLE_READ 標準。

SQL 92 推薦使用的隔離級別是:REPEATABLE_READ。

5 JDBC 事務

我們可以通過 Connection 的 getMetaData() 方法獲取 DatabaseMetaData 物件,然後通過該物件的 supportsTransactions()supportsTransactionIsolationLevel(int level) 方法檢視底層資料庫的事務支援情況 。

Connection 在預設情況下是自動提交的,也就是說,每一條執行的 SQL 都對應一個事務。為了能夠將多條 SQL 放在一個事務中執行,我們可以通過 Connection 的 setAutoCommit(false) 來關閉 Connection 的自動提交機制,還可以通過 Connection 的 setTransactionIsolation() 來設定事務的隔離級別, Connection 中定義了 SQL 92 標準中的 4 個事務隔離級別常量 。


Connection connection = null;

try {
	String url = "xxx";

	//獲取資料庫連線
	connection = DriverManager.getConnection(url);

	//關閉自動提交機制
	connection.setAutoCommit(false);

	//設定事務隔離級別
	connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);

	Statement statement = connection.createStatement();
	String sql = "xxx";
	statement.execute(sql);

	//提交事務
	connection.commit();


} catch (Exception e) {
	e.printStackTrace();
	try {
		//回滾事務
		connection.rollback();
	} catch (SQLException e1) {
		e1.printStackTrace();
	}
}
複製程式碼

JDBC2.0 中事務只有提交與回滾操作 。在 JDBC3.0 中(Java1.4+)引入了儲存點( SavePoint 介面)。儲存點可以把事務分割為多個階段,這樣我們就可以根據業務要求,來指定需要回滾到的特定儲存點啦O(∩_∩)O~

我們可以通過 DatabaseMetaData 的 supportsSavepoints() 方法驗證所連線的資料庫是否支援儲存點特性 。


Statement statement = connection.createStatement();
String sql1 = "xxx";
statement.execute(sql1);

//設定儲存點
Savepoint savepoint=connection.setSavepoint();

String sql2 = "xxx";
statement.execute(sql2);

//回退到儲存點
connection.rollback(savepoint);
複製程式碼

如果事務提交了上段程式碼, 那麼 sql1 語句將有效,而 sql2 語句因為在儲存點之後,所以被回滾咯。