七、JDBC-事務的隔離級別&批量處理
資料庫事務的隔離級別
對於同時執行的多個事務, 當這些事務訪問資料庫中相同的資料時, 如果沒有采取必要的隔離機制, 就會導致各種併發問題:
髒讀: 對於兩個事務 T1, T2, T1 讀取了已經被 T2 更新但還沒有被提交的欄位. 之後, 若 T2 回滾, T1讀取的內容就是臨時且無效的.
不可重複讀: 對於兩個事務 T1, T2, T1 讀取了一個欄位, 然後 T2 更新了該欄位. 之後, T1再次讀取同一個欄位, 值就不同了.
幻讀: 對於兩個事務 T1, T2, T1 從一個表中讀取了一個欄位, 然後 T2 在該表中插入了一些新的行. 之後, 如果 T1 再次讀取同一個表, 就會多出幾行.
資料庫事務的隔離性: 資料庫系統必須具有隔離併發執行各個事務的能力, 使它們不會相互影響, 避免各種併發問題.
一個事務與其他事務隔離的程度稱為隔離級別. 資料庫規定了多種事務隔離級別, 不同隔離級別對應不同的干擾程度, 隔離級別越高, 資料一致性就越好, 但併發性越弱
資料庫提供的 4 種事務隔離級別:
Oracle 支援的 2 種事務隔離級別:READ COMMITED, SERIALIZABLE. Oracle 預設的事務隔離級別為: READ COMMITED
Mysql 支援 4 中事務隔離級別. Mysql 預設的事務隔離級別為: REPEATABLE READ
具體程式碼實現:
/** * ID1 給 ID2 500錢 * 關於事務: * 1.如果多個操作,每個使用自己單獨的連線,則無法保證事務 例 test1演示 * 2.具體步驟: * 1) 事務開始前,取消Connection 的預設的自動提交 setAutoCommit(false); * 2) 如果事務的操作都成功,那麼就提交事務 * 3)否則在 try-catch塊中回滾 * try { * * conn.setAutoCommit(false); * ... * conn.commit(); * }catch{ * ... * conn.rollback(); * } */ @Test public void test2(){ Connection conn = null; try { conn = JDBC_Tools.getConnection(); //System.out.println(conn.getAutoCommit()); // 1) 取消自動提交 conn.setAutoCommit(false); String sql = "UPDATE rent set money = " + "money - 500 where id = ?"; // 2) 如果事務的操作都成功,那麼就提交事務 update(conn,sql, 1); //int i = 1 / 0; sql = "UPDATE rent set money = " + "money + 500 where id = ?"; update(conn,sql, 2); conn.commit(); } catch (Exception e) { e.printStackTrace(); // 3)否則在 try-catch塊中回滾 try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } }finally{ JDBC_Tools.relaseSource(conn, null); } } public static void update(Connection conn,String sql,Object...objs){ PreparedStatement ps =null; try { ps = conn.prepareStatement(sql); for(int i = 0;i<objs.length;i++){ ps.setObject(i+1, objs[i]); } ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); }finally{ JDBC_Tools.relaseSource(null, ps); } } @Test public void test1() { String sql = "UPDATE rent set money = " + "money - 500 where id = ?"; DAO.update(sql, 1); int i = 1 / 0; //一旦出現異常, ID1 減了500,但是 ID2 的錢並沒有增加 sql = "UPDATE rent set money = " + "money + 500 where id = ?"; DAO.update(sql, 2); }設定隔離級別 public static <E> E getForValue(String sql){ //1. 得到結果集,該結果只有一行一列 Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { //1. 獲取資料庫連線 conn = JDBC_Tools.getConnection();//System.out.println(conn.getTransactionIsolation()); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); //2. 獲取 PreparedStatement 物件 ps = conn.prepareStatement(sql); //2. 取得結果 rs = ps.executeQuery(); if(rs.next()){ return (E)rs.getObject(1); } }catch(Exception e){ e.printStackTrace(); }finally{ JDBC_Tools.relaseSource(rs,conn, ps); } return null; }
啟動一個 mysql 程式, 就會獲得一個單獨的資料庫連線. 每個資料庫連線都有一個全域性變數 @@tx_isolation, 表示當前的事務隔離級別. MySQL 預設的隔離級別為 Repeatable Read
檢視當前的隔離級別: SELECT @@tx_isolation;
設定當前 mySQL 連線的隔離級別:
set transaction isolation level read committed;
設定資料庫系統的全域性的隔離級別:
set global transaction isolation level read committed;
JDBC批量執行
當需要成批插入或者更新記錄時。可以採用Java的批量更新機制,這一機制允許多條語句一次性提交給資料庫批量處理。通常情況下比單獨提交處理更有效率
/**
* 向mysql的testJ資料表中插入100000條記錄
* 測試如何插入用時最短
* 版本一:使用Statement
*/
版本一:我們使用Statement進行事務的操作
@Test
public void testBatchWithStatement(){
Connection connection=null;
Statement statement=null;
String sql;
try {
connection=JDBCTools.getConnection();
//放到一個事務裡面
JDBCTools.beginTx(connection);
statement=connection.createStatement();
long begin=System.currentTimeMillis();
for(int i=0;i<100000;i++){
sql="insert into testj values("+
(i+1)+", 'name_"+ i+"', '2016-05-08')";
statement.execute(sql);
}
long end=System.currentTimeMillis();
System.out.println("Time:"+(end-begin));
JDBCTools.commit(connection);
} catch (Exception e) {
e.printStackTrace();
JDBCTools.rollback(connection);
}finally{
JDBCTools.release(null, statement, connection);
}
}
執行結果:
Time:8991
結論一:我們使用Statement插入100000條記錄用時8991;
版本二:我們使用PreparedStatement進行事務的操作
@Test
public void testBatchWithPreparedStatement() {
Connection connection = null;
PreparedStatement preparedStatement = null;
String sql;
try {
connection = JDBCTools.getConnection();
// 放到一個事務裡面
JDBCTools.beginTx(connection);
sql = "isnert into testJ values(?,?,?)";
preparedStatement = connection.prepareStatement(sql);
long begin = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
preparedStatement.setInt(1, i + 1);
preparedStatement.setString(2, "name_" + i);
preparedStatement.setDate(3,
new Date(new java.util.Date().getTime()));
preparedStatement.execute();
}
long end = System.currentTimeMillis();
System.out.println("Time:" + (end - begin));
JDBCTools.commit(connection);
} catch (Exception e) {
e.printStackTrace();
JDBCTools.rollback(connection);
} finally {
JDBCTools.release(null, preparedStatement, connection);
}
}
執行結果:
Time:8563
結論2:因為我這裡使用的是mysql資料庫進行的操作,插入大量資料的時間效能方面的影響不是很大,如果我們換成oracle資料庫或其他大型的關係型資料庫,事務執行用時相比版本一的1/4;
版本三:批處理插入資料
@Test
public void testBatchWithBatch() {
Connection connection = null;
PreparedStatement preparedStatement = null;
String sql=null;
try {
connection = JDBCTools.getConnection();
// 放到一個事務裡面
JDBCTools.beginTx(connection);
sql = "insert into testJ values(?,?,?)";
preparedStatement = connection.prepareStatement(sql);
long begin = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
preparedStatement.setInt(1, i + 1);
preparedStatement.setString(2, "name_" + i);
preparedStatement.setDate(3,
new Date(new java.util.Date().getTime()));
//積攢SQL
preparedStatement.addBatch();
//當積攢到一定程度,就統一執行,並且清空先前積攢的SQL
if((i+1)%300==0){
//執行
preparedStatement.executeBatch();
//清空
preparedStatement.clearBatch();
}
}
//如果插入的記錄數不是300的整倍數,再執行一次
if(100000%300!=0){
//執行
preparedStatement.executeBatch();
//清空
preparedStatement.clearBatch();
}
long end = System.currentTimeMillis();
System.out.println("Time:" + (end - begin));
JDBCTools.commit(connection);
} catch (Exception e) {
e.printStackTrace();
JDBCTools.rollback(connection);
} finally {
JDBCTools.release(null, preparedStatement, connection);
}
}
執行結果:4587(又提高了,但是還是不明顯)
結論三:批處理事務建議採用版本三的方式,再次建議使用oracle資料庫做這個插入資料事務的實驗,mysql小資料還成,大量的資料也真呵呵了;