1 | mysql> CREATE TABLE `t` ( |
第一类:查询长时间不返回
select * from t where id = 1;查询结果长时间不返回。
一般碰到这种情况,大概率是表t被锁住了。
使用show processlist命令查看当前语句处于什么状态。
show processlist;
或者select * from information_schema.processlist;
等MDL锁
Id | User | Host | db | Command | Time | State | Info |
---|---|---|---|---|---|---|---|
5 | root | localhost:61558 | test | Query | 0 | init | show processlist |
7 | root | localhost:63852 | test | Sleep | 31 | NULL | |
8 | root | localhost:63870 | test | Query | 25 | Waiting for table metadata lock | select * from t where id = 1 |
Waiting for table metadata lock;表示有一个线程正在表t上请求或者持有MDL写锁,把select语句堵住了。(MySQL5.7修改了MDL的加锁策略,尽量避免此类问题。)
复现
sessionA | sessionB |
---|---|
lock table t write; | |
select * from t where id=1; |
sessionA通过lock table命令持有表t的MDL写锁(MySQL5.5,阻止其他线程访问)(InnoDB中除非明确些lock table,否则不会锁表),sessionB的查询需要获取MDL读锁。sessionB进入等待状态。
解决
找到谁持有MDL写锁,kill掉。通过performance_schema和sys系统库处理。(MySQL启动时设置performance_schema=on;相比于设置为off会有10%左右的性能损失)。
select blocking_pid from sys.schema_table_lock_waits;
select * from performance_schema.metadata_locks;
通过查询sys.schema_table_lock_waits这张表,直接找出造成阻塞的process id(blocking_pid),把这个连接用kill命令断开即可。
等flush
Id | User | Host | db | Command | Time | State | Info |
---|---|---|---|---|---|---|---|
4 | root | localhost:49548 | test | Query | 38 | User sleep | select sleep()1 from t |
5 | root | localhost:49604 | test | Query | 35 | Waiting for table flush | sflush tables t |
6 | root | localhost:49634 | test | Query | 30 | Waiting for table flush | select * from t where id=1 |
7 | root | localhost:49726 | test | Query | 0 | starting | show processlist |
Waiting for table flush:表示有一个线程正要对表t做flush操作。有一个flush tables命令被别的语句堵住了(flush之前必须等待所有语句执行完成(包括select)),然后它又堵住了select语句。MySQL对表做flush操作有两种用法
- flush tables t with read lock;– 只关闭表t;
- flush tables with read lock;– 关闭MySQL里所有打开的表(FTWRL加全局读锁,让整个库处于只读状态。flush过程中(关闭表),查询也不行???);
复现
sessionA | sessionB | sessionC |
---|---|---|
select sleep(1) from t; | ||
flush tables t; | ||
select * from t where id=1; |
sessionA中每行都调用一次sleep(1),这个语句默认要执行10W秒,在这期间表t一直是被sessionA“打开”着。sessionB的flush tables t命令要去关闭表t,就需要等sessionA的查询结束。sessionC再次查询的话,会被flush命令堵住。
解决
同上
等行锁(InnoDB引擎)
Id | User | Host | db | Command | Time | State | Info |
---|---|---|---|---|---|---|---|
4 | root | localhost:65224 | test | Query | 0 | starting | show processlist |
8 | root | localhost:10354 | test | Query | 1 | statistics | select * from t where id=1 lock in share mode |
10 | root | localhost:11276 | test | Sleep | 52 | NULL |
访问id=1这个记录时要加读锁,如果这时已经有一个事务在这行记录上持有一个写锁,select语句就会被堵住。
复现
sessionA | sessionB |
---|---|
begin; | |
update t set c=c+1 where id=1; | |
select * from t where id=1 lock in share mode; |
sessionA启动了事务,占有写锁,还不提交,导致sessionB被堵住。
解决
MySQL5.7,通过sys.innodb_lock_waits表查到谁占着写锁,kill掉链接。
select blocking_pid from t sys.innodb_lock_waits where locked_table=’test
.t
‘;
假设4号线程是造成堵塞的罪魁祸首,”kill query 4”命令表示停止4号线程当前正在执行的语句,这个方法没有用。占行锁的是update语句,这个语句已经是之前执行完成了的,执行kill query无法让这个事务去掉id=1上的行锁。
“kill 4”命令表示直接断开这个链接。隐含的逻辑是,连接被断开的时候,会自动回滚这个链接里面正在执行的线程,也就释放了id=1上的行锁。
第二类:查询慢
扫描行数多
set long_query_time=0;– 慢查询日志的时间阈值设置为0;
select * from t where c=50000 limit 1;
Query_time:0.011543 Lock_time:0.000104 Rows_sent:1 Rows_examined:50000 SET timestamp=1544723147;– slow log慢查询日志
字段c上没有索引,这个语句只能走id主键顺序扫描,需要扫描5w行。
扫描一行,一致性读事务期间大量更新
select * from t where id=1;
Query_time:0.804400 Lock_time:0.000132205 Rows_sent:1 Rows_examined:1 SET timestamp=1544728393;
select * from t where id=1 lock in share mode;
Query_time:0.000258 Lock_time:0.000132 Rows_sent:1 Rows_examined:1 SET timestamp=1544728398;
sessionA | sessionB |
---|---|
start transaction with consistent snapshot | |
update t set c=c+1 where id=1;//执行100W次 | |
select * from t where id=1; | |
select * from t where id=1 lock in share mode; |
sessionA先用start transaction with consistent snapshot命令启动了一个事务,之后sessionB才开始执行update语句。
sessionB更新完100W次,生成了100W个回滚日志(undo log)。
带lock in share mode的SQL语句,是当前读,直接读到1000001这个结果,所以速度很快;而select * from t where id=1这个语句,是一致性读,需要从当前1000001开始,依次执行undo log,执行100W次后,才将1这个结果返回,所以速度慢。
扩展
只要访问表,增删改查就要加MDL读锁。快照读没有读锁。
read-commited隔离级别下,update语句有一个“semi-consistent”read优化:
一个update语句,如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的最新版本,由MySQL上层判断此版本是否满足update的where条件。若满足(需要更新),则MySQL会重新发起一次读操作,此时会读取行的最新版本并加锁(锁等待);对于扫描返回的不满足条件的记录,会提前释放锁。
(如果update语句碰到一个已经被锁了的行,会读入最新的版本,然后判断一下是不是满足查询条件,如果不满足则直接跳过,如果满足就进入锁等待。在语句执行完成后,只有行锁,而且语句执行完成后,InnoDB就会把不满足条件的行的行锁去掉,满足条件的行锁等到commit的时候才会释放)
repeatable-read隔离级别下,对于具有唯一搜索条件的唯一索引,InnoDB只锁定找到的索引记录,而不锁定其前面的间隙。对于update或delete语句,InnoDB只对其更新或者删除的行持有锁。在MySQL评估where条件之后,将释放非匹配行的记录锁,大大降低了死锁的可能性,但死锁仍可能发生。
为了保证binlog记录顺序,非索引更新则会锁住全表记录(无索引字段,一行一行逐一给所有行加行锁(在主键索引上),不会加表锁),且事务结束前不会对不符合条件记录有逐步释放的过程。