MySQL AutoCommit帶來的問題


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

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

現象描述

測試中發現,服務A在得到了服務B的注冊用戶成功response以后,開始調用查詢用戶信息接口,卻發現無法查詢出任何結果。檢查binlog發現,在查詢請求之前,數據庫確實已經完成了commit操作,並且可以在sqlyog等客戶端工具中查詢出正確的結果。

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

時序圖

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

問題分析

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

顯然這並不能支持剛才的設想,但是結論一定是正確的,就是因為事務隔離級別導致了Server A讀到的永遠是快照,發生了可重復讀。

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

表1

但是,如果AutoCommit不是默認開啟呢?結果就會變成下面的表格,表2:

表2

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

那么在連接池條件下,情況如何?

設置一個極端條件,連接池只給一個連接,編寫兩個類,一個負責插入數據,一個負責循環讀取數據,但是讀取數據的類在執行讀取方法之前,會執行一個空方法,這個方法只會做一件事情,就是獲取連接,將其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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM