MySQL优化之提高插入效率以及带来的问题

下面是mysql关闭自动提交带来的效率提升很好, 但是弄不好,后面有个例子造成了长事务就是不好的情况了。

MySQL优化之提高插入效率(全局关闭autocommit)

近来做mysql的导入时,发现插入速度极慢。5W多条数据要花半个小时,难以忍受。在网上查阅资料,总结出来这个方法。 即 关闭Mysql的自动提交 因为我是qt+mysql 配合使用,所以需要全局关闭。关闭后插入5W多条数据只需要29S

MySQL默认的数据提交操作模式是自动提交模式(autocommit)。这就表示除非显式地开始一个事务,否则每个查询都被当做一个单独的事务自动执行。

Mysql的InnoDB存储引擎支持事务处理。InnoDB表引擎下关闭mysql自动事务提交可以大大提高数据插入的效率,这是因为如果需要插入1000条数据,mysql会自动发起(提交)1000次的数据写入请求,如果把autocommit关闭掉,通过程序来控制,只要一次commit就可以搞定,这是会大大提高速率。关闭后如下图

所以说应该将存储引擎改为InnoDB,具体方法不做讲述。

首先查询当前数据库事务提交方式的命令为:
show variables like "autocommit"
查询结果如下(如果是off 就可以关闭这篇文章了)

下面说一下具体操作方法

更改配置文件
在MySQL的安装根目录下 找到my.ini .使用记事本打开,在[mysqld] 后面加入**init_connect=‘SET autocommit=off’ ** 如果没有my.ini 请新建一个记事本 把图片的内容输入进去,并更改名字。

容易出问题, 后面给出了。

重新启动mysql服务

打开管理员模式的cmd输入
net stop MySQL

net start MySQL

创建子权限用户
这一步是因为 当我们以Super权限(root)登录mysql数据库服务时,show variables like '%autocommit%'查得结果还是:autocommit ON,这是因init_connect(手册中有对其解释)以Super用户登录是不被加载的,这是mysql出于安全考虑,而后用普通用户登录,show variables like ‘%autocommit%’ 显示的结果如下:autocommit OFF。

所以我们就登陆root 创建一个用户 授予他你想操作数据库的权限。使用该用户登陆MySQL即可。

mysql -uroot -p //登陆root账户 输入完毕后按两下回车
CREATE USER 'tomato'@'localhost' IDENTIFIED BY '123456'; //三个单引号内容 用户名 端口号 密码
GRANT [ALL] ON [cg].[*] TO ‘tomato’@’localhost’; // 实际输入没有[] 为了方便解释才采用
// 第一个为权限名 all是所有权限 第二个是数据库名 第三个 *代表cg数据库的所有表
避免长事务

将autocommit设置为0 很容易导致长事务的产生。
而长事务会存在很多潜在风险,且会占用磁盘资源。

mysql的事务启动方式有以下几种:

显式启动事务语句,begin start transaction 配套的commit 回滚语句roll back。
set autocommit=0.这个命令会将这个线程的自动提交关闭。意味着执行一个select语句的时候,事务就启动了,并且不自动提交。知道主动执行commit或者rollback,或者断开连接。
使用第二种方式会导致长事务。
所以建议显式指定begin和commit。
另外 begin 后 输入 commit work and chain 会提交当前事务,开启下一次事务。

可以通过

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

查询持续时间超过60s的事务。

到此为止我们就大功告成了,去试试此时插入的速度把。
还有一个记录了mysql优化插入的博客

原文链接:https://blog.csdn.net/sunburst_lf/article/details/81206222

---------------------

下面是一个长事务造成的问题, 请引起注意

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