原創內容,轉載請註明出處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
可是,若是AutoCommit不是默認開啓呢?結果就會變成下面的表格,表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);
此時這個問題再也不出現。
Connection是Java提供的一個標準接口:java.sql.Connection,其具體實現是:com.mysql.jdbc.ConnectionImpl。
分析jdbc驅動代碼可知,jdbc默認的AutoCommit狀態是TRUE:
這實際上和MySQL的默認值是同樣的。
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的Connection具體實現,發如今close方法的具體實現中,有這樣的一段代碼邏輯:
if (!getAutoCommit()) { setAutoCommit(true); }
這段邏輯會判斷該鏈接的AutoCommit屬性是否爲FALSE,若是是,就自動將其置爲TRUE。
所以,在這個鏈接被交還回鏈接池時,AutoCommit屬性老是TRUE。
任何查詢接口都應該在獲取鏈接之後進行AutoCommit的設置,將其設置爲true。
原創內容,轉載請註明出處