一、避免死锁
1.1、导致mysql死锁的要素
1、两个或者两个以上事务。
2、锁资源只能被同一个事务持有或者多个事务竞争的锁是不兼容的,比如排他锁和共享锁、排他锁和排他锁。
3、每个事务都已经持有锁并且申请新的锁。
4、事务之间因为持有锁和申请锁导致彼此循环等待。
1.2、死锁演示
-- 事务1中执行
begin;
select * from user_innodb where id=3 for update;
-- 事务2中执行
begin;
delete from user_innodb where id=4;
-- 事务1中执行
update user_innodb set name='haoya' where id=4;
-- 事务2中执行
delete from user_innodb where id=3;
在上述脚本运行后,会在第一个事务中看到下面这样的日志
update user_innodb set name='haoya' where id=4
> 1213 - Deadlock found when trying to get lock; try restarting
transaction
> 时间: 2.475s
Mysql立刻检测到了死锁,并且事务1马上退出了。
为什么可以直接检测到呢?是因为死锁的发生需要满足一定的条件,所以在发生死锁时,InnoDB一般都能通过算法(wait-for graph)自动检测到。
1.3、事务竞争锁的超时时间
另外还有一个要注意的点,就是事务竞争锁的时候,如果发现当前有事务正在持有锁,那么它不会一直等下去。默认情况下,它会等待50s的时间,如果50s还没有办法竞争到锁,则直接释放锁资源。
-- 事务1中执行
begin;
select * from user_innodb where id=3 for update;
-- 事务2中执行
begin;
select * from user_innodb where id=3 for update;
上面这个的执行结果如下:
select * from user_innodb where id=3 for update
> 1205 - Lock wait timeout exceeded; try restarting transaction
> 时间: 51.011s
事务竞争锁超时的时间,是通过下面这个属性来控制的
show VARIABLES like 'innodb_lock_wait_timeout';
所以,一个事务的锁的释放,有三种情况:
- 事务结束(commit/rollback)
- 客户端连接断开
- 事务竞争锁超时(50s)
1.4、查看锁的日志
如果我们需要定位当前数据库吞吐量下降的原因是否是锁竞争或者死锁导致的,应该怎么去判断呢?
在Mysql中提供了一些属性,可以看到整个数据库锁的使用情况。
show status like 'innodb_row_lock_%';
上述参数说明:
Innodb_row_lock_current_waits:当前正在等待锁定的数量;
Innodb_row_lock_time :从系统启动到现在锁定的总时间长度,单位ms;
Innodb_row_lock_time_avg :每次等待锁的平均时间;
Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间;
Innodb_row_lock_waits :从系统启动到现在总共等待的次数;
上面只是一个总的信息,如果想看到更加详细的数据,Mysql提供了下面几个命令:
- 查看当前运行的所有事务 ,还有具体的SQL语句;
select * from information_schema.INNODB_TRX;
- 查看处于锁等待的详细信息
select * from sys.innodb_lock_waits;
- 查看正在处理中的事务
select * from information_schema.PROCESSLIST p where p.state<>'';
-- 锁定数据查询
select * from performance_schema.data_locks;
基于这些操作命令,可以定位到死锁的源头,比如select * from sys.innodb_lock_waits;这个语句中的最后两列,提供了两个属性:
sql_kill_blocking_query kill掉这个query语句的线程
sql_kill_blocking_connection kill条这个阻塞的连接
通过提供的参考命令,可以直接把锁等待的线程终止。
也可以通过select * from information_schema.INNODB_TRX;这个语句中查询到的trx_mysql_thread_id,就是这个事务的线程id,通过kill trx_mysql_thread_id来终止。
当然,死锁的问题不能每次都靠kill线程来解决,这是治标不治本的行为。我们应该尽量在应用端,也就是在编码的过程中避免。有哪些可以避免死锁的方法呢?
1.5、如何防止死锁
- 在程序中,操作多张表时,尽量以相同的顺序来访问(避免形成等待环路);
- 尽量避免大事务,占有的资源锁越多,越容易出现死锁。建议拆成小事务。
- 降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。
- 为表添加合理的索引。防止没有索引出现表锁,出现的死锁的概率会突增。
二、Mysql优化
三、Sql优化
SQL分析主要有两个切入点:
- EXPLAIN 执行计划分析
- 数据库慢SQL查询
3.1、EXPLAIN执行计划分析
MySQL 提供了一个 EXPLAIN 命令, 它可以对 SELECT 语句进行分析, 并输出SELECT 执行计划的详细信息, 这些信息给到开发人员参考并作出优化的方向。
为了更好的展示Explain的效果,首先来初始化一个SQL脚本
-- 创建课程表
DROP TABLE IF EXISTS course;
CREATE TABLE `course` (
`cid` int(3) DEFAULT NULL,
`cname` varchar(20) DEFAULT NULL,
`tid` int(3) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 创建教师表
DROP TABLE IF EXISTS teacher;
CREATE TABLE `teacher` (
`tid` int(3) DEFAULT NULL,
`tname` varchar(20) DEFAULT NULL,
`tcid` int(3) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 创建教师联系方式表
DROP TABLE IF EXISTS teacher_contact;
CREATE TABLE `teacher_contact` (
`tcid` int(3) DEFAULT NULL,
`phone` varchar(200) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入数据
INSERT INTO `course` VALUES ('1', 'mysql', '1');
INSERT INTO `course` VALUES ('2', 'jvm', '1');
INSERT INTO `course` VALUES ('3', 'juc', '2');
INSERT INTO `course` VALUES ('4', 'spring', '3');
INSERT INTO `teacher` VALUES ('1', 'qingshan', '1');
INSERT INTO `teacher` VALUES ('2', 'huihui', '2');
INSERT INTO `teacher` VALUES ('3', 'mic', '3');
INSERT INTO `teacher_contact` VALUES ('1', '13688888888');
INSERT INTO `teacher_contact` VALUES ('2', '18166669999');
INSERT INTO `teacher_contact` VALUES ('3', '17722225555');
接着我们执行下面一个语句
explain select * from course;
在Explain中可以看到很多的数据列,下面分别来说明一下每个数据列的含义。
官方文档
3.1.1、id
id是查询序列编号,每张表都是单独访问的,一个SELECT就会有一个序号,比如下面这样一个sql。
explain select tc.phone from teacher_contact tc where tcid=
(select tcid from teacher t where t.tid=
(select c.tid from course c where c.cname='mysql'));
对应的执行计划如下:
查询顺序:course c——teacher t——teacher_contact tc。
先查课程表,再查老师表,最后查老师联系方式表。子查询只能以这种方式进行,只有拿到内层的结果之后才能进行外层的查询。
还有一种查询的情况,id的值相同,查询语句如下:
-- 查询课程ID为2,或者联系表ID为3的老师
EXPLAIN
SELECT t.tname,c.cname,tc.phone
FROM teacher t, course c, teacher_contact tc
WHERE t.tid = c.tid
AND t.tcid = tc.tcid
AND (c.cid = 2
OR tc.tcid = 3);
执行计划如下图所示:
这种连接查询的情况下,id值相同,表的查询顺序是从上往下执行。
查询的顺序是teacher t(3条)——course c(4条)——teacher_contact tc(3条)。
在连接查询中,先查询的叫做驱动表,后查询的叫做被驱动表。
应该先查小表(得到结果少的表)还是大表(得到结果多的表)?我们肯定要把小表放在前面查询,因为它的中间结果最少。 (小表驱动大表的思想)
因此,总结来看: ID不同的先大后小,ID相同的从上往下。
3.1.2、select_type
select_type表示查询类型,它的常用取值类型如下:
- SIMPLE, 表示此查询不包含 UNION 查询或子查询
- PRIMARY, 表示此查询是最外层的查询
- UNION, 表示此查询是 UNION 的第二或随后的查询
- DEPENDENT UNION, UNION 中的第二个或后面的查询语句, 取决于外面的查询
- UNION RESULT, UNION 的结果
- SUBQUERY, 子查询中的第一个 SELECT
- DEPENDENT SUBQUERY: 子查询中的第一个 SELECT, 取决于外面的查询. 即子查询依赖于外层查询的结果.
- DERIVED: 在from列表中包含的子查询会被标记为DERIVED(衍生),MySQL会递归执行这些子查询,将结果放在临时表中。
下面对常规类型做一个简单的说明:
- SIMPLE
简单查询,不包含子查询,不包含关联查询union
EXPLAIN SELECT * FROM teacher;
执行计划如下图所示:
- PRIMARY/SUBQUERY
执行下面这种包含多个子查询的语句
select tc.phone from teacher_contact tc where tcid=
(select tcid from teacher t where t.tid=
(select c.tid from course c where c.cname='mysql'));
执行计划如下:
从上图的执行计划中可以看到,它有两种类型:
- primary, 表示SQL语句中的主查询,也就是最外层的查询。
- subquery, 子查询中的所有内查询,都是subquery类型。
- DERIVED
DERIVED类型,衍生查询,表示的到最终查询结果之前会用到临时表,比如
-- 查询ID为1或2的老师教授的课程
EXPLAIN SELECT cr.cname
FROM (
SELECT * FROM course WHERE tid = 1
UNION
SELECT * FROM course WHERE tid = 2
) cr;
执行计划如下图所示:
注意看,id=2的查询类型是DERIVED,id=3的查询类型是UNION。执行顺序如下:
1、先执行UNION右边的表,也就是(SELECT * FROM course WHERE tid=2),再执行左边的表SELECT * FROM course WHERE tid = 1,此时左边表的类型是DERIVED。
2. UNION RESULT,显示那些表之间存在UNION查询,<union2,3> 表示id=2和id=3的查询存在union。
3.1.3、type连接类型
type 字段比较重要, 它提供了判断查询是否高效的重要依据,通过type 字段, 我们判断此次查询是全表扫描还是索引扫描等。type常见的取值类型有很多,比如system/const/eq_ref/ref/range/index/all等。下面详细展开说明一下
const
const: 针对主键或唯一索引的等值查询扫描, 最多只返回一行数据. const 查询速度非常快, 因为它仅仅读取一次即可。比如下面这个sql,根据主键id=1查询表的数据。
explain select * from user_innodb where id=1;
执行计划如下:
system
system: 表中只有一条数据. 这个类型是特殊的 const 类型。
对于MyISAM、Memory的表,只查询到一条记录,也是system。
-- 建表指定MyISAM存储引擎
CREATE TABLE `table1` (
`id` int NOT NULL,
`a` int DEFAULT NULL,
`b` int DEFAULT NULL,
`c` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_0900_ai_ci;
-- 插入一条数据
insert into table1 value(1,1,1,1);
-- 查看执行计划
explain select * from table1
执行计划如下
eq_ref
在多表连接查询中,被驱动表通过唯一索引(UNIQUE或PRIMARY KEY)进行访问的时候,被驱动表的访问方式就是eq_ref,通过下面这条sql进行演示
--增加主键索引
ALTER TABLE teacher_contact ADD PRIMARY KEY(tcid);
explain select t.tcid from teacher t,teacher_contact tc where t.tcid = tc.tcid;
执行计划如下:
ref
查询用到了非唯一性索引,或者关联操作只使用了索引的最左前缀,则连接类型是ref。
下面通过sql演示一下这种类型:
-- 增加普通索引
ALTER TABLE teacher ADD INDEX idx_tcid (tcid);
explain SELECT * FROM teacher where tcid = 3;
执行计划如下
range
索引范围扫描。
如果where后面是 between and 或 < 或 > 或 >= 或 <=或in这些,type类型就为range。
下面通过sql演示一下range范围检索
ALTER TABLE teacher ADD INDEX idx_tid (tid);
-- 执行范围查询(字段上有普通索引)
EXPLAIN SELECT * FROM teacher t WHERE t.tid <3;
EXPLAIN SELECT * FROM teacher t WHERE tid BETWEEN 1 AND 2;
-- IN查询也是range(字段有主键索引)
EXPLAIN SELECT * FROM teacher_contact t WHERE tcid in (1,2,3);
注意:
1、mysql中in中的参数个数不受限制,但是sql本身的长度会受到限制,默认是64M。官网参考
2、in通常是走索引的,当in后面的参数个数较多的情况下,就不会再走索引,直接走全表扫描。
index
Full Index Scan,全索引扫描(Full Index Scan)。index 和ALL最大的区别是,index类型只扫描索引树即可,所以它要比ALL的查询效率要快。比如在teacher表中,tid是主键,当我们只查询主键列tid时。
EXPLAIN SELECT tid FROM teacher;
执行计划如下:
all
Full Table Scan,如果没有索引或者没有用到索引,type就是ALL。代表全表扫描,比如下面这种情况必然是全表扫描。
EXPLAIN SELECT * FROM teacher;
执行计划如下:
null
不用访问表或者索引就能得到结果,例如:
EXPLAIN select 1 from dual;
执行计划如下:
3.1.4、possible_key、key
possible_key表示可能用到的索引,key表示实际用到的索引。如果这一列为空,表示没有用到索引。
possible_key可以有一个或者多个,当然,可能用到索引不代表一定用到索引。
下面演示一下这种索引的使用。
-- 首先需要创建一张表,并且建立一个联合索引
CREATE TABLE `user_info` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`gender` tinyint(1) DEFAULT NULL,
`phone` varchar(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `name_key` (`name`,`phone`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3000002 DEFAULT
CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 执行下面这个sql
explain select phone from user_info where phone='126';
执行计划如下图
这里用到了覆盖索引,也就是是直接在二级索引树上就获得了数据,所以possible_keys和key对应的是联合索引的名字(name_key)。
3.1.5、key_len
索引的长度(使用的字节数)。跟索引字段的类型、长度有关。比如我们执行下面这个sql
explain select * from user_innodb where name ='小明';
执行计划如下图
可以看到,key_len是1023。我们来看下这个1023是如何计算的,首先在name和phone上建立了联合索引,而name定义的长度是255、phone的长度是11。这里索引只用到了name字段,所以定义长度是255,而在utf8mb4编码中,一个字符占4个字节,所以255*4=1020。另外,使用可变长字段varchar,需要额外增加2个字节,允许NULL需要额外增加1个字节,所以一共是1023个字节。key_len越长,表示使用的索引范围越广,比如我们再使用下面这个sql执行一次
explain select * from user_innodb where name ='小明' and phone='13002811115';
可以看到,key_len=1070, 因为phone是varchar类型长度为11,字节长度=11*4 =44 再加三个字节一共是47。所以一共是1070个字节。
3.1.6、rows
MySQL认为扫描多少行才能返回请求的数据,是一个预估值,一般来说行数越少越好。
3.1.7、filtered
Filtered表示返回结果的行数占需读取行数的百分比 ,它只对index和all的扫描有效。
如果比例很低,说明存储引擎层返回的数据需要经过大量过滤,这个是会消耗性能的,需要关注。
官网参考
3.1.8、ref
使用哪个列或者常数和索引一起从表中筛选数据。
explain select * from user_info where name ='小明' and phone='13002811115';
执行计划如下图:
上面的ref,用到了两个常量进行数据的筛选。
3.1.9、Extra
EXplain 中的很多额外的信息会在 Extra 字段显示, 常见的有以下几种内容:
- Using filesort
当 Extra 中有 Using filesort 时, 表示 MySQL 需额外的排序操作, 不能通过索引顺序达到排序效果。一般有 Using filesort, 都建议优化去掉, 因为这样的查询 CPU 资源消耗大。
--先删除user_info中的联合索引
alter table user_info drop index name_key;
explain select * from user_innodb order by name;
得到的执行计划如下图
- Using index
“覆盖索引扫描”, 表示查询在索引树中就可查找所需数据, 不用扫描表数据文件, 往往说明性能不错。
-- 下面查询的tid直接可以从索引中检索到
EXPLAIN SELECT tid FROM teacher ;
得到的执行计划如下图
- Using temporary
查询有使用临时表, 一般出现于排序, 分组和多表 join 的情况, 查询效率不高, 建议优化。
下面这个 sql,就会用到 Using temporary
EXPLAIN select t.tid from teacher t join course c on t.tid = c.tid group by t.tid;
得到的执行计划如下图
3.2、数据库慢查询日志
在一个数据库中,有很多的应用程序来执行sql,我们一般不会等到sql查询慢了才去处理,而是希望能够有一个慢查询的监控,在应用程序中,有两种方式监控。
- Druid包提供了慢查询的监控
- Mysql提供了慢查询日志