2023-07-28
原文作者:说好不能打脸 原文地址:https://yinwj.blog.csdn.net/article/details/52461398

4-3-3-3、避免死锁的建议

上一篇文章我们主要介绍了MySQL数据库中锁的基本原理、工作过程和产生死锁的原因。通过上一篇文章的介绍,可以确定我们需要业务系统中尽可能避免死锁的出现。这里为各位读者介绍一些在InnoDB引擎使用过程中减少死锁的建议。

  • 正确使用读操作语句

经过之前文章介绍,我们知道一般的快照读是不会给数据表任何锁的。那么这些快照读操作也就不涉及到参与任何锁等待的情况。那么对于类似insert…select这样需要做当前读操作的语句(但又不是必须进行当前读的操作),笔者的建议是尽可能避免使用它们,如果非要进行也最好放到数据库操作的非高峰期进行(例如晚间)。

  • 基于索引进行写操作,避免基于表扫描(聚集索引扫描)进行写操作

基于索引进行写操作的目的是保证一个写操作性质的事务中,被锁住的索引和需要请求的锁定资源被控制在最小范围内。而避免使用表锁的原因是保证一个写操作性质的事务中,不会额外锁住完全不需要的索引资源或者抢占完全不需要的索引资源。表锁虽然不会直接导致死锁,但是由于表锁的工作方式,导致它成为死锁原因的几率增大了。

  • 避免索引失效

使用索引一定要注意索引字段的类型,例如当字段是一个varchar类型,赋值却是一个int类型,就会导致索引失效。如下所示:

    explain select * from myuser where user_name = 1
    # user_name 字段的类型是varchar,该字段建立了一个非唯一键索引
    # 但是以上语句在使用字段进行检索时,却使用了一个int作为条件值。
    # 通过MySQL的执行计划可以看到,InnoDB引擎在执行查询时并未使用索引,而是走的全表扫描
    
    +----+-------------+-------+------+---------------+-----+------+-------------+
    | id | select_type | table | type | possible_keys | key | rows |    Extra    |
    +----+-------------+-------+------+---------------+-----+------+-------------+
    | 1  |   SIMPLE    | myuser|  ALL |  name_index   |     |  13  | Using where |
    +----+-------------+-------+------+---------------+-----+------+-------------+
  • 关键业务的delete、update语句应该使用执行计划进行审核:从MySQL version 5.6 版本开始,MySQL中的执行计划功能已经支持对delete、update语句进行执行过程分析了。如果需要执行比较复杂和相关操作或者关键业务的写操作,都应该首先在执行计划中观察其运行方式。后文我们马上开始执行计划的讲解。

5、SQL执行计划

为了帮助开发人员根据数据表中现有索引情况,了解自己编写的SQL的执行过程、优化SQL结构,MySQL提供了一套分析功能叫做SQL执行计划(explain)。下面我们就为大家介绍一下执行计划功能的使用。

5-1、执行计划基本使用

5-1-1、简单实例

首先我们给出几个执行计划的具体案例,这里使用的数据表还是上一篇文章中展示各种示例所使用的数据表。为了便于读者查看,这里再一次给出数据表的结构:

    # 我们所示例的数据表和SQL语句均是工作在InnoDB数据库引擎下
    # myuser数据表一共有4个字段,3个索引。
    # user_name字段上创建了非唯一键非聚簇索引
    # user_number字段上创建了唯一键非聚簇索引
    # id字段上是聚簇索引
    CREATE TABLE `myuser` (
      `Id` int(11) NOT NULL AUTO_INCREMENT,
      `user_name` varchar(255) NOT NULL DEFAULT '',
      `usersex` int(9) NOT NULL DEFAULT '0',
      `user_number` int(11) NOT NULL DEFAULT '0',
      PRIMARY KEY (`Id`),
      UNIQUE KEY `number_index` (`user_number`),
      KEY `name_index` (`user_name`)
    )

您可以使用任何一种MySQL数据库客户端执行以下执行计划:

  • 不使用任何查询条件
    explain select * from myuser;
    +----+-------------+-------+------+---------------+-----+---------+-----+------+-------+
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
    +----+-------------+-------+------+---------------+-----+---------+-----+------+-------+
    | 1  |   SIMPLE    | myuser|  ALL |               |     |         |     |  13  |       |
    +----+-------------+-------+------+---------------+-----+---------+-----+------+-------+
    # 检索数据表中的所有记录,由于没有使用任何检索条件,所以InnoDB引擎从聚簇索引上扫描出所有的数据行
  • 使用非唯一建索引作为查询条件
    explain select * from myuser where user_name = '用户1';
    
    # 省去了表头,因为不好排版(可以参考上一个示例的表头)
    ......
    |1 | SIMPLE | myuser | ref |name_index|name_index | 767 | const | 6 | Using index condition |
    
    # InnoDB引擎首先从非聚簇索引上查找满足条件的多个索引项,然后在聚簇索引上找到具体的数据
  • 直接使用主键作为查询条件
    explain select * from myuser where id = 1;
    
    # 省去了表头,因为不好排版(可以参考上上一个示例的表头)
    ......
    |  1  |   SIMPLE   | myuser | const | PRIMARY | PRIMARY | 4 | const | 1 |  --这列没有信息--  |
    
    #使用聚簇索引直接定位数据
  • 使用非索引字段作为查询条件
    explain select * from myuser where usersex = 1
    +----+-------------+-------+------+---------------+-----+---------+-----+------+-------------+
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows |    Extra    |
    +----+-------------+-------+------+---------------+-----+---------+-----+------+-------------+
    | 1  |   SIMPLE    | myuser|  ALL |               |     |         |     |  13  | Using where |
    +----+-------------+-------+------+---------------+-----+---------+-----+------+-------------+
    
    # 由于没有创建索引,所以在聚簇索引上进行全表扫秒,并且过滤出满足条件的信息。

5-1-2、执行计划结果项

虽然本文还没有针对以上执行计划示例的分析结果进行讲解,但是为了让各位读者能够无阻碍的看下去,本文需要首先说明一下执行计划中的各个结果项的基本含义。在以上的示例中我们使用的MySQL的版本为MySQL version 5.6,根据不同的数据库版本,执行计划的分析结果可能会有一些不同。

    # 以下是MySQL 5.6版本的执行计划的分析结果的表头
    +----+-------------+-------+------+---------------+-----+---------+-----+------+-------+
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
    +----+-------------+-------+------+---------------+-----+---------+-----+------+-------+

以上表头的各个字段项目的大致意义如下:

  • id:每个被独立执行的操作的标识,表示对象被操作的顺序;ID值大,先被执行;如果相同,执行顺序一般从上到下。
  • select_type: 数据库引擎将SQL拆分成若干部分的子查询/子操作,每个查询select子句中的查询类型(后文详细讲解)。
  • table: 本次子查询所查询的目标数据表。SQL查询语句即使再复杂,一次子查询也只可能最多关联一张数据表。
  • partitions: 本次查询所涉及的数据表目标分区信息(如果有分区的话)。后文将对分区的概念进行概要说明。
  • type: 子查询类型,非常重要的性能衡量点。这个字段项可能显示的值包括:“ALL->index->range->ref->eq_ref->const | system->NULL”这些值所表示的查询性能,从左至右依次增加(注意,按照数据库基本思想——B+树,查询性能可能呈几何级的变化也可能差异不大)。这些值所代表的查询动作,在后文中会详细进行介绍。
  • possible_keys: 本次子查询可能使用的索引(前提是,您要建立了索引)。如果查询所使用的检索条件可能涉及到多个索引,这里将会列出这些所有的可能性。
  • key: 本次子查询最终被选定的执行索引。有的时候possible_keys可能有值,但keys可能没有,这就代表InnoDB引擎最终并没有使用任何索引作为检所依据。
  • key_len: 被选定的索引键的长度。
  • ref: 表示本次子查询参照的参照条件/参照数据表,参照条件/参照数据表,这个字段的值还可能是一个常量。
  • rows: 执行根据目前数据表的实际情况预估的,完成这个子查询需要扫描的数据行数。
  • Extra:包含不适合在其他列中显示但十分重要的额外信息。这个字段所呈现的信息在后文也会进行详细说明。

5-1-3、MySQL数据库中的分区(partitions)

InnoDB引擎和MYISAM引擎都支持分区功能,只是不同的数据引擎实现细节不一样。分区功能是指将某一张数据表中的数据和索引按照一定的规则在磁盘上进行存储。 分区功能只限于数据和索引的存储,是否对数据表进行了分区都不会影响索引在内存中的组织方式 ,并且分区功能的优势在数据量较小的情况下,是不怎么体现出来的。

202307282257214031.png

目前主要的分区方式包括:按照某个字段的值范围进行分区(Range)、按照某一个或者多个字段的Hash Key进行分区(Hash)、按照某个字段的固定值进行分区(List)。并且开发人员还可以同时使用多种分区方式,对数据表进行复合分区。以下是一个分区的示例:

    # 为partitionTable数据表建立四个存储分区
    CREATE TABLE `partitionTable` (
      `Id` int(11) NOT NULL AUTO_INCREMENT,
      `FieldA` varchar(255) NOT NULL DEFAULT '',
      `FieldB` int(9) NOT NULL DEFAULT '0',
      PRIMARY KEY (`Id`)
    )
    ENGINE=innodb  
    PARTITION BY HASH(Id)
    PARTITIONS 4;

接着我们可以到MySQL的基础库中观察到partitionTable数据表的数据和索引计数结构:

    # 查询partitiontable数据表的存储状态(库名为mysql)
    # 为节约篇幅,省略了不相关的行和列
    select * from innodb_table_stats where table_name like 'partitiontable%'
    +---------------------+--------+----------------------+--------------------------+
    |      table_name     | n_rows | clustered_index_size | sum_of_other_index_sizes |
    +---------------------+--------+----------------------+--------------------------+
    | partitiontable#p#p0 |   0    |           1          |            0             |
    | partitiontable#p#p1 |   0    |           1          |            0             |
    | partitiontable#p#p2 |   0    |           1          |            0             |
    | partitiontable#p#p3 |   0    |           1          |            0             |
    +---------------------+--------+----------------------+--------------------------+
    
    # 从以上结果可以看出MySQL对于这个数据表的存储状态按照分区情况进行分别管理。

有一定数据量的情况下(至少应该超过100万),当数据按照某个字段进行分区存储, 且这个字段(或者几个字段)并没有创建索引 ,那么查询操作的性能将会有明显提高,而且数据表的数据量越大性能提高越明显;如果这个字段(或者几个字段)创建了索引,则查询操作的性能提升并不明显——因为检索还是依靠索引结构。在执行计划的分析结果中有一个列,名字叫做partitions。该列的信息实际上是说明执行计划可能涉及的分区情况。

5-2、关键性能点

在我们根据SQL的执行计划进行查询语句和索引调整时,我们主要需要注意以下这些字段显示的值,以及它们背后所代表的性能表述。它们是:select_type列、type列、Extra列和key列。

5-2-1、select_type概要说明

一个复杂的SQL查询语句,在进行执行时会被拆分成若干个子查询。这些子查询根据存在的位置、执行先后顺序等要素被分解为不同的操作类型。当然还有的操作可能不涉及到任何实际数据表,例如两个子查询间的连接操作过程。在执行计划分析结果的select_type列,显示了拆分后这些子查询的类型,它们是:

  • SIMPLE(常见):简单的 SELECT查询。没有表UNION查询,没有子查询(嵌套查询)。我们在本节之前内容中给出的示例基本上属于这种查询类型,它基本上不需要也不能再进行子查询拆分。
  • PRIMARY(常见):由子查询(嵌套查询)的SQL语句下,最外层的Select 作为primary 查询。
  • DERIVED(常见):在from语句块中的子查询,属于衍生查询。例如以下的查询中接在“from”后面的子查询就属于这种类型的子查询:
    explain select * from (select * from t_interfacemethod_param where name = 'uid') t_interfacemethod_param
  • SUBQUERY 和 DEPENDENT SUBQUERY:这两种类型都表示第一个查询是子查询。区别是SUBQUERY表示的子查询不依赖于外部查询,而后者的子查询依赖于外部查询。
  • UNCACHEABLE SUBQUERY:子查询结果不能被缓存, 而且必须重写(分析)外部查询的每一行
  • UNION:从第二个或者在union 之后的select 作为 union 查询。这种查询类型出现在结果集与结果集的UNION操作中。
  • UNION RESULT:结果集是通过union 而来的。这种查询类型出现在结果集与结果集的UNION操作中。
  • DEPENDENT UNION:从第二个或者在union 之后的select 作为 union 查询, 依赖于外部查询。这种查询类型出现在结果集与结果集的UNION操作中。
  • UNCACHEABLE UNION:第二个 或者 在UNION 查询之后的select ,属于不可缓存的查询。这种查询类型出现在结果集与结果集的UNION操作中。

5-2-2、type概要说明

执行计划的type列中,主要说明了子查询的执行方式。它的值可能有如下的这些项目(根据MySQL数据库的执行引擎和版本还会有一些其它选项):

  • ALL:全表扫描,实际上是扫描数据表的聚簇索引,并在其上加锁还会视事务隔离情况加GAP间隙锁。在数据量非常少的情况下,做全表扫描和使用聚簇索引检索当然不会有太大的性能差异。但是数据量一旦增多情况就完全不一样了。
  • index:进行索引进行的扫描,它和ALL一样都是扫描,不同点是index类型的扫描只扫描索引信息,并不会对聚簇索引上对应的数据值进行任何形式的读取。例如基于主键的函数统计:
    # 以下语句还是要进行全表扫描,但是它并不需要读取任何数据信息。
    explain select count(*) from myuser
  • range:在索引(聚簇索引和非聚簇索引都有可能)的基础上进行检索某一个范围内满足条件的范围,而并不是指定的某一个或者某几个值,例如:
    # 以下查询语句在聚簇索引上检索一个范围
    explain select * from myuser where id >= 10
  • ref:在非聚簇索引的基础上使用“非唯一键索引”的方式进行查找。例如:
    # 在myuser中已基于user_name字段建立了非聚簇索引,且并非唯一键索引
    explain select count(*) from myuser where user_name = '用户1'
  • const | system:const可以理解为“固定值”查询,常见于使用主键作为“简单查询”的条件时。system是比较特殊的const,当这个数据表只有一行的情况下,出现system类型。例如以下查询的操作类型就是const:
    # 直接使用主键值就可以在索引中进行定位,无论数据量多大,这个定位的性能都不会改变
    explain select * from myuser where id = 1

5-2-3、Extra概要说明

执行计划分析结果中的Extra字段,包含了结果中其他字段中没有说明但是对性能分析有非常有帮助的信息。甚至有的时候可以但从这个字段分析出某个子查询是否需要调整、涉及到的索引是否需要调整或者MySQL服务的环境参数配置是否需要进行调整。Extra字段还可以看成是对特定子查询的总结。

  • Using index:使用了索引(无论是聚簇索引还是非聚簇索引)并且整个子查询都只是访问了索引信息,而没有访问真实的数据信息,那么在Extra字段就会出现这个描述。请看如下示例:
    explain select user_name from myuser where user_name = '用户1';
    +--------+-------------------------------------+
    | ...... |                Extra                |
    +--------+-------------------------------------+
    | ...... |     Using where; Using index        |
    +--------+-------------------------------------+
    
    # 使用user_name字段进行查询,原本需要再从聚簇索引中查找数据信息
    # 但是InnoDB引擎发现只需要输出一个字段,且这个字段就是user_name本身,甚至不需要去找全部数据了。
  • Using where 和 Using index condition:此where关键字并不是SQL查询语句中的where关键字(此where非彼where),而是指该子查询是否需要依据一定的条件对满足条件的索引(全表扫描也是扫描的聚簇索引)进行过滤。示例如下:
    # user_number 是一个非聚簇唯一键索引,所以where条件后的user_number只会定位到唯一一条记录
    # 不需要再根据这个条件查询是否还有其它满足条件的索引项了
    explain select * from myuser where user_number = 77777
    +--------+-------------+
    | ...... |    Extra    |
    +--------+-------------+
    | ...... |             |
    +--------+-------------+
    
    # user_name 是一个非聚簇非唯一键索引,索引where条件后的user_name可能定位到多条记录
    # 这时数据库引擎就会对这些索引进行检索,以便定位满足查询条件的若干索引项
    #(由于B+树的结构,所以这些索引项是连续的)
    explain select * from from myuser where user_name = '用户1'
    +--------+------------------------------+
    | ...... |              Extra           |
    +--------+------------------------------+
    | ...... |   Using index condition      |
    +--------+------------------------------+

为什么以上示例中显示的是“Using index condition”而不是“Using where”呢?这是MySQL Version 5.6+ 的新功能特性,Index Condition Pushdown (ICP)。简单的说就是减少了查询执行时MySQL服务和下层数据引擎的交互次数,达到提高执行性能的目的。如果您关闭MySQL服务中的ICP功能(这个功能默认打开),以上示例的第二个执行语句就会显示“Using where”了。

  • Using temporary:Mysql中的数据引擎需要建立临时表进行中间结果的记录,才能完成查询操作。这个常见于查询语句中存在GROUP BY 或者 ORDER BY操作的情况。但并不是说主要子查询中出现了GROUP BY 或者 ORDER BY就会建立临时表,而如果Group By 或者 Order By所依据的字段(或多个字段)没有建立索引,则一定会出现“Using temporary”这样的提示。另一种常见情况发生在子查询join连接时,连接所依据的一个字段(或多个字段)没有建立物理外键和索引。 一旦在Extra字段中出现了“Using temporary”提示,一般来说这条子查询就需要重点优化
  • Using filesort:Mysql服务无法直接使用索引完成排序时, 就需要动用一个内存空间甚至需要磁盘交换动作辅助才能完成排序操作 。这句话有两层含义,如果排序所依据的字段(一个或者多个)并没有创建索引,那么肯定无法基于索引完成排序;即使排序过程能够依据正确的索引完成,但是由于涉及到的查询结果太多,导致用于排序的内存空间不足,所以MySQL服务在进行排序时还会有磁盘交换动作。负责配置某一个客户(session)可用的内存空间参数项名字为“sort_buffer_size”。默认的大小为256KB,如果读者对查询结果集有特别要求,可以将该值改为1MB。 一旦在Extra字段中出现了“Using filesort”提示,那么说明这条子查询也需要进行优化
    explain select * from myuser order by usersex
    +--------+-----------------------+
    | ...... |          Extra        |
    +--------+-----------------------+
    | ...... |   Using filesort      |
    +--------+-----------------------+
    
    # 由于usersex并没有创建索引,所以使用filesort策略进行排序。

注意,在子查询中为Group By和Order by操作创建索引时,有时需要联合where关键字使用的查询字段一起创建复合索引才能起作用。这是因为子查询为了检索,所首先选择一个可用的索引项,随后进行排序时,却发现无法按照之前的索引进行排序,所以只有走filesort了。例如以下示例:

    # user_name字段和user_number字段都独立创建了索引
    explain select * from myuser where user_name = '用户1' group by user_number
    +--------+------------+----------------------------------------------------------+
    | ...... |    key     |                             Extra                        |
    +--------+------------+----------------------------------------------------------+
    | ...... | name_index |  Using index condition; Using where; Using filesort      |
    +--------+------------+----------------------------------------------------------+
    
    # 为了首先完成条件检索,InnoDB引擎选择了user_name字段的索引
    # 但是排序时发现无法按照之前的索引字段完成,所以选择走filesort
  • Using join buffer:使用InnoDB引擎预留的join buffer区域(一个专门用来做表连接的内存区域),这是一个正常现象主要涉及到两个子查询通过join关键字进行连接的操作。每一个客户端连接(session)独立使用的join buffer区域大小可以通过join_buffer_size参数进行设置。这个参数在MySQL 5.6 Version中的默认值为128KB。如果开发人员经常需要用到join操作,可以适当增加区域大小到1MB或者2MB。
    # 以下语句是一个左外连接的操作
    # 并且t_interfacemethod.uid和t_interfacemethod_param.interfacemethod之间有外键和索引存在
    explain select * from  t_interfacemethod_param 
    left join  t_interfacemethod on t_interfacemethod.uid = t_interfacemethod_param.interfacemethod
    
    +--------+----------------------------------------------------------+
    | ...... |                          Extra                           |
    +--------+----------------------------------------------------------+
    | ...... |                                                          |
    +--------+----------------------------------------------------------+
    | ...... |  Using where; Using join buffer (Block Nested Loop)      |
    +--------+----------------------------------------------------------+

5-3、执行计划的局限性

  • 执行计划不考虑Query Cache : 执行计划只考虑单次语句的执行效果,但实际上MySQL服务以及上层业务系统一般都会有一些缓存机制,例如MySQL服务中提供的Query Cache功能。所以实际上可能查询语句的重复执行速度会快一些。
  • 执行计划不能分析insert语句:insert语句的执行效果实际上是和其他语句相互作用的,所以执行计划不能单独分析insert语句的执行效果。不过update和delete语句都是可以分析的(请使用MySQL Version 5.6+ 版本)。
  • 执行计划不考虑可能涉及的存储过程、函数、触发器带来的额外性能消耗。

总的来说经过各个MySQL版本对执行计划功能的优化,现在这个功能得到的分析结果已经非常接近真实执行效果了。但是MySQL执行性能最关键的依据还是各位技术人员的数据库设计能力,起飞吧程序猿!

6、更高效的InnoDB引擎

MariaDB数据库管理系统是MySQL的一个分支,主要由开源社区在维护,采用GPL授权许可 MariaDB的目的是完全兼容MySQL。Google早已将他们服务器运行的上万个MySQL服务替换成了MariaDB,从公开的资料看淘宝技术部门也掀起一股使用MariaDB替换MySQL的技术思潮。

202307282257220472.png

MariaDB数据库的产生和发展和甲骨文公司收购MySQL事件有关,它是MySQL之父Widenius先生重新主导开发的完全和MySQL兼容的产品,其下运行的核心引擎还是InnoDB(这个版本的InnoDB引擎,也被称为XtraDB )。各位读者所在的技术团队也不妨尝试一下,因为这两中数据库的使用从业务层开发人员来看完全没有任何区别,DBA的维护手册甚至都不需要做任何更改。

================================
后记:MySQL数据库性能优化 部分的文章本来只计划了三篇,但是这一动笔完全就是一发不可收拾。到这里先告一段落吧,不然会严重影响后续知识内容的写作。从下篇文章开始,我们将一起讨论一下MySQL数据库的备份和集群方案。

阅读全文