四、Explain性能分析
4.1、概述
使用 EXPLAIN 关键字可以模拟优化器执行 SQL 查询语句,从而知道 MySQL 是如何处理你的 SQL 语句的。分析你的查询语句或是表结构的性能瓶颈。使用语法是:Explatn+SQL语句,他执行后返回的信息有好几列。
4.2、数据准备
create table course ( cid int(3), cname varchar(20), tid int(3) ); create table teacher ( tid int(3), tname varchar(20), tcid int(3) ); create table teacherCard ( tcid int(3), tcdesc varchar(200) ); insert into course values(1,'java',1); insert into course values(2,'html',1); insert into course values(3,'sql',2); insert into course values(4,'web',3); insert into teacher values(1,'tz',1); insert into teacher values(2,'tw',2); insert into teacher values(3,'tl',3); insert into teacherCard values(1,'tzdesc') ; insert into teacherCard values(2,'twdesc') ; insert into teacherCard values(3,'tldesc') ; 复制代码
4.3、id
explain返回的结果集中的id列表示select查询的序列号,表示查询中执行 select 子句或操作表的顺序。
id 的每个号码,表示一趟独立的查询,一个 sql 的查询趟数越少越好。
# 查询课程编号为2或者教师编号为3的老师信息 EXPLAIN select t.* from teacher t left join teacherCard tc on tc.tcid = t.tcid left join course c on c.tid = t.tid where c.cid = 2 or t.tid = 3 复制代码
我们可以发现,id值相同,从上往下顺序执行。
那要是id值不同呢?
# 查询教授SQL课程的老师的描述(desc) # 我们如果不使用子查询的话,会发现id值还是相同,所以下面将展示子查询的形式 EXPLAIN select tc.tcdesc from teacherCard tc where tc.tcid = (select t.tcid from teacher t where t.tid = (select c.tid from course c where c.cname = 'sql') ); 复制代码
id 不同,如果是子查询,id 的序号会递增,id 值越大优先级越高,越先被执行,在嵌套子查询时,先查内层 再查外层。所以先查询c表,然后是t表,最后是tc表。
那继续深入,如果相同1又有不同的id呢?
# 查询教授SQL课程的老师的描述(desc) # 我们采用子查询1加多表的形式进行查询 explain select t.tname ,tc.tcdesc from teacher t,teacherCard tc where t.tcid= tc.tcid and t.tid = (select c.tid from course c where cname = 'sql') ; 复制代码
id值越大越优先,若id值相同,从上往下顺序执行。
4.4、select_type
select_type 代表查询的类型,主要是用于区别普通查询、联合查询、子查询等的复杂查询。
属性 | 含义 |
SIMPLE | 简单的 select 查询,查询中不包含子查询或者 UNION连接查询 |
PRIMARY | 查询中若包含任何复杂的子部分,最外层查询则被标记为 Primary |
DERIVED | 使用到了临时表 |
SUBQUERY | 包含了子查询SQL中的子查询(非最外层) |
DEPEDENT SUBQUERY | 在SELECT或WHERE列表中包含了子查询,子查询基于外层 |
UNCACHEABLE SUBQUERY | 无法使用缓存的子查询 |
UNION | 如果有table1 union table2 ,则table1 就是derived,table2就是union |
UNION RESULT | 告知开发人员,那些表之间存在union查询 |
4.4.1、SIMPLE
简单的 select 查询,查询中不包含子查询或者 UNION连接查询。
select * from teacher 复制代码
4.4.2、PRIMARY
查询中若包含任何复杂的子部分,最外层查询则被标记为 Primary。
4.4.3、DERIVED
使用到了临时表就会被标记为DERIVED。他有两种1情况:
- 在from子查询中只有一张表。
explain select cr.cname from ( select * from course where tid in (1,2) ) cr ; 复制代码
- 在from子查询中, 如果有table1 union table2 ,则table1 就是derived,table2就是union
explain select cr.cname from ( select * from course where tid = 1 union select * from course where tid = 2 ) cr ; 复制代码
4.4.4、UNION
若第二个 SELECT 出现在 UNION 之后,则被标记为 UNION,第一个SELECT会被标记为DERIVED
explain select cr.cname from ( select * from course where tid = 1 union select * from course where tid = 2 ) cr ; 复制代码
4.4.5、UNION RESULT
告知开发人员,那些表之间存在union查询
explain select cr.cname from ( select * from course where tid = 1 union select * from course where tid = 2 ) cr ; 复制代码
4.5、table
table表明这个数据是基于哪张表的。
# 给一条复杂一点的SQL explain select cr.cname from ( select * from course where tid = 1 union select * from course where tid = 2 ) cr ; 复制代码
在id = 1的查询的table中,有一个<derived2>的值,说明是这个查询用到了衍生表,衍生表的出处是id为2的衍生表。
4.6、type
type表示的是索引类型,是较为重要的一个指标,性能从高到低依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index >ALL
一般来说,得保证查询至少达到 range 级别,最好能达到 ref,其中system、const是理想情况,一般很难达到,一般达到的是ref或者range。
如果想对type优化的前提是必须有索引。
4.6.1、system
只有一条数据的系统表或者是衍生表中只有一条数据的主查询,一般是无法达到的,忽略不计。
4.6.2、const
表示通过索引一次就找到了,仅仅能查询到一条数据的SQL,用于Primary key 或 unique,只针对这两个索引有效。也是很难达到的。
# 创建表 create table user ( tid int(3), tname varchar(20) ); insert into test01 values(1,'xiaolin') ; commit; # 添加索引 alter table test01 add constraint tid_pk primary key(tid) ; # 测试 explain select * from (select * from test01 )t where tid =1 ; 复制代码
4.6.3、eq_ref
唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配(有且只有一个,不能多也不能为0)。常见于主键或唯一索引扫描。是可遇不可求的。
explain select t.tcid from teacher t,teacherCard tc where t.tcid = tc.tcid 复制代码
4.6.4、ref
非唯一性索引扫描,对于每个索引键的查询,返回匹配的所有行。
# 先给teacher表的name字段加一个索引 alter table teacher add index index_name(tname); # 在修改数据库表,两条语句用同一个name explain SELECT * from teacher where tname = 'tw'; 复制代码
4.6.5、range
只检索给定范围的行,使用一个索引来选择行。key 列显示使用了哪个索引一般就是在你的 where 语句中出现了 between
、<
、>
、in
(有时候会失效,会转为无索引状态)等的查询这种范围扫描索引扫描比全表扫描要好,因为它只需要开始于索引的某一点,而结束语另一点,不用扫描全部索引。
# 给teacher的tid加一个普通索引 alter table teacher add index index_id(tid); # 查询id小于3的老师 explain select * from teacher where tid < 3 复制代码
4.6.6、index
查询全部加了索引的那一列数据。
# 我们刚刚给tid加了索引 explain select tid from teacher; 复制代码
4.6.7、all
查询表中的所有数据,一般是没有索引的情况。tname字段是没有索引的。
4.6.8、总结
- system\const:结果只有一条数据。
- eq_ref:结果多条数据,但是每条数据是唯一的。
- ref:结果多条,但是每条数据是0条或者多条。
4.7、possible_keys
显示可能应用在这张表中的索引,一个或多个。查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用。
4.8、key
实际使用的索引。如果为NULL,则没有使用索引。
4.9、key_len
表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。 key_len 字段能够帮你检查是否充分的利用上了索引。ken_len 越长,说明索引使用的越充分。
key_len的计算方式:
- 先看索引上字段的类型+长度比如 int=4 、varchar(20) =20 、 char(20) =20。
- 如果是 varchar 或者 char 这种字符串字段,视字符集要乘不同的值,比如 utf-8要乘 3,GBK 要乘 2。
- archar 这种动态字符串要加 2 个字节。
- 允许为空的字段要加 1 个字节。
- 如果是复合索引的话,key_len的长度是当前索引以及之前的索引之和。
4.10、ref
指明当前表所参照的字段,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值。
4.11、rows
rows 列显示 MySQL 认为它执行查询时必须检查的行数。越少越好!
4.12、Extra
其他的额外重要的信息。
4.12.1、Using filesort
出现这个说明你的SQL性能消耗大,需要额外的依次排序(查询),比方说有年龄、名字字段,我先通过名字查找出来,然后再根据年龄排序。
对于单索引来说,如果排序和查找是同一个字段,就不会出现Using filesort,反之亦然。
对于复合索引来说,不能跨列,要满足最佳左前缀,where和order by按照复合索引顺序使用,不要跨列或者无序使用。
# 我们先删除掉course中cid的主键,再执行查询 EXPLAIN select * from course where tid=1 order by cid 复制代码
4.12.2、Using temporary
使了用临时表保存中间结果,表示性能损耗比较大。MySQL 在对查询结果排序时使用临时表。常见于排序order by 和分组查询 group by,已经有了一张表,但是不使用,必须使用额外一张表来进行存储。
避免出现Using temporary的方法:查询哪些列就用哪些列来分组。
4.12.3、Using index
Using index 代表表示相应的 select 操作中使用了覆盖索引(Covering Index),只要使用到的列全部都在索引中,就是索引覆盖。他**避免访问了表的数据行,性能得到了提升!**原因在于这条SQL查询不读取源文件,只从索引文件中获取数据,不在原表中查询(不回表查询)。
- 如果同时出现 using where,表明索引被用来执行索引键值的查找。
- 如果没有同时出现 using where,表明索引只是用来读取数据而非利用索引执行查找。
- 如果用到了覆盖索引的时候,会对possible_keys和key造成影响:
- 如果没有where,则索引只出现在key中。
- 如果有where,则索引会出现在possible_keys和key中
4.12.4、Using where
表明使用了 where 过滤(既需要从索引中去查,又需要回原表中查询)。
# 索引列id可以从索引中查询,但是除了id之外的其他列需要去原表中查询 explain SELECT * from course c where c.tid =1 复制代码
4.12.5、Using join buffer
表明使用了连接缓存。
explain SELECT * from course c,teacher t where t.tid = c.tid 复制代码
五、单表SQL优化
建表语句:
CREATE TABLE `dept` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `deptName` VARCHAR(30) DEFAULT NULL, `address` VARCHAR(40) DEFAULT NULL, ceo INT NULL , PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; CREATE TABLE `emp` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `empno` INT NOT NULL , `name` VARCHAR(20) DEFAULT NULL, `age` INT(3) DEFAULT NULL, `deptId` INT(11) DEFAULT NULL, PRIMARY KEY (`id`) #CONSTRAINT `fk_dept_id` FOREIGN KEY (`deptId`) REFERENCES `t_dept` (`id`) ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 复制代码
5.1、全值索引我最爱
全值索引我最爱指的是,查询的字段按照顺序在索引中都可以匹配到!我们要根据联合索引字段的顺序,不能出现跨列的现象。
SQL 中查询字段的顺序,跟使用索引中字段的顺序,没有关系。优化器会在不影响 SQL 执行结果的前提下,给你自动地优化。
建立索引
create index index_age_depid_name on emp(age,deptid,name); # 查看emp的索引,检测我们建立索引是否成功 show index from emp 复制代码
书写SQL测试
EXPLAIN SELECT * FROM emp WHERE emp.age=30; 复制代码
EXPLAIN SELECT * FROM emp WHERE emp.age=30 and deptid=4; 复制代码
EXPLAIN SELECT * FROM emp WHERE emp.age=30 and deptid=4 AND emp.name = 'abcd'; 复制代码
5.2、最佳左前缀法则
5.2.1、索引失效情况
explain select * from emp where deptid = 4 and name = "xiaolin" 复制代码
我们可以发现,这个时候索引失效了。
5.2.2、索引有效的情况
explain select * from emp where age = 18 and deptid = 4 复制代码
5.2.3、总结
查询字段与索引字段顺序的不同会导致,索引无法充分使用,甚至索引失效!使用复合索引,需要遵循最佳左前缀法则,即如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。
过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。
5.3、不要在索引列上做任何计算
explain select * from emp where age +1 = 18 and deptid = 4 复制代码
我们可以发现,这个时候索引失效了,因为我们在索引列——age上进行了+1的操作,我们不能在索引列上做任何操作(计算、函数、(自动 or 手动)类型转换),因为会导致索引失效而转向全表扫描。
5.4、尽量使用覆盖索引
我们先看不适用覆盖索引的情况。
explain SELECT SQL_NO_CACHE * FROM emp WHERE emp.age=30 and deptid=5 AND emp.name = 'xiaolin'; 复制代码
再看使用到了覆盖索引的情况。
出现了一个Using Index,说明性能得到了提升。查询列和索引列一直,**不要写 select ***
5.5、尽量不要出现前缀模糊匹配
在日常的使用过程中,模糊匹配可以说是使用很多的关键字了,在使用的过程中,我们需要避免使用前缀的模糊匹配,因为会造成索引失效。也就是说like进行以常量开头,不要以%开头
# 先给name字段加上一个索引 create index index_name on emp(name); # 测试后缀模糊匹配 explain select * from emp where name like "a%"; 复制代码
# 测试前缀模糊匹配 explain select * from emp where name like "%a"; 复制代码
# 测试前后都模糊匹配 explain select * from emp where name like "%a"; 复制代码
我们可以发现,只要是出现了前缀的模糊匹配的时候,都会出现索引失效的问题。如果一定需要使用%开头的模糊查询,我们可以使用索引覆盖来一定程度提高性能。
5.6、减少使用or
# 我们使用or的时候也很容易造成索引失效的问题。 explain select * from emp where age = 18 or deptid = 4 复制代码
如果我们在实际开发中,需要使用到or的话,我们可以使用 union all 或者 union 来替代。
# 使用union all替代 explain select * from emp where age = 18 union all select * from emp where deptid = 4; 复制代码
# 使用union替代 explain select * from emp where age = 18 union select * from emp where deptid = 4; 复制代码
5.8、尽量不要使用显示、隐式类型转换
# 先试一下正常情况 explain select * from emp where name="123"; 复制代码
# 再试试索引失效情况 explain select * from emp where name=123;
索引失效的原因是因为mysql底层会把int类型的123转化为varchar类型的123,索引失
效。
5.7、总结
全职匹配我最爱,最左前缀要遵守。 带头大哥不能死,中间兄弟不能断。 索引列上少计算,范围之后全失效。 LIKE 百分写最右,覆盖索引不写*。 不等空值还有 OR,索引影响要注意。 VAR 引号不可丢,SQL 优化有诀窍。
六、多表SQL优化
6.1、建表语句
create table teacher2 ( tid int(4) primary key, cid int(4) not null ); insert into teacher2 values(1,2); insert into teacher2 values(2,1); insert into teacher2 values(3,3); create table course2 ( cid int(4) , cname varchar(20) ); insert into course2 values(1,'java'); insert into course2 values(2,'python'); insert into course2 values(3,'kotlin'); commit; 复制代码
6.2、left join
当我们进行连表查询的时候,会想到一个问题,索引往哪张表加?
explain select *from teacher2 t left outer join course2 c on t.cid=c.cid where c.cname='java'; 复制代码
一般情况下,我们把数据量小的表放在左边,数据量大的表放在右边,在进行连表查询的时候,是左表驱动右表,也就是数据量小的表驱动数据量大的表,这是因为这条SQL查询的底层,实际上是两个循环,一个外层循环,一个内层循环,在开发中,一般是将数据小的循环放外层,数据大的循环放内存。
索引建立在经常使用的字段上,所以可得,如果是左外连接,索引建立在左表的字段,右外连接,索引建立在右表的字段。
按照规则,我们给teacher2这张表加上索引。
alter table teacher2 add index index_teacher2_cid(cid); # 再次执行 explain select *from teacher2 t left outer join course2 c on t.cid=c.cid where c.cname='java'; 复制代码
我们可以发现,有一张表已经用上了索引了。一般来说,where后面的字段也要加索引,于是我们进一步优化。
# 给cname字段加上索引 alter table course2 add index index_course2_cname(cname); # 再次执行 explain select *from teacher2 t left outer join course2 c on t.cid=c.cid where c.cname='java'; 复制代码
七、其他的优化方法
7.1、exist
exist语法是将主查询的结果,放到子查询的进行校验(判断子查询是否有数据,如果有数据则校验成功),如果符合校验就保留数据。
select tname from teacher where exists (select * from teacher); # 等价于 select * from teacher; 复制代码
- 如果主查询的数据集大,用
in
。 - 如果子查询的数据集很大,用
exist
。
7.2、order by
7.2.1、MySQL的排序算法
我们一般使用order by的时候都会出现using filesort。using filesort有两种算法:
- 双路排序:MySQL4.1之前默认使用双路排序,所谓的双路就是扫描2次磁盘。第一次从磁盘中读取排序字段,在buffer缓冲区对排序字段进行排序。第二次扫描其他字段。这种两次IO是很消耗性能的。
- 单路排序:MySQL4.1之后,为了减少IO访问次数,就改为了单路排序。他只读取一次全部字段,在buffer中挑出排序字段进行排序。但
# 单位是字节,如果max_length_for_sort_data值太低(需要排序的总大小超过了max_length_for_sort_data定义的字节数),MySQL会自动从单路切换到双路。 set max_length_for_sort_data = 2048; 复制代码
7.2.1.1、双路排序
MySQL 4.1 之前是使用双路排序,字面意思就是两次扫描磁盘,最终得到数据,第一次读取行指针和 orderby 列,对他们进行排序,然后第二次扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据出。从磁盘取排序字段,在 buffer 进行排序,再从磁盘取其他字段。
简单来说,取一批数据,要对磁盘进行了两次扫描,众所周知,I\O 是很耗时的,所以在 mysql4.1 之后,出现了第二种改进的算法,就是单路排序。
7.2.1.2、单路排序
从磁盘读取查询需要的所有列,按照 order by 列在 buffer 对它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据。并且把随机 IO 变成了顺序 IO,但是它会使用更多的空间, 因为它把每一行都保存在内存中了。
7.2.1.3、单路排序存在的问题
单路排序会有一定的隐患,他有可能不是一次IO,可能是多次IO。因为如果数据量太大的话会进行数据拆分,拆分成多次在buffer中进行排序,分片读取,多次读取。我们可以通过sql语句来调大buffer的大小。
7.2.2、提高order by查询的策略
7.2.2.1、增大 sort_butter_size 参数的设置
不管用哪种算法,提高这个参数都会提高效率,当然,要根据系统的能力去提高,因为这个参数是针对每个进程的1M-8M 之间调整。
7.2.2.2、增大 max_length_for_sort_data 参数的设置
mysql 使用单路排序的前提是排序的字段大小要小于max_length_for_sort_data。
提高这个参数,会增加用改进算法的概率。但是如果设的太高,数据总容量超出 sort_buffer_size 的概率就增大,明显症状是高的磁盘 I/O 活动和低的处理器使用率。(1024-8192 之间调整)。
7.2.2.3、减少 select 后面的查询的字段
当 Query 的字段大小总和小于 max_length_for_sort_data 而且排序字段不是 TEXT|BLOB 类型时,会用改进后的算法——单路排序, 否则用老算法——多路排序。
两种算法的数据都有可能超出 sort_buffer 的容量,超出之后,会创建 tmp 文件进行合并排序,导致多次 I/O,但是用单路排序算法的风险会更大一些,所以要提高 sort_buffer的大小。
所以千万不要使用select * ...;
。
7.2.2.4、使用覆盖索引
SQL 只需要通过索引就可以返回查询所需要的数据,而不必通过二级索引查到主键之后再去查询数据。
7.2.2.5、保证排序的一致性
我们要保证全部的排序字段排序的一致性,要么全部升序,要么全部降序,不要出现某些部分升序,某些部分降序。
八、慢查询日志
8.1、慢查询日志
8.1.1、是什么
MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。
具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是运行10秒以上的语句。
由他来查看哪些SQL超出了我们的最大忍耐时间值,比如一条sql执行超过5秒钟,我们就算慢SQL,希望能收集超过5秒的sql,结合之前explain进行全面分析。
8.1.2、怎么用
默认情况下,MySQL 数据库没有开启慢查询日志,需要我们手动来设置这个参数。
当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件。一般在开发的时候打开,上线部署的时候关闭。
# 检查是否开启了慢查询日志,默认是off表示未开启。 show variables like '%slow_query_log'; 复制代码
# 临时开启,在内存中开启,MySQL服务关闭时就关闭了 set global slow_query_log = 1; 复制代码
# 永久开启,需要在MySQL的配置文件中进行编辑 # 进入MySQL的配置文件 vim /etc/my.cnf 复制代码
在[mysqld]中添加两行配置。
# 开启慢查询日志 slow_query_log=1 # 指定慢查询日志的存放路径 slow_query_log_file=/var/lib/mysql/localhost-slow.log 复制代码
# 查询慢查询的阈值 show variables like '%long_query_time%'; 复制代码
# 设置慢查询阈值 # 临时设置,设置完毕后,需要重新登陆后才生效 set global long_query_time = 5; 复制代码
# 永久开启,需要在MySQL的配置文件中进行编辑,步骤和设置是否开启慢查询相同,只是写的参数不同。 # 进入MySQL的配置文件 vim /etc/my.cnf # 在[mysqld]下面追加 long_query_time=3 复制代码
# 查询超过慢查询阈值的sql数量 # 睡眠4s,模拟一条超过了4s的SQL select sleep(4); show global status like '%slow_queries%'; 复制代码
# 如果我们想知道具体是哪条SQL的话,我们需要去刚刚指定的慢查询日志文件中进行查询 cat /var/lib/mysql/localhost-slow.log 复制代码
8.2、日志分析工具 mysqldumpslow
我们可以发现用原生的慢查询日志十分不友好,我们可以通过mysql自带的日志分析工具 mysqldumpslow来分析慢查询。
# 在Linux中查看mysqldumpslow的帮助信息 mysqldumpslow --help 复制代码
参数 | 描述 |
-s | 是表示按照何种方式排序 |
c | 访问次数 |
l | 锁定时间 |
r | 返回记录 |
t | 查询时间 |
al | 平均锁定时间 |
ar | 平均返回记录数 |
at | 平均查询时间 |
-t | 返回前面多少条的数据 |
-g | 后边搭配一个正则匹配模式,大小写不敏感的 |
# 得到返回记录集最多的 10 个 SQL mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log # 得到访问次数最多的 10 个 SQL mysqldumpslow -s c -t 10 /var/lib/mysql/atguigu-slow.log # 得到按照时间排序的前 10 条里面含有左连接的查询语句 mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/atguigu-slow.log # 另外建议在使用这些命令时结合 | 和 more 使用 ,否则有可能出现爆屏情况 mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log | more 复制代码
九、锁
9.1、什么是锁机制
在MySQL中有很多种类型的锁,比如我们最熟悉的行锁,那这里有个问题,为什么MySQL中会有行锁呢?其实原因就是:MySQL要保证数据的一致性。当数据 update 时首先要进行当前读(读取最新的数据)得到数据,而且要保证查出来的数据到更改完成的这段时间内数据不会被其他事务更改。这样的话你的 update 语句执行得到的结果和语义上是“一致的”。
9.2、锁的分类
- 根据操作类型分:
- 读锁(共享锁):对同一个数据,多个读操作可以同时进行,互不干扰。
- 写锁(互斥锁):如果当前写操作没有完毕,则无法进行其他的读(相当于A在买衣服的时候把原本在前台展示的衣服带到了是试衣间里面了,B连看都无法看了)写操作。
- 操作范围:
- 表锁:一次性锁一整张表,对一张表整体枷锁,粒度粗。MyISAM存储引擎使用的是表锁,开销小,加锁快,无死锁,但是锁的范围大,容易发生锁冲突,并发度低。
- 行锁:一次性对一条数据加锁,粒度细不容易发生冲突,InnoDB存储引擎使用的是行锁,开销大,加锁慢,容易出现死锁,锁的范围小,并发度高很小概率发生脏读、幻读、不可重复读等高并发问题。
- 页锁
9.3、锁的操作
/* MYSQL/SQLSERVER 支持自增,Oracle 需要借助于序列来实现自增 */ create table tablelock ( id int primary key auto_increment, name varchar(20) ) engine myisam; insert into tablelock(name) values('a1'); insert into tablelock(name) values('a2'); insert into tablelock(name) values('a3'); insert into tablelock(name) values('a4'); insert into tablelock(name) values('a5'); 复制代码
9.3.1、表锁
9.3.1.1、加读/写锁
# 给表加读锁或者写锁,可以给多张表一起加,语法格式为 lock table 表1 read/writelock table 表2 read/write; 复制代码
9.3.1.2、查看锁
# 查看加锁的表,1代表加了锁 show open tables; 复制代码
9.3.1.3、释放锁
# 释放锁 unlock tables; 复制代码
9.3.1.4、分析表锁定的严重程度
show status like 'table%'; 复制代码
他的结果有两行数据:
- Table_locks_immedicate:表示可以立刻获取到的锁数量
- Tbale_locks_waited:表示需要等待的表锁数,他的值越大说明锁竞争越激烈
一般建议用Table_locks_immedicate/Tbale_locks_waited的值来衡量,如果大于5000,采用InnoDB引擎,否则使用MyISAM引擎。
9.3.1.5、总结
- 如果某一个会话对A表加了读锁,则该会话可以对A表进行读操作,但是不可以进行写操作,该会话不可以对除A表外的其他表进行任何读写操作。
- 简单来说,给A表加了读锁,则当前会话只能对A表进行读操作。
- 其他会话可以对该表进行读操作,也可以进行写操作,但是在进行写操作的时候需要等带加锁的会话释放锁。
- 对一个加写锁的会话,当前会话可以对加了写锁的表进行任何增删改查的操作,但是不能对其他表进行增删改查操作。其他会话得等当前会话释放了锁之后才可以进行增删改查操作。
9.3.2、行锁
行锁,一次锁一行数据,因此 如果操作的是不同数据,则不干扰。
create table linelock ( id int(5) primary key auto_increment, name varchar(20) )engine=innodb; insert into linelock(name) values('1'); insert into linelock(name) values('2'); insert into linelock(name) values('3'); insert into linelock(name) values('4'); insert into linelock(name) values('5'); 复制代码
为了研究行锁,我们需要暂时将自动提交关闭,方便我们手动提交。
set autocommit = 0; 复制代码
9.3.2.1、行锁总结
- 表锁是通过unlock tables来进行解锁,也可以通过事务解锁 ;。而行锁是通过事务(commit/rollback)解锁。
- 如果会话x对某条数据a进行 DML操作(研究时:关闭了自动commit的情况下),则其他会话必须等待会话x结束事务(commit/rollback)后 才能对数据a进行操作。
9.3.2.2、行锁的注意事项
如果没有索引,则行锁会转为表锁。
show index from linelock ; alter table linelock add index idx_linelock_name(name); # 索引未失效 # 会话0进行写操作 update linelock set name = 'ai' where name = '3' ; # 会话1进行写操作,不同的数据 update linelock set name = 'aiX' where name = '4' ; # 索引失效(发生了索引类型转换) # 会话0进行写操作 update linelock set name = 'ai' where name = 3 ; # 会话1对不同的数据进行写操作 update linelock set name = 'aiX' where name = 4 ; 复制代码
可以发现,数据被阻塞了(加锁),因为索引类型发生了类型转换导致了索引失效,因此此次操作会从行锁转为表锁。
行锁存在一种极为特殊的情况
值在范围内,但是却不存在,这种称为间隙锁。比如我们在linelock表中没有id=7的数据,当我们写一条SQL的时候:update linelock set name ='x' where id >1 and id<9 ;
,在中国where的范围内,没有id=7在这个范围内,但是没有id=7的数据,则id=7的数据成为了间隙。MySQL会自动给间隙加锁,名为间隙锁,同时他也是行锁。即MySQL会自动给id=7的数据加间隙锁(行锁)。
9.3.2.3、关闭自动提交的四种方式
- set autocommit =0 ;
- start transaction ;
- begin ;
- 在sql后加for update。
我们也可以在查询的时候加行锁,只需使用第四种方式。
# 通过for update对query语句进行加锁。 select * from linelock where id =2 for update ; 复制代码
9.3.2.4、行锁分析
我们可以使用SQL语句来分析行锁。
show status like '%innodb_row_lock%' ; 复制代码
他有四个参数:
- Innodb_row_lock_current_waits :当前正在等待锁的数量。
- Innodb_row_lock_time:等待总时长。从系统启到现在 一共等待的时间。
- Innodb_row_lock_time_avg :平均等待时长。从系统启到现在平均等待的时间。
- Innodb_row_lock_time_max :最大等待时长。从系统启到现在最大一次等待的时间。
- Innodb_row_lock_waits :等待次数。从系统启到现在一共等待的次数