测试中发现,服务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属性。