1. 程式人生 > >七、JDBC-事務的隔離級別&批量處理

七、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小資料還成,大量的資料也真呵呵了;