原創內容,轉載請注明出處
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:
但是,如果AutoCommit不是默認開啟呢?結果就會變成下面的表格,表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。
原創內容,轉載請注明出處