领导让我
SQL
优化,我直接把服务干挂了...html
MySQL
大表加字段或者加索引,是有必定风险的。mysql
大公司通常有DBA
,会帮助开发解决这个痛点,但是DBA
是怎么作的呢?算法
小公司没有DBA
,做为开发咱们的责任就更大了。那么咱们怎么才能安全的加个索引呢?sql
今天,咱们经过模拟案例以及原理分析,去弄清楚MySQL
中DDL
的风险,以及如何避免事故发生。shell
# 若是存在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();
# 查看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的配置文件中配置,启动时生效。安全
# 展现哪些线程正在运行 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();
后面咱们会主要用前两条。微信
user
表除了主键是没有其余索引的。user
表数据量为一百万。MySQL
版本为5.7.28
。运行测试项目session
这里咱们能够看到,项目已经正常启动了。并发
postman
调用一下接口
这里咱们随便测试一个接口,请求时间2秒左右。
执行JMeter的Test Plan,观察项目日志
这里咱们建立了四个线程组,每一个线程组调用一个咱们的接口。模拟10我的循环1000次的访问。
这里咱们看到该请求频率下,日志无异常。
慢SQL日志
这里咱们看到,百万级的SQL,若是没加索引SQL执行时间仍是比较长的,有的已经达到了2s。
加个索引,再观察项目日志
这里咱们看到,项目已经开始报错了,大量的Connection is not available, request timed out after 30001ms
。
SHOW PROCESSLIST
一下
这里咱们看到,有大量的Waiting for table metadata lock
。
postman
再次调用一下接口
这个时候,调用接口已经报错了,响应时间也比较久。此时,服务对用户来讲,已经基本不可用了。
我就想加个索引,怎么就这么难?
看吧,就由于我加了个索引,服务就挂了,我没加以前仍是好好的。遇到问题,咱们要冷静,不是咱们的锅坚定不能背,真的是咱们的问题,下次必定要记得改正。那么,此刻的服务为何就不可用了呢?
首先咱们要知道,在InnoDB
事务中,锁是在须要的时候才加上的,但并非不须要了就马上释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
而后,在MySQL5.5
版本中引入了MDL(Metadata Lock)
,当对一个表作增删改查操做的时候,加MDL
读锁;当要对表作结构变动操做的时候,加MDL
写锁。
咱们能够简单的尝试一下下面的状况。
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操做就会一直被堵塞,还会间接的影响后面其余的查询,致使全部的查询都被堵塞。
这也就是为何咱们把服务干挂的缘由了。
针对上面出现的状况,咱们怎么解决呢?
Online DDL
MySQL
从5.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
,结果两种写法有的时候是同样的...
这里顺便提一句,学习的途径有不少,可是官网,的确能够多看看。
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
建立触发器的时候卡在那了。实际上这里也是在等待锁。
最终成功了,可是整个过程时间比较久。过程当中咱们也发现了一些死锁的日志。
其实,这个跟个人代码有必定的关系,个人测试代码随机数生成的范围是[0, 20000]
,而后我根据生成的随机数,去查询数据库,锁的冲突会比较多。把范围修改成[0, 1000000]
会好不少。
Online DDL
由于刚才咱们发现了,本身代码写的有一些问题,因此咱们刚才的结论也有一些影响。咱们把随机数的范围改到100万,从新测试一遍。
此次Online DDL
也成功了。可是也是有一些链接超时的日志。以前的测试若是一直执行下去,也会成功,只不过堵塞时间太长,对用户影响太大,我就中止算执行失败了。
实际效果跟机器性能也是有一些关系的,这里的关键点在于拿
MDL
写锁的等待时间,这个时间稍微久一些就会对用户形成很大的影响。
pt-osc
执行过程_tablename_new
),执行alter
修改临时表表结构。insert
delete
update
对应的触发器,用于copy
数据的过程当中,在原表的更新操做,更新到新表。rename
原数据表为old
表,把新表rename
为原表名,并将old
表删除。这里面建立、删除触发器和rename
表的时候都会尝试获取DML
写锁,若是获取不到会等待。就是咱们看到的Waiting for table metadata lock
。
因此,这些时间段若是长时间获取不到锁,就会一直堵塞,仍是会出现问题的。
Online DDL
执行过程MDL
写锁MDL
读锁DDL
MDL
写锁MDL
锁一、4
若是没有锁冲突,执行时间很是短。第3步占用了DDL
绝大部分时间,这期间这个表能够正常读写数据,所以称为online。
可是,若是拿锁的时候没拿到,或者升级MDL
写锁不能成功,就会等待,咱们又会看到Waiting for table metadata lock
,而后就接着的一系列问题了。
加个索引,说难也难,说不难也不难。若是数据量大,又存在长事务,加索引的过程又有用户访问,Online DDL
和pt-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
欢迎关注我的微信公众号【如逆水行舟】,用心输出基础、算法、源码系列文章。