1. 程式人生 > >jdbc事務和事務的隔離級別

jdbc事務和事務的隔離級別

jdbc的使用中以最簡單的jdbc的使用為例,說明了jdbc的具體用法。然而在通常專案中,需要考慮更多內容,例如事務。
事務,在單個數據處理單元中,存在若干個資料處理,要麼整體成功,要麼整體失敗。事務需要滿足ACID屬性(原子性、一致性、隔離性和永續性)。

  • 原子性:所謂原子性是指本次資料處理要麼都提交、要麼都不提交,即不能先提交一部分,然後處理其他的程式,然後接著提交未完成提交的剩餘部分。概念類似於程式語言的原子操作。
  • 一致性:所謂一致性是指資料庫資料由一個一致的狀態在提交事務後變為另外一個一致的狀態。例如,使用者確認到貨操作:確認前,訂單狀態為待簽收、客戶積分為原始積分,此狀態為一致的狀態;在客戶確認到後後,訂單狀態為已完成、客戶積分增加本次消費的積分,這兩個狀態為一致狀態。不能出現,訂單狀態為待簽收,客戶積分增加或者訂單狀態為已完成,客戶積分未增加的狀態,這兩種均為不一致的情況。一致性與原子性息息相關。
  • 隔離性:所謂隔離性是指事物與事務之間的隔離,即在事務提交完成前,其他事務與未完成事務的資料中間狀態訪問許可權,具體可通過設定隔離級別來控制。
  • 永續性:所謂永續性是指本次事務提交完成或者回滾完成均為持久的修改,除非其他事務進行操作否則資料庫資料不能發生改變。

本文重點描述事物隔離性及使用方法。
要詳細說明資料庫隔離級別,需要先對資料庫併發事務可能出現的幾種狀態進行說明:

  1. 讀髒:一個事務讀取另外一個事務尚未提交的資料。如下圖,執行緒thread1在事務中在time1時刻向庫表中新增一條資料‘test’並在time3時刻回滾資料;執行緒thread2在time2時刻讀取,若thread2讀取到‘test’,則為讀髒。
    這裡寫圖片描述
  2. 不可重新讀:其他事務的操作導致某個事務兩次讀取資料不一致。如下圖,執行緒thread1在事務中time1時刻將資料庫中‘test’更新為‘00’,並在time3時刻提交;thread2在一個事務中分別在time2和time4兩個時刻讀取這條記錄,若兩次讀取結果不同則為不可重讀。(注意:1.不可重讀針對已經提交的資料。2.兩次或多次讀取同一條資料。
    這裡寫圖片描述
  3. 幻讀:其他事務的資料操作導致某個事務兩次讀取資料數量不一致。如下圖,執行緒thread1在事務中time1時刻向資料庫中新增‘00’,並在time3時刻提交;thread2在一個事務中分別在time2和time4兩個時刻掃描庫表,若兩次讀取結果不同則為幻讀。(注意:1.幻讀針對已經提交的資料。2.兩次或多次讀取不同行資料,數量上新增或減少。

    這裡寫圖片描述

針對上訴3中事務併發情況,jdbc定義了5中事務隔離級別:
- TRANSACTION_NONE 無事務
- TRANSACTION_READ_UNCOMMITTED 允許讀髒,不可重讀,幻讀。
- TRANSACTION_READ_COMMITTED 直譯為僅允許讀取已提交的資料,即不能讀髒,但是可能發生不可重讀和幻讀。
- TRANSACTION_REPEATABLE_READ 不可讀髒,保證同一事務重複讀取相同資料,但是可能發生幻讀。
- TRANSACTION_SERIALIZABLE 直譯為序列事務,保證不讀髒,可重複讀,不可幻讀,事務隔離級別最高。

**> 注意:

  • 隔離級別對當前事務有效,例如若當前事務設定為TRANSACTION_READ_UNCOMMITTED,則允許當前事務對其他事務未提交的資料進行讀髒,而非其他事務可對當前事務未提交的資料讀髒。
  • 部分資料庫不支援TRANSACTION_NONE,例如mysql。
  • 在TRANSACTION_SERIALIZABLE 隔離級別下,為先執行DML更新,再執行查詢,此處為實驗的結論。
  • 若未顯示設定隔離級別,jdbc將採用資料庫預設隔離級別。文中實驗資料庫的預設隔離級別為:**
  • 這裡寫圖片描述

    以下將分別在各種事務隔離級別下,通過設定事務內訪問間隔時間,模擬讀髒、不可重讀、幻讀。

  • 建立庫表指令碼如下:

CREATE TABLE `t_dict` (
  `dict_type` varchar(255) DEFAULT NULL,
  `dict_code` varchar(255) DEFAULT NULL,
  `dict_name` varchar(255) DEFAULT NULL,
  `dict_remark` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • 主執行緒,用於建立資料庫連線、設定隔離級別、列印輸出等
package DBTest;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

public class DBTest {
    private String url ;
    private String user;
    private String password;

    /**
     * 建立資料連線
     * @return
     */
    private Connection getCon(){
        Connection con = null;
        try{
            Class.forName("com.mysql.jdbc.Driver");
            url = "jdbc:mysql://localhost:3306/twork";
            user = "root";
            password = "root";
            con = DriverManager.getConnection(url, user, password);
        }catch (Exception e){
            e.printStackTrace();
            try {
                con.close();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }
        return con;
    }

    /**
     * 通過連結獲取宣告
     * @param con
     * @return
     */
    private Statement getStat(Connection con){
        Statement state = null;
        try{
            state = con.createStatement();
        }catch(Exception e){
            e.printStackTrace();
        }
        return state;
    }

    /**
     *  列印資料庫所有資料
     */
    public void selectAll(int transactionType){
        Connection con = null;
        Statement state = null;
        ResultSet rs = null;
        try{
            con = getCon();
            if(transactionType >= 0 ){
                 con.setTransactionIsolation(transactionType);
            }
            System.out.println("-------------當前事務隔離級別為:"+con.getTransactionIsolation()+"-------------");
            state = getStat(con);
            rs = state.executeQuery("select * from t_dict");
            ResultSetMetaData rsmd = rs.getMetaData();
            for(int i = 1;i<= rsmd.getColumnCount() ;i++){
                System.out.print(rsmd.getColumnName(i)+"| ");
            }
            System.out.println();
            System.out.println("-------------------------------------------");
            //列印所有行
            while(rs.next()){
                for(int i = 1;i<= rsmd.getColumnCount() ;i++){
                    System.out.print(rs.getString(i)+"|     ");
                }
                System.out.println();
            }
        }
        catch (Exception e){
            try {
                con.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        }
        finally {
            try {
                if(rs != null){
                    rs.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
            try {
                if(state != null){
                    state.close();
                }
            } catch (Exception e){

            }
            try {
                if(con != null){
                    con.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 新增一行
     * @param needExcepition
     * @param sleepTimes
     * @param values
     * @return
     */
    public int insertOne(int needExcepition,int sleepTimes, List<String> values){
        Connection con = getCon();
        PreparedStatement pre = null;
        String sql = "INSERT INTO t_dict (dict_type, dict_code, dict_name, dict_remark) VALUES (?, ?, ?, ?)";
        int res = 0;
        try {
            con.setAutoCommit(false);
            pre = con.prepareStatement(sql);
            for(int i = 0; i < values.size() ;i++){
                pre.setString(i+1, values.get(i));
            }
            Thread.sleep(sleepTimes);
            System.out.println("before execute");
            res = pre.executeUpdate();
            System.out.println("after execute");
            Thread.sleep(sleepTimes);
            int i = 1/needExcepition;
            System.out.println("before commit");
            con.commit();
            System.out.println("after commit");
        } catch (Exception e) {
            try {
                System.out.println("before roll back");
                con.rollback();
                System.out.println("after roll back");
                res = 0;
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            try {
                if(pre != null){
                    pre.close();
                }
            } catch (Exception e){

            }
            try {
                if(con != null){
                    con.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return res;
    }

    /**
     * 間隔一定時間讀取多次
     * @param dictType 要去讀取的資料型別
     * @param sleepTimes 每次讀取之間的間隔時間
     * @param printTimes 列印次數
     * @param transactionType 事務隔離級別
     */
    private void printMultiple(String dictType,int sleepTimes,int printTimes, int transactionType){
        Connection con = null;
        Statement state = null;
        ResultSet rs = null;
        try{
            con = getCon();
            con.setAutoCommit(false);
            if(transactionType >= 0){
                con.setTransactionIsolation(transactionType);
            }
            System.out.println("-------------當前事務隔離級別為:"+con.getTransactionIsolation()+"-------------");
            state = getStat(con);
            for (int j = 0; j < printTimes; j++) {
                Thread.sleep(sleepTimes);
                rs = state.executeQuery("select * from t_dict where dict_type = '"+dictType+"' ");
                ResultSetMetaData rsmd = rs.getMetaData();
                System.out.println("第"+(j+1)+"次讀取");
                for(int i = 1;i<= rsmd.getColumnCount() ;i++){
                    System.out.print(rsmd.getColumnName(i)+"| ");
                }
                System.out.println();
                System.out.println("-------------------------------------------");
                while(rs.next()){
                    for(int i = 1;i<= rsmd.getColumnCount() ;i++){
                        System.out.print(rs.getString(i)+"|     ");
                    }
                    System.out.println();
                }   
            }
            con.commit();
        }
        catch (Exception e){
            try {
                con.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        }
        finally {
            try {
                if(rs != null){
                    rs.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
            try {
                if(state != null){
                    state.close();
                }
            } catch (Exception e){

            }
            try {
                if(con != null){
                    con.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 更改一條資料的內容
     * @param dict_type
     * @param sleepTimes
     * @param values
     * @return
     */
    public int updateOne(String dict_type,int sleepTimes, List<String> values){
        Connection con = null;
        PreparedStatement pre = null;
        String sql = "UPDATE t_dict  SET dict_code = ?, dict_name = ?, dict_remark = ? WHERE dict_type ='"+dict_type+"'";
        int res = 0;
        try {
            con = getCon();
            con.setAutoCommit(false);
            con.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
            pre = con.prepareStatement(sql);
            for(int i = 0; i < values.size() ;i++){
                pre.setString(i+1, values.get(i));
            }
            Thread.sleep(sleepTimes);
            System.out.println("before execute ");
            res = pre.executeUpdate();
            System.out.println("after execute ");
            Thread.sleep(sleepTimes);
            System.out.println("before commit");
            con.commit();
            System.out.println("after commit");
        } catch (Exception e) {
            try {
                con.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            try {
                if(pre != null){
                    pre.close();
                }
            } catch (Exception e){

            }
            try {
                if(con != null){
                    con.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return res;
    }

    /**
     * @param transType
     */
    public void testTransaction(int transType){
        intDate();
        System.out.println("-------------------讀髒模擬---------------------");
        testDirty(transType);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-------------------不可重讀模擬------------------");
        testRepeat(transType);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-------------------幻讀模擬----------------------");
        testTrick(transType);
    }

    /**
     * 初始化資料
     */
    private void intDate(){
        System.out.println("------------初始化資料 start-------------");
        Connection con = getCon();
        Statement pre = null;
        String sqlDelete = "delete from t_dict";
        String sqlInsert = "INSERT INTO `twork`.`t_dict` (`dict_type`, `dict_code`, `dict_name`, `dict_remark`) VALUES ('type0', '00', 'type00', 'type00')";
        try {
            con.setAutoCommit(false);
            pre = con.createStatement();
            pre.execute(sqlDelete);
            pre.execute(sqlInsert);
            con.commit();
        } catch (Exception e) {
            try {
                con.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            try {
                if(pre != null){
                    pre.close();
                }
            } catch (Exception e){
                e.printStackTrace();
            }
            try {
                if(con != null){
                    con.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        System.out.println("------------初始化資料 end-------------");
    }
    /**
     * 模擬讀髒,丟擲未捕獲異常,插入資料不提交
     */
    public void testDirty(int transactionType){
        List<String> list = new ArrayList<String>();
        list.add("type1");
        list.add("11");
        list.add("type11");
        list.add("type11");
        TestThread testThread = new TestThread("insert",0,300,list);
        Thread thread = new Thread(testThread);
        thread.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        selectAll(transactionType);
    }

    /**
     * 模擬幻讀,第N次讀取多出資料
     */
    public void testTrick(int transactionType){
        List<String> list = new ArrayList<String>();
        list.add("type0");
        list.add("11");
        list.add("type11");
        list.add("type11");
        //執行插入,不產生異常
        TestThread testThread = new TestThread("insert",1,400,list);
        Thread thread = new Thread(testThread);
        thread.start();
        //列印兩次
        printMultiple("type0", 300,4,transactionType);
    }

    /**
     * 模擬不可重讀,多次讀取同一條記錄,記錄被更改
     */
    public void testRepeat(int transactionType){
        List<String> list = new ArrayList<String>();
        list.add("type0");
        list.add("11");
        list.add("type11");
        list.add("type11");
        //執行插入,不產生異常
        TestThread testThread = new TestThread("update",1,400,list);
        Thread thread = new Thread(testThread);
        thread.start();
        //列印4次
        printMultiple("type0", 300,4,transactionType);
    }

    public static void main(String[] args){
        DBTest dbTest = new DBTest();
        /*分別執行下面的方法,即可模擬各個隔離級別下,執行緒併發事務間的訪問結果*/
        System.out.println(" -----------------------TRANSACTION_READ_UNCOMMITTED test start------------------------");
        dbTest.testTransaction(Connection.TRANSACTION_READ_UNCOMMITTED);

//      System.out.println(" -----------------------TRANSACTION_READ_COMMITTED test start------------------------");
//      dbTest.testTransaction(Connection.TRANSACTION_READ_COMMITTED);
//      
//      System.out.println(" -----------------------TRANSACTION_REPEATABLE_READ test start------------------------");
//      dbTest.testTransaction(Connection.TRANSACTION_REPEATABLE_READ);
//      
//      System.out.println(" -----------------------TRANSACTION_SERIALIZABLE test start------------------------");
//      dbTest.testTransaction(Connection.TRANSACTION_SERIALIZABLE);
//      
//      System.out.println(" -----------------------default test start------------------------");
//      dbTest.testTransaction(-1);
    }

}    
  • 併發執行緒,呼叫讀取方法
package DBTest;

import java.util.ArrayList;
import java.util.List;

/** 
* Created by ygl on 2016/5/1. 
*/ 
public class TestThread implements Runnable {
    int needException = 1;

    int sleepTimes = 0;

    List<String> list = new ArrayList<String>();

    String method = "";

    DBTest dbTest = new DBTest();
    /**
     * @param method insert 或 update
     * @param needException 是否需要丟擲異常,0丟擲異常,1不丟擲異常
     * @param sleepTimes 執行緒睡眠時間(毫秒)
     * @param list 更新資料庫的資料,當method為update時,list的第一個元素為條件,其他為更新內容
     */
    public TestThread(String method,int needException, int sleepTimes , List<String> list){
        this.needException = needException;
        this.sleepTimes = sleepTimes;
        this.list = list;
        this.method = method;
    }

    public void run(){
        if("insert".equals(method)){
            insert();
        } else if("update".equals(method)){
            update();
        }
    }

    private void insert(){
        int res = dbTest.insertOne(needException, sleepTimes, list);
        if(res == 1){
            System.out.println("insert success");
        }else{
            System.out.println("insert fail");
        }
    }

    private void update(){
        String updateKey = list.get(0);
        list.remove(0);
        int res = dbTest.updateOne(updateKey, sleepTimes, list);
        if(res == 1){
            System.out.println("update success");
        }else{
            System.out.println("update fail");
        }
    }
}

讀者可以使用上述程式分別測試,這裡僅以TRANSACTION_READ_UNCOMMITTED為例,輸出結果為:

 -----------------------TRANSACTION_READ_UNCOMMITTED test start------------------------
------------初始化資料 start-------------

------------初始化資料 end-------------
-------------------讀髒模擬---------------------

before execute
after execute

-------------當前事務隔離級別為:1-------------
dict_type| dict_code| dict_name| dict_remark| 
-------------------------------------------
type0|     00|     type00|     type00|     
type1|     11|     type11|     type11|     
before roll back
after roll back
java.lang.ArithmeticException: / by zero
    at DBTest.DBTest.insertOne(DBTest.java:141)
    at DBTest.TestThread.insert(TestThread.java:41)
    at DBTest.TestThread.run(TestThread.java:34)
    at java.lang.Thread.run(Unknown Source)
insert fail
-------------------不可重讀模擬------------------

-------------當前事務隔離級別為:1-------------
第1次讀取
dict_type| dict_code| dict_name| dict_remark| 
-------------------------------------------
type0|     00|     type00|     type00|     
before execute 
after execute 
第2次讀取
dict_type| dict_code| dict_name| dict_remark| 
-------------------------------------------
type0|     11|     type11|     type11|     
before commit
after commit
update success
第3次讀取
dict_type| dict_code| dict_name| dict_remark| 
-------------------------------------------
type0|     11|     type11|     type11|     
第4次讀取
dict_type| dict_code| dict_name| dict_remark| 
-------------------------------------------
type0|     11|     type11|     type11|     
-------------------幻讀模擬----------------------

-------------當前事務隔離級別為:1-------------
第1次讀取
dict_type| dict_code| dict_name| dict_remark| 
-------------------------------------------
type0|     11|     type11|     type11|     
before execute
after execute
第2次讀取
dict_type| dict_code| dict_name| dict_remark| 
-------------------------------------------
type0|     11|     type11|     type11|     
type0|     11|     type11|     type11|     
before commit
第3次讀取
dict_type| dict_code| dict_name| dict_remark| 
-------------------------------------------
type0|     11|     type11|     type11|     
type0|     11|     type11|     type11|     
after commit
insert success
第4次讀取
dict_type| dict_code| dict_name| dict_remark| 
-------------------------------------------
type0|     11|     type11|     type11|     
type0|     11|     type11|     type11|