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

相关文章
相关标签/搜索