| 現象 | Read Committed | Serializable |
| 髒寫 | 不可發生 | 不可發生 |
| 髒讀 | 不可發生 | 不可發生 |
| 非重複讀 | 可能 | 不可發生 |
| 幻讀 | 可能 | 不可發生 |
語義深度解析
Read Committed:語句級快照
- 事務中執行
executeQuery() 時,建立該查詢的 當前快照 (SCN)
- 同一事務中兩次相同查詢,若中間有其他事務提交修改 → 結果不同 (非重複讀)
- 新插入的滿足條件行出現在第二次查詢中 → 幻讀
- Oracle 通過 多版本並發控制 (MVCC) 實現:讀不阻塞寫,寫不阻塞讀
Serializable:事務級快照
- 事務啟動時 (首條 DML) 建立 事務快照 SCN
- 所有查詢均基於此快照,**完全隔離其他事務的並發提交**
- 若嘗試修改被其他事務已修改且提交的行 → ORA-08177: can't serialize access 拋出異常,需重試
- 實現代價:更多鎖競爭、更高中止率、較低並發吞吐
JDBC 驗證代碼完整實戰
基礎測試框架
package test.transaction;
import java.sql.*;
import junit.framework.TestCase;
public class BaseTestCase extends TestCase {
protected Connection conn = null;
private String url = "jdbc:oracle:thin:@10.200.10.19:1521:aaaa";
private String user = "loocky";
private String pwd = "loocky";
protected void setUp() throws Exception {
super.setUp();
Class.forName("oracle.jdbc.driver.OracleDriver");
conn = DriverManager.getConnection(url, user, pwd);
}
protected void tearDown() throws Exception {
super.tearDown();
if (conn != null && !conn.isClosed()) conn.close();
}
}
測試 1:Read Committed 非重複讀驗證
public class TestReadCommitted extends BaseTestCase {
public void testNonRepeatableRead() {
try {
conn.setAutoCommit(false);
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
// T1: 查詢初始值
String sql = "SELECT item FROM sys_dbinfo WHERE id = 1";
PreparedStatement ps1 = conn.prepareStatement(sql);
ResultSet rs1 = ps1.executeQuery();
rs1.next();
String initial = rs1.getString(1); // 假設 "aaa"
System.out.println("T1 第一次讀取: " + initial);
rs1.close(); ps1.close();
// 模擬:此處暫停 T1,在另一終端執行 T2 更新並提交
// UPDATE sys_dbinfo SET item='bbb' WHERE id=1; COMMIT;
System.out.println(">>> 請在另一終端執行 T2 更新並提交,按 Enter 繼續...");
System.in.read();
// T1: 再次查詢
PreparedStatement ps2 = conn.prepareStatement(sql);
ResultSet rs2 = ps2.executeQuery();
rs2.next();
String after = rs2.getString(1); // 將讀到 "bbb"
System.out.println("T1 第二次讀取: " + after);
rs2.close(); ps2.close();
conn.commit();
assertNotEquals("Read Committed 允許非重複讀", initial, after);
} catch (Exception e) { e.printStackTrace(); }
}
}
測試 2:Serializable 可重複讀驗證
public class TestSerializable extends BaseTestCase {
public void testRepeatableRead() {
try {
conn.setAutoCommit(false);
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
String sql = "SELECT item FROM sys_dbinfo WHERE id = 1";
// T1: 第一次查詢 (建立事務快照)
PreparedStatement ps1 = conn.prepareStatement(sql);
ResultSet rs1 = ps1.executeQuery();
rs1.next();
String first = rs1.getString(1); // "aaa"
System.out.println("T1 第一次讀取: " + first);
rs1.close(); ps1.close();
System.out.println("======================");
// 模擬:暫停 T1,另一終端執行 T2 更新並提交
System.out.println(">>> 請在另一終端執行 T2 更新並提交,按 Enter 繼續...");
System.in.read();
// T1: 第二次查詢 (基於事務快照)
PreparedStatement ps2 = conn.prepareStatement(sql);
ResultSet rs2 = ps2.executeQuery();
rs2.next();
String second = rs2.getString(1); // 仍讀到 "aaa" (T2 提交前的值)
System.out.println("T1 第二次讀取: " + second);
rs2.close(); ps2.close();
conn.commit();
assertEquals("Serializable 保證可重複讀", first, second);
} catch (Exception e) { e.printStackTrace(); }
}
}
測試 3:Serializable 更新衝突驗證 (ORA-08177)
public class TestSerializableConflict extends BaseTestCase {
public void testUpdateConflict() {
try {
conn.setAutoCommit(false);
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
// T1: 讀取
String sql = "SELECT item FROM sys_dbinfo WHERE id = 1";
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
rs.next();
System.out.println("T1 讀取: " + rs.getString(1));
rs.close(); ps.close();
// 模擬:T2 更新並提交
System.out.println(">>> 請在另一終端執行 T2 更新並提交,按 Enter 繼續...");
System.in.read();
// T1: 嘗試更新同一行 → 應拋出 ORA-08177
String updateSql = "UPDATE sys_dbinfo SET item='ccc' WHERE id = 1";
PreparedStatement psUpdate = conn.prepareStatement(updateSql);
int count = psUpdate.executeUpdate(); // 拋出 SQLException: ORA-08177
System.out.println("更新行數: " + count);
psUpdate.close();
conn.commit();
} catch (SQLException e) {
System.out.println("預期衝突: " + e.getMessage());
assertTrue(e.getMessage().contains("ORA-08177"));
try { conn.rollback(); } catch (Exception ignore) {}
} catch (Exception e) { e.printStackTrace(); }
}
}
調試執行步驟
- 啟動資料庫,確保
sys_dbinfo 表有 id=1, item='aaa'
- 以 Debug 模式運行
TestReadCommitted,在 System.in.read() 處斷點暫停
- 開啟第二個 SQL Plus / SQL Developer 終端,執行:
UPDATE sys_dbinfo SET item='bbb' WHERE id=1; COMMIT;
- 繼續 T1 運行,觀察第二次讀取結果變為
bbb (非重複讀)
- 以 Debug 模式運行
TestSerializable,同樣在暫停處執行 T2 更新
- 觀察 T1 第二次讀取仍為
aaa (可重複讀)
- 運行
TestSerializableConflict,觀察 ORA-08177 異常
生產環境選型指南
| 場景 | 推薦隔離級別 | 理由 |
| 高併發 OLTP、讀多寫少、可容忍非重複讀 | Read Committed (預設) | 最高並發吞吐、無序列化失敗重試、MVCC 讀寫不阻塞 |
| 財務對帳、報表統計、需一致性快照 | Serializable | 保證報表數據前後一致、避免幻讀干擾統計 |
| 長事務、批次處理、低併發 | Serializable | 事務級一致性、接受較高中止率 |
| 樂觀鎖 + 版本欄位應用層控制 | Read Committed | 應用層保證業務一致性、資料庫層面最高並發 |
進階主題:Oracle 隱式鎖行為
- SELECT FOR UPDATE:显式鎖定讀取行,其他事務修改該行將阻塞
- Read Committed + SELECT FOR UPDATE:可實現「讀取時鎖定、處理後更新」模式,但需注意死鎖
- Serializable 實際上使用行級鎖 + 系統變更號 (SCN) 檢查:提交時驗證讀取集合是否被修改
總結
Oracle 只支援 Read Committed 和 Serializable 兩種隔離級別,這是由其 MVCC 架構決定的。Read Committed 以語句級一致性換取極致並發,是 95%+ OLTP 應用的最佳選擇。Serializable 提供事務級一致性,但付出序列化失敗重試、鎖競爭、吞吐下降的代價。**關鍵洞見**:現代應用多採用「Read Committed + 應用層樂觀鎖 (版本欄位) + 關鍵業務 SELECT FOR UPDATE」組合策略,在資料庫層面保持最高並發,在業務層面保證邏輯一致性。JDBC 代碼驗證證明:隔離級別不僅是理論概念,直接決定了併發異常的具體行為模式。
- 上一篇文章: EJBCA 3.0 開源 CA 系統深度評測:基於 J2EE 的企業級 PKI 基礎設施完整功能清單與架構分析
- 下一篇文章: JBoss EJB 3.0 開發實戰:無狀態會話 Bean 的 Remote/Local 雙接口實現與 JNDI 查找
Copyright ©
程式碼之家 All Rights Reserved