MySQL AutoCommit帶來的問題

原創內容,轉載請註明出處html

http://www.cnblogs.com/wingsless/p/6803542.htmljava

現象描述

測試中發現,服務A在獲得了服務B的註冊用戶成功response之後,開始調用查詢用戶信息接口,卻發現沒法查詢出任何結果。檢查binlog發現,在查詢請求以前,數據庫確實已經完成了commit操做,而且能夠在sqlyog等客戶端工具中查詢出正確的結果。mysql

下面是這個流程的時序圖:sql

時序圖

問題出如今Server A向數據庫發起查詢的時候,返回的結果老是空。數據庫

問題分析

這個問題顯然是一個事務隔離的問題,最開始的思路是,服務A所在的機器,其事務開啓時間應該是在服務B的機器commit操做以前開啓的,可是經過DEBUG日誌分析connection的獲取和提交時間,發現兩個服務器之間不存在這樣的關係,服務B永遠是在服務A返回了正確的response以後纔會調用數據庫接口,進行getConnection操做,進而進行查詢操做。tomcat

顯然這並不能支持剛纔的設想,可是結論必定是正確的,就是由於事務隔離級別致使了Server A讀到的永遠是快照,發生了可重複讀。服務器

後來調整了一下思路,發現MySQL還有一個特性就是AutoCommit,即默認狀況下,MySQL是開啓事務的,下面表格能說明問題,表1:less

表1

可是,若是AutoCommit不是默認開啓呢?結果就會變成下面的表格,表2:工具

表2

在關閉AutoCommit的條件下,SessionA在T1和T2兩個時間點執行的SQL語句其實在一個事務裏,所以每次讀到的其實只是一個快照。oop

那麼在鏈接池條件下,狀況如何?

設置一個極端條件,鏈接池只給一個鏈接,編寫兩個類,一個負責插入數據,一個負責循環讀取數據,可是讀取數據的類在執行讀取方法以前,會執行一個空方法,這個方法只會作一件事情,就是獲取鏈接,將其AutoCommit設置爲FALSE,關閉鏈接。

兩段代碼以下:

寫入線程:

public static void main( String[] args ) throws Exception
    {
        DBconfigEntity entity = new DBconfigEntity();
        entity.setDbName("test");
        entity.setDbPasswd("123456");
        entity.setDbUser("root");
        entity.setIp("127.0.0.1");
        entity.setPort(3306);
        MysqlClient.init(entity);
        MysqlClient instance = MysqlClient.getInstance();
 
        Connection conn = instance.getConnection();
        conn.setAutoCommit(false);
        String sql = "insert into test1(uname) values (?)";
        PreparedStatement statement = conn.prepareStatement(sql);
        statement.setString(1, "PPP");
        statement.executeUpdate();
        conn.commit();
 
        statement.close();
        conn.close();
 
        //永遠休眠,可是永遠持有鏈接池
        Thread.sleep(Long.MAX_VALUE);
    }

讀取類:

public class GetClient {
 
    private void query() throws SQLException
    {
        System.out.println("start");
        MysqlClient instance = MysqlClient.getInstance();
        Connection conn = instance.getConnection();
        String sql = "select uname from test1";
        PreparedStatement statement = conn.prepareStatement(sql);
        ResultSet rs = statement.executeQuery();
        while (rs.next()) {
            System.out.println(rs.getString("uname"));
        }
 
        statement.close();
        rs.close();
        conn.close();
    }
 
    private void nothing() throws SQLException
    {
        MysqlClient instance = MysqlClient.getInstance();
        Connection conn = instance.getConnection();
        conn.setAutoCommit(false);
        conn.close();
 
    }
    public static void main(String[] args) throws SQLException, InterruptedException, ClassNotFoundException {
        DBconfigEntity entity = new DBconfigEntity();
        entity.setDbName("test");
        entity.setDbPasswd("123456");
        entity.setDbUser("root");
        entity.setIp("127.0.0.1");
        entity.setPort(3306);
        MysqlClient.init(entity);
 
        GetClient client = new GetClient();
        client.nothing();
        while (true) {
            client.query();
            Thread.sleep(5000);
        }
    }
}

表初始沒有任何數據,首先運行讀取類,此時讀取類只會不停的打印「start」,此時啓動寫入類,觀察發現,console並不會打印數據庫test1表查詢的結果,可是在數據庫工具中查看,test1表確實已經有了數據。

這是由於在鏈接池條件下,若是這個鏈接以前被借出過,而且曾經被設置成了AutoCommit爲FALSE,那麼這個鏈接在其生存時間內,永遠會默認開啓事務,這是MySQL自身決定的,由於鏈接池只是持有鏈接,代碼中的close操做只是將該鏈接還給鏈接池,可是並無真的將鏈接銷燬,所以鏈接的屬性仍然保持上次設置的樣子。當另外一個方法開始,從新執行getConnection獲取連接時,是有可能獲取到以前被設置爲AutoCommit爲FALSE的鏈接的,這個時候就至關於上面的表2中Session A在T3時間點的狀況,不管如何查詢,都會查不出任何數據來。

以下圖:

不管如何commit,都沒法改變這個鏈接的autocommit屬性。

由於測試時採用的是一個鏈接這種極端條件,所以該現象很是容易復現,且是100%的復現,可是在測試條件下,並不是100%復現,而是在重啓以後會好一段時間,一段時間之後就會從新出現這個狀況。

若是將讀取類的代碼稍加修改:

public class GetClient {
 
    private void query() throws SQLException
    {
        System.out.println("start");
        MysqlClient instance = MysqlClient.getInstance();
        Connection conn = instance.getConnection();
        conn.setAutoCommit(true);
        String sql = "select uname from test1";
        PreparedStatement statement = conn.prepareStatement(sql);
        ResultSet rs = statement.executeQuery();
        while (rs.next()) {
            System.out.println(rs.getString("uname"));
        }
 
        statement.close();
        rs.close();
        conn.close();
    }
 
    private void nothing() throws SQLException
    {
        MysqlClient instance = MysqlClient.getInstance();
        Connection conn = instance.getConnection();
        conn.setAutoCommit(false);
        conn.close();
 
    }
    public static void main(String[] args) throws SQLException, InterruptedException, ClassNotFoundException {
        DBconfigEntity entity = new DBconfigEntity();
        entity.setDbName("test");
        entity.setDbPasswd("123456");
        entity.setDbUser("root");
        entity.setIp("127.0.0.1");
        entity.setPort(3306);
        MysqlClient.init(entity);
 
        GetClient client = new GetClient();
        client.nothing();
        while (true) {
            client.query();
            Thread.sleep(5000);
        }
    }
}

注意我在query方法中加入這一句:conn.setAutoCommit(true);

此時這個問題再也不出現。

源碼分析

jdbc驅動源碼分析

Connection是Java提供的一個標準接口:java.sql.Connection,其具體實現是:com.mysql.jdbc.ConnectionImpl。

分析jdbc驅動代碼可知,jdbc默認的AutoCommit狀態是TRUE:

這實際上和MySQL的默認值是同樣的。

tomcat-jdbc源碼分析

tomcat-jdbc的close方法由攔截器實現,具體的邏輯代碼:

if (compare(CLOSE_VAL,method)) {
            if (connection==null) return null; //noop for already closed.
            PooledConnection poolc = this.connection;
            this.connection = null;
            pool.returnConnection(poolc);
            return null;
}

實際上此處只是將鏈接還給了鏈接池,沒有對鏈接進行任何處理。

tomcat-jdbc維護了兩個Queue:busy和idle,用於存放空閒和已借出鏈接,鏈接還給鏈接池的過程簡單的說就是將該鏈接從busy隊列中移除,並放在idle隊列中的過程。

boneCP源碼分析

根據實際使用的經驗看,boneCP鏈接池在使用的過程當中並無出現這個問題,分析boneCP的Connection具體實現,發如今close方法的具體實現中,有這樣的一段代碼邏輯:

if (!getAutoCommit()) {
    setAutoCommit(true);
}

這段邏輯會判斷該鏈接的AutoCommit屬性是否爲FALSE,若是是,就自動將其置爲TRUE。

所以,在這個鏈接被交還回鏈接池時,AutoCommit屬性老是TRUE。

結論

任何查詢接口都應該在獲取鏈接之後進行AutoCommit的設置,將其設置爲true。

原創內容,轉載請註明出處

http://www.cnblogs.com/wingsless/p/6803542.html

相關文章
相關標籤/搜索