我就想加个索引,怎么就这么难?

领导让我SQL优化,我直接把服务干挂了...html

前言

MySQL大表加字段或者加索引,是有必定风险的。mysql

大公司通常有DBA,会帮助开发解决这个痛点,但是DBA是怎么作的呢?算法

小公司没有DBA,做为开发咱们的责任就更大了。那么咱们怎么才能安全的加个索引呢?sql

今天,咱们经过模拟案例以及原理分析,去弄清楚MySQLDDL的风险,以及如何避免事故发生。shell

准备

软件以及项目

  1. 安装本地版本MySQL。
  2. 一个简单的增删改查项目。
  3. 使用JMeter进行并发请求测试。

建立表

# 若是存在user表则删除
DROP TABLE  IF EXISTS user;

# 建立user表
CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `name` varchar(10) DEFAULT NULL COMMENT '姓名',
  `age` int(2) DEFAULT NULL COMMENT '年龄',
  `address` varchar(30) DEFAULT NULL COMMENT '地址',
  `description` varchar(100) DEFAULT NULL COMMENT '描述',
  `test_id` bigint DEFAULT NULL COMMENT '测试 id',
  `create_time` timestamp NULL DEFAULT NULL COMMENT '建立时间',
  `modify_time` timestamp NULL DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='mysql ddl测试表';

建立存储过程

# 若是存在test存储过程则删除
DROP PROCEDURE IF EXISTS `test`;

# 建立无参存储过程,名称为test
CREATE PROCEDURE test()

BEGIN
    # 声明变量
    DECLARE i INT;
    # 变量赋值
    SET i = 0;
    # 结束循环的条件: 当i等于100万时跳出while循环
    WHILE i < 1000000 DO
    # 往t_test表添加数据
    INSERT INTO `test`.user (`name`, `age`, `address`, 
                             `description`, `test_id`, `create_time`, `modify_time`)
    VALUES ('iisheng', 26, '北京', '如逆水行舟', LAST_INSERT_ID() + 1, 
            '2020-05-17 16:01:44', '2020-05-17 16:01:51');

    # 循环一次, i加1
    SET i = i + 1;
    # 结束while循环
    END WHILE;

END

下面的建立存储过程语句,是在IDE内选择代码块执行的,若是在Terminal中执行,须要使用DELIMITER关键字,更改语句结束标志。数据库

调用存储过程,生成百万数据

CALL test();

开启慢SQL日志

# 查看MySQL是否开启慢日志记录
SHOW VARIABLES LIKE 'slow_query_log';

# 开启慢SQL日志记录
SET GLOBAL slow_query_log = 'ON';

# 查看慢SQL日志位置
SHOW VARIABLES LIKE 'slow_query_log_file';

# 查看执行多久的SQL才算慢SQL
SHOW VARIABLES LIKE 'long_query_time';

# 设置慢SQL执行时间 只有新session才生效
SET GLOBAL long_query_time = 1;

一般状况下这些会在MySQL的配置文件中配置,启动时生效。安全

几个有用的SQL语句

# 展现哪些线程正在运行
SHOW PROCESSLIST;

# 查看正在执行的事务
SELECT * FROM information_schema.INNODB_TRX;

# 查看正在锁的事务
SELECT * FROM information_schema.INNODB_LOCKS;

# 查看正在等待锁的事务
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

# 显示innodb存储引擎状态的大量信息,包含死锁日志
SHOW ENGINE INNODB STATUS ;

# 展现数据库最大链接数的配置
SHOW VARIABLES LIKE 'max_connections';

# 查看存在哪些触发器
SELECT * FROM information_schema.TRIGGERS;

# 查看MySQL版本
SELECT VERSION();

后面咱们会主要用前两条。微信

事故现场

说明

  1. 我建立的user表除了主键是没有其余索引的。
  2. 测试的user表数据量为一百万。
  3. 测试MySQL版本为5.7.28
  4. 测试项目的逻辑:随机get()、list()、update()、create(),每一个操做都开启事务,而且休眠500毫秒。

步骤

运行测试项目session

项目启动图

这里咱们能够看到,项目已经正常启动了。并发

postman调用一下接口

接口请求图

这里咱们随便测试一个接口,请求时间2秒左右。

执行JMeter的Test Plan,观察项目日志

JMeter配置图

这里咱们建立了四个线程组,每一个线程组调用一个咱们的接口。模拟10我的循环1000次的访问。

正常项目日志图

这里咱们看到该请求频率下,日志无异常。

慢SQL日志

慢SQL日志图

这里咱们看到,百万级的SQL,若是没加索引SQL执行时间仍是比较长的,有的已经达到了2s。

加个索引,再观察项目日志

加索引过程日志图

这里咱们看到,项目已经开始报错了,大量的Connection is not available, request timed out after 30001ms

SHOW PROCESSLIST 一下

PROCESSLIST图

这里咱们看到,有大量的Waiting for table metadata lock

postman再次调用一下接口

请求接口报错图

这个时候,调用接口已经报错了,响应时间也比较久。此时,服务对用户来讲,已经基本不可用了。

为何会这样?

我就想加个索引,怎么就这么难?

看吧,就由于我加了个索引,服务就挂了,我没加以前仍是好好的。遇到问题,咱们要冷静,不是咱们的锅坚定不能背,真的是咱们的问题,下次必定要记得改正。那么,此刻的服务为何就不可用了呢?

首先咱们要知道,在InnoDB事务中,锁是在须要的时候才加上的,但并非不须要了就马上释放,而是要等到事务结束时才释放。这个就是两阶段锁协议

而后,在MySQL5.5版本中引入了MDL(Metadata Lock),当对一个表作增删改查操做的时候,加MDL读锁;当要对表作结构变动操做的时候,加MDL写锁。

咱们能够简单的尝试一下下面的状况。

DDL锁等待图

Session A开启一个事务,执行了一个简单的查询语句。此时,Session B,执行另外一个查询语句,能够成功。接着,Session C执行了一个DDL操做,加了个字段,由于Session A的事务没有提交,并且Session A持有MDL读锁,Session C获取不到MDL写锁,因此Session C堵塞等待MDL写锁。又因为MDL写锁获取优先级高于MDL读锁,所以Session D这个时候也获取不到MDL读锁,等待Session C获取到MDL写锁以后它才能获取到MDL读锁。

咱们发现,DDL操做以前若是存在长事务,一直不提交,DDL操做就会一直被堵塞,还会间接的影响后面其余的查询,致使全部的查询都被堵塞。

这也就是为何咱们把服务干挂的缘由了。

目前主流解决方案

针对上面出现的状况,咱们怎么解决呢?

MySQL5.6的Online DDL

MySQL5.6开始,支持Online DDL。相似于这种的语句ALTER TABLE user ADD INDEX idx_test_id (test_id), ALGORITHM=INPLACE, LOCK=NONE在普通的ALTER TABLE或者CREATE INDEX语句后面添加ALGORITHM参数和LOCK参数。

实际上,ALTERT TABLE语句若是不加ALGORITHM参数,默认就会选择ALGORITHM=INPLACE的形式,若是执行的语句支持INPLACE,不然,会使用ALGORITHM=COPY

之前写SQL只会ALTER TABLE不知道后面还能够加ALGORITHM参数,后来知道了Online DDL,知道了能够加ALGORITHM=INPLACE,结果两种写法有的时候是同样的...

MySQL官网截图

这里顺便提一句,学习的途径有不少,可是官网,的确能够多看看。

使用pt-online-schema-change

简单说一下怎么安装这个东西

首先官网下载,而后校验以及安装,执行下面命令

perl Makefile.PL
make
make install

而后使用CPAN安装相关依赖(适用Unix),CentOS下直接yum更简单

perl -MCPAN -e shell
cpan> install DBI
cpan> install DBD::mysql

我本身Mac安装没啥问题,公司Mac安装失败了,而后升级了一下Perl版本就能够了。

语法

pt-online-schema-change --charset=utf8 --no-check-replication-filters --no-version-check --user=user --password=pass --host=host_addr  P=3306,D=database,t=table --alter "ADD INDEX idx_name(field_name)" --execute

个人脚本添加索引

pt-online-schema-change --charset=utf8 --no-check-replication-filters --no-version-check --user=root --password=mGy6GAzdawFPTJ7R --host=127.0.0.1  P=3306,D=test,t=user --alter "add INDEX idx_test_id(test_id)" --execute

使用pt-osc测试

pt-osc执行图

这里咱们看到,pt-osc建立触发器的时候卡在那了。实际上这里也是在等待锁。

最终成功了,可是整个过程时间比较久。过程当中咱们也发现了一些死锁的日志。

pt-osc死锁日志

其实,这个跟个人代码有必定的关系,个人测试代码随机数生成的范围是[0, 20000],而后我根据生成的随机数,去查询数据库,锁的冲突会比较多。把范围修改成[0, 1000000]会好不少。

再看Online DDL

由于刚才咱们发现了,本身代码写的有一些问题,因此咱们刚才的结论也有一些影响。咱们把随机数的范围改到100万,从新测试一遍。

Online DDL 成功

此次Online DDL也成功了。可是也是有一些链接超时的日志。以前的测试若是一直执行下去,也会成功,只不过堵塞时间太长,对用户影响太大,我就中止算执行失败了。

实际效果跟机器性能也是有一些关系的,这里的关键点在于拿MDL写锁的等待时间,这个时间稍微久一些就会对用户形成很大的影响。

pt-osc执行过程

  1. 建立一个和原表表结构同样的临时表(_tablename_new),执行alter修改临时表表结构。
  2. 在原表上建立3个与insert delete update对应的触发器,用于copy数据的过程当中,在原表的更新操做,更新到新表。
  3. 从原表拷贝数据到临时表,拷贝过程当中在原表进行的写操做都会更新到新建的临时表。
  4. rename原数据表为old表,把新表rename为原表名,并将old表删除。
  5. 删除触发器。

这里面建立、删除触发器和rename表的时候都会尝试获取DML写锁,若是获取不到会等待。就是咱们看到的Waiting for table metadata lock

因此,这些时间段若是长时间获取不到锁,就会一直堵塞,仍是会出现问题的。

Online DDL执行过程

  1. MDL写锁
  2. 降级成MDL读锁
  3. 真正作DDL
  4. 升级成MDL写锁
  5. 释放MDL

一、4若是没有锁冲突,执行时间很是短。第3步占用了DDL绝大部分时间,这期间这个表能够正常读写数据,所以称为online

可是,若是拿锁的时候没拿到,或者升级MDL写锁不能成功,就会等待,咱们又会看到Waiting for table metadata lock,而后就接着的一系列问题了。

总结

加个索引,说难也难,说不难也不难。若是数据量大,又存在长事务,加索引的过程又有用户访问,Online DDLpt-osc都不能保证对业务没有影响。可是若是咱们SQL的执行时间比较短,或者咱们加索引的时候,对应的业务没有多少请求。那么咱们就能够很快的加完索引。

加字段也是相似的过程,可是若是咱们能保证没有慢SQL,那么就不会存在长事务,那么执行时间就会很快,对用户就能够作到几乎没有影响。至于选择Online DDL仍是pt-osc就要看他们的一些限制以及本身的场景需求了。感兴趣的同窗,本身尝试一下。

最后想说

当万丈高楼崩塌的时候,超人也不能将它复原。咱们应该作的,是有一个好的规范,好的认知,好的监控,在问题没有出现的时候,就将问题扼杀在摇篮中。而不是让问题,日渐壮大,大到覆水难收...

参考文献:

[1]:《MySQL实战45讲》

[2]: https://dev.mysql.com/doc/refman/5.7/en/

[3]: https://www.percona.com/doc/percona-toolkit/3.0/pt-online-schema-change.html

欢迎关注我的微信公众号【如逆水行舟】,用心输出基础、算法、源码系列文章。

相关文章
相关标签/搜索