架构设计:系统存储(8)——MySQL数据库性能优化(4)

================================
(接上文《架构设计:系统存储(7)——MySQL数据库性能优化(3)》)web

4-三、InnoDB中的锁

虽然锁机制是InnoDB引擎中为了保证事务性而天然存在的,在索引、表结构、配置参数必定的前提下,InnoDB引擎加锁过程是同样的,因此理论上来讲也就不存在“锁机制可以提高性能”这样的说法。但若是技术人员不理解InnoDB中的锁机制或者混乱、错误的索引定义和一样混乱的SQL写操做语句共同做用,那么致使死锁出现的可能性就越大,须要InnoDB进行死锁检测的状况就越多,最终致使没必要要的性能浪费甚至事务执行失败。因此理解InnoDB引擎中的锁机制能够帮助咱们在高并发系统中尽量不让锁和死锁成为数据库服务的一个性能瓶颈sql

4-3-一、InnoDB中的锁类型

本文讲解的锁机制主要依据MySQL Version 5.6以及以前的版本(这是目前线上环境使用最多的版本),在MySQL Version 5.7以及最新的MySQL 8.0中InnoDB引擎的锁类型发生了一些变化(后文会说起),但基本思路没有变化。InnoDB引擎中的锁类型按照独占形式能够分为共享锁和排它锁(还有意向性共享锁和意向性排它锁);按照锁定数据的范围能够分为行级锁(其它引擎中还有页级锁的定义)、间隙锁、间隙复合锁??和表锁;为了保证锁的粒度可以至上而下传递,InnoDB中还设计有不能被用户干预的意向共享锁和意向排它锁。数据库

  • 共享锁(S锁)

因为InnoDB引擎支持事务,因此须要锁机制在多个事务同时工做时保证每一个事务的ACID特性。共享锁的特性是多个事务能够同时为某个资源加锁后进行读操做,而且这些事务间不会出现相互等待的现象。性能优化

  • 排它锁(X锁)

排它锁又被称为独占锁,一旦某个事务对资源加排它锁,其它事务就不能再为这个资源加共享锁或者排它锁了。一直要等待到当前的独占锁从资源上解除后,才能继续对资源进行操做。排它锁只会影响其余事务的加锁操做,也就是说若是其它事务只是使用简单的SELECT查询语句检索资源,就不会受到影响,由于这些SELECT查询语句不会试图为资源加任何锁,也就不会受资源上已有的排它锁的影响。咱们能够用一张表表示排它锁和共享锁的互斥关系:架构

锁类型 共享锁S 排它锁X
共享锁S 不互斥:多个共享锁不会相互影响相互等待 互斥:若是某个资源要加共享锁,则须要等待到资源上的排它锁配解除后,才能进行这个操做
排它锁X 互斥:若是资源要加排它锁,则须要等待到资源上全部共享锁都被解除后,才能进行这个操做 互斥:若是某个资源要加排它锁,则须要等待到资源上的排它锁配解除后,才能进行这个操做

排它锁和共享锁的互斥关系并发

  • 行级锁(Record lock)

行级锁是InnoDB引擎中对锁的最小支持粒度,便是指这个锁能够锁定数据表中某一个具体的数据行,锁的类型能够是排它锁也能够是共享锁。例如读者能够在两个独立事务中同时使用如下语句查询指定的行,可是两个事务并不会相互等待:svg

# lock in share mode 是为知足查询条件的数据行加共享锁 # 注意它和直接使用select语句的不一样特性 select * from myuser where id = 6 lock in share mode;
  • 间隙锁(GAP锁)

间隙锁只有在特定事务级别下才会使用,具体来讲是“可重复读”(Repeatable Read )这样的事务级别,这也是InnoDB引擎默认的事务级别,它的大体解释是不管在这个事务中执行多少次相同语句的当前读操做,其每次读取的记录内容都是同样的,并不受外部事务操做的影响。间隙锁主要为了防止多个事务在交叉工做的状况下,特别是同时进行数据插入的状况下出现幻读。举一个简单的例子,事务A中的操做正在执行如下update语句的操做:高并发

...... # 事务A正在执行一个范围内数据的更新操做 # 大意是说将用户会员卡号序列大于10的全部记录中user_name字段所有更新为一个新的值 update myuser set user_name = '用户11' where user_number >= 10; ......

其中user_number带有一个索引(后续咱们将讨论这个索引类型对间隙锁策略的影响),这样的检索条件很显然会涉及到一个范围的数据都将被更新(例如user_number==十、1三、1五、1七、1九、21……),于此同时有另外一个事务B正在执行如下语句:性能

...... # 事务B正在执行一个插入操做 insert into myuser(.........,'user_number') values (.........,11); # 插入一个卡号为11的新会员,而后提交事务B ......

若是InnoDB只锁住user_number值为10的非聚簇索引和相应的聚簇索引,显然就会形成一个问题:在A事务处理过程当中,忽然多出了一条知足更新条件的记录。事务A会很纠结的,很尴尬的。若是读者是InnoDB引擎的开发者,您会怎么作呢?正确的作法是为知足事务A所执行检索条件的整个范围加锁,这个锁不是加在某个或某几个具体的记录上,由于那样作仍是没法限制相似插入“一个卡号为11的新纪录”这样的状况,而是加在到具体索引和下一个索引之间,告诉这个索引B+树的其它使用者,包括这个索引在内的以后区域都不容许使用。这样的锁机制称为间隙锁(GAP锁)。优化

间隙锁和行级锁组合起来称为Next-Key Lock,实际上这两种锁通常状况下都是组合工做的。

  • 表级锁:没有能够检索的索引,就没法使用InnoDB特定的锁。另外,索引失效InnoDB也会为整个数据表加锁。若是表级锁的性质是排它锁(实际上大多数状况是这样的锁),那么全部试图为这张数据表中任何资源加共享锁或者排它锁的事务都必须等待在数据表上的排它锁被解除后,才能继续工做。表级锁能够看做基于InnoDB引擎工做的数据表的最悲观锁,它是InnoDB引擎为了保持事务特性的一场豪赌。例如咱们有以下的数据表结构:

uid(PK) varchar
user_name varchar
user_sex int

这张数据表中只有一个由uid字段构成的主索引。接着两个事务同时执行如下语句:

begin;
select * from t_user where uid = 2 lock in share mode;
#都先不执行commit,以便观察现象 #commit;

这里的select查询虽然使用的检索依据是uid,可是设置检索条件时uid的varchar类型却被错误的使用成了int类型。那么数据表将再也不使用索引进行检索,转而进行全表扫秒。这是一种典型的索引失效状况,最终读者观察到的现象是,在执行以上同一查询语句的两个事务中,有一个返回了查询结果,可是另一个一直为等待状态。以上的小例子也可让读者看到,科学管理索引在InnoDB引擎中是何等重要。本文后续部分将向读者介绍表级锁的实质结构。

  • 意向共享锁(IS锁)和意向排它锁(IX锁)

为了在某一个具体索引上加共享锁,事务须要首先为涉及到的数据表加意向共享锁(IS锁);为了在某一个具体因此上加排它锁,事务须要首先为涉及到的数据表加意向排它锁(IX锁)。这样InnoDB能够总体把握在并发的若干个事务中,让哪些事务优先执行更能产生好的执行效果。意向共享锁是InnoDB引擎自动控制的,开发人员没法人工干预,也不须要干预。

4-3-二、加锁过程实例

InnoDB引擎中的锁机制基于索引才能工做。对数据进行锁定时并非真的锁定数据自己,而是对数据涉及的汇集索引和非汇集索引进行锁定。在以前的文章中咱们已经介绍到,InnoDB引擎中的索引按照B+树的结构进行组织,那么加锁的过程很明显就是在对应的B+树上进行加锁位置检索和进行标记的过程。而且InnoDB引擎中的非聚簇索引最终都要依靠聚簇索引才能找到具体的数据记录位置,因此加锁的过程都涉及到对聚簇索引进行操做。

SELECT关键字的查询操做通常状况下都不会涉及到锁的问题(这种类型的读操做称为快照读),但并非全部的查询操做都不涉及到锁机制。只要SELECT属于某种写操做的前置子查询/检索或者开发人员显式为SELECT加锁,这些SELECT语句就涉及到锁机制——这种读操做称为当前读。而执行Update、Delete、Insert操做时,InnoDB会根据会根据操做中where检索条件所涉及的一条或者多条数据加排它锁。

为了进一步详细说明各类典型的加锁过程,本小节为读者准备了几个实例场景,并使用图文混合的方式从索引逻辑层面上进行说明。后续的几种实例场景都将以如下数据表和数据做为讲解依据:

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`) )

这张表中有三个索引,一个是以id字段为依据的聚簇索引,一个是以user_name字段为依据的非惟一键非聚簇索引,最后一个是以user_number字段为依据的惟一键非聚簇索引。咱们将在实例场景中观察惟一键索引和非惟一键索引在加锁,特别是加GAP锁的状况的不一样点。这张数据表中的数据状况以下图所示:

示例数据

4-3-2-一、 行锁加锁过程

首先咱们演示一个工做在InnoDB引擎下的数据表只加行锁的状况。

begin;
update myuser set user_name = '用户11' where id = 10;
commit;

以上事务中只有一条更新操做,它直接使用聚簇索引做为检索条件。聚簇索引确定是一个惟一键索引,因此InnoDB得出的加锁条件也就不须要考虑相似“insert into myuser(id,………) values(10,………)”这样的字段重复状况。由于若是有事务执行了这样的语句,就会直接报错退出。那么最终的加锁结果就是:只须要在聚簇索引上加X锁。

这里写图片描述
(额~~~你要问我为何树结构会是连续遍历的?请重读B+树的介绍)

其它事务依然能够对聚簇索引上的其它节点进行操做,例如使用update语句更新id为14的数据:

begin;
update myuser set user_name = '用户1414' where id = 14;
commit;

固然,因为这样的执行过程没有在X锁临近的边界加GAP锁,因此开发人员也可使用insert语句插入一条id为11的数据:

begin;
insert into myuser(id,user_name,usersex,user_number) values (11,'用户1_1',1,'110110110');
commit;

4-3-2-二、间隙锁加锁过程

工做在InnoDB引擎下的数据表,更多的操做过程都涉及到加间隙锁(GAP)的状况,这是由于毕竟大多数状况下咱们定义和使用的索引都不是惟一键索引,都在“可重复读”的事务级别下存在“幻读”风险。请看以下事务执行过程:

begin;
update myuser set usersex = 0 where user_name = '用户8' commit;

这个事务操做过程当中的update语句,使用非惟一键非聚簇索引’name_index’进行检索。InnoDB引擎进行分析后发现存在幻读风险,例如可能有一个事务在同时执行如下操做:

begin;
insert into myuser(id,user_name,usersex,user_number) values (11,'用户8',1,'110110110');
# 或者执行如下插入
# insert into myuser(id,user_name,usersex,user_number) values (11,'用户88',1,'110110110');
commit;

因此InnoDB须要在X锁临近的位置加GAP锁,避免幻读:

这里写图片描述

以上示意图有一个注意点,在许多技术文章中对GAP锁的讲解都是以int字段类型为基准,可是这里讲解所使用的类型是varchar。因此在加GAP锁的时候,看似’用户8’和’用户9’这两个索引节点没有中间值了。可是字符串也是能够排序的,因此’用户8’和’用户9’这两个字符串之间其实是能够放置不少中间值的,例如’用户88’、’用户888’、’用户8888’等。

这就是为何另外的事务执行相似”insert into myuser(id,user_name,usersex,user_number) values (11,’用户88’,1,’110110110’);”这样的语句,一样会进入等待状态:由于有GAP锁进行独占控制。

4-3-2-三、表锁加锁过程

上文已经提到,索引一旦失效InnoDB也会为整个数据表加锁。那么“为整个数据表加锁”这个动做怎么理解呢?不少技术文章在这里通常都归纳为一句话“在XXX数据表上加锁”。要弄清楚表锁的加载位置,咱们就须要进行实践验证。首先,为了更好的查看InnoDB引擎的工做状态和加锁状态,咱们须要打开InnoDB引擎的监控功能:

# 使用如下语句开启锁监控
set GLOBAL innodb_status_output=ON;
set GLOBAL innodb_status_output_locks=ON;

接下来咱们就可使用myuser数据表中没有键立索引的“usersex”字段进行加锁验证:

begin;
update myuser set user_name = '用户1414' where usersex = 1;
# 先不忙使用commit,以便观察锁状态 #commit;

在执行以上事务以前,myuser数据表中最新的记录状况以下图所示:

这里写图片描述

能够看到myuser数据表中一共有13条记录,其中知足“usersex = 1”的数据一共有9条记录。那么按照InnoDB引擎行锁机制来讲,就应该只有这9条记录被锁定,那么是否如此呢?咱们经过执行InnoDB引擎的状态监控功能来进行验证:

show engine innodb status;

# 如下是执行结果(省略了一部分不相关信息)
=====================================
2016-10-06 22:22:49 2f74 INNODB MONITOR OUTPUT
=====================================
.......
------------
TRANSACTIONS
------------
Trx id counter 268113
Purge done for trx's n:o < 268113 undo n:o < 0 state: running but idle
History list length 640
LIST OF TRANSACTIONS FOR EACH SESSION:
......

---TRANSACTION 268103, ACTIVE 21 sec
2 lock struct(s), heap size 360, 14 row lock(s), undo log entries 9
MySQL thread id 5, OS thread handle 0x1a3c, query id 311 localhost 127.0.0.1 root cleaning up
TABLE LOCK table `qiang`.`myuser` trx id 268103 lock mode IX
RECORD LOCKS space id 1014 page no 3 n bits 152 index `PRIMARY` of table `qiang`.`myuser` trx id 268103 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

Record lock, heap no 79 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 6; hex 000000041723; asc      #;;
 2: len 7; hex 2c000001e423fd; asc ,    # ;;
 3: len 8; hex e794a8e688b73130; asc       10;;
 4: len 4; hex 80000000; asc     ;;
 5: len 4; hex 80018a92; asc     ;;

Record lock, heap no 80 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 8000000e; asc     ;;
 1: len 6; hex 000000041721; asc      !;;
 2: len 7; hex 2b000001db176a; asc +     j;;
 3: len 8; hex e794a8e688b73134; asc       14;;
 4: len 4; hex 80000000; asc     ;;
 5: len 4; hex 80022866; asc   (f;;

Record lock, heap no 81 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000012; asc     ;;
 1: len 6; hex 00000004171f; asc       ;;
 2: len 7; hex 2a000001da17b2; asc *      ;;
 3: len 8; hex e794a8e688b73138; asc       18;;
 4: len 4; hex 80000000; asc     ;;
 5: len 4; hex 8002c63a; asc    :;;

Record lock, heap no 82 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000016; asc     ;;
 1: len 6; hex 00000004171d; asc       ;;
 2: len 7; hex 290000024d0237; asc )   M 7;;
 3: len 8; hex e794a8e688b73232; asc       22;;
 4: len 4; hex 80000000; asc     ;;
 5: len 4; hex 80035c3c; asc   \<;;

Record lock, heap no 86 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 000000041747; asc      G;;
 2: len 7; hex 41000002580110; asc A   X  ;;
 3: len 10; hex e794a8e688b731343134; asc       1414;;
 4: len 4; hex 80000001; asc     ;;
 5: len 4; hex 80002b67; asc   +g;;

...... 这里为节约篇幅,省略了6条行锁记录......

Record lock, heap no 93 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000008; asc     ;;
 1: len 6; hex 000000041747; asc      G;;
 2: len 7; hex 410000025802b4; asc A   X  ;;
 3: len 10; hex e794a8e688b731343134; asc       1414;;
 4: len 4; hex 80000001; asc     ;;
 5: len 4; hex 80015b38; asc   [8;;

Record lock, heap no 94 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000009; asc     ;;
 1: len 6; hex 000000041747; asc      G;;
 2: len 7; hex 410000025802f0; asc A   X  ;;
 3: len 10; hex e794a8e688b731343134; asc       1414;;
 4: len 4; hex 80000001; asc     ;;
 5: len 4; hex 8001869f; asc     ;;
......

经过以上日志咱们观察到的比较重要状况是,编号为268103的事务拥有两个锁结构(2 lock struct(s)),其中一个锁结构是意向性排它锁IX,这个锁结构一共锁定了一条记录(这条记录并非myuser数据表中的一条记录);另一个锁结构是排它锁(X),这个锁结构加载在主键索引上(“page no 3 n bits 152 index ‘PRIMARY’ of table ‘qiang’.’myuser’”),而且锁定了13条记录。这13条记录就是myuser数据表中的全部数据记录,并不是咱们最早预计的9条记录。

这就是表锁在锁定规律上的具体表现:由于不能基于索引检索查询条件,因此就只能基于汇集索引进行全表扫描。由于不能肯定汇集索引上哪些Page中数据知足检索条件,因此只能用排它锁一边锁定数据一边进行检索。由于要知足事务的ACID特性,因此在事务完成执行(或错误回滚)前都不能解除锁定:

这里写图片描述

因为咱们一直讨论的InnoDB引擎默认的事务级别是“可重复度”(Repeatable Read),因此为了不幻读,InnoDB还会在每个排它性行锁周围都加上间隙锁(GAP)。那么在这个事务级别下表锁最终的逻辑表现就以下图所示:

这里写图片描述

是的,没有索引能够提供检索依据的数据表正在进行一场豪赌!这仍是只有13条数据的状况下,那么试想一下若是数据表中有10,000,000条记录呢?这不只形成资源的浪费,更重要的是表锁是形成死锁的重要缘由,并且由此引起的InnoDB自动解锁代价很是昂贵(后文会详细讲到)。

4-3-三、死锁

一旦构成死锁,InnoDB会尽量的帮助开发者解除死锁。其作法是自动终止一些事务的运行从而释放锁定状态。在上一小节咱们示范的多个加锁场景,它们虽然都构成锁等待,可是都没有构成死锁。那么本文就要首先说明一下,什么样的状况才构成死锁。

4-3-3-一、什么是死锁

两个或者多个事务相互等待对方已锁定的资源,而彼此都不为协助对方达成操做目而主动释放已锁定的资源,这样的状况就称为死锁。请区分正常的锁等待和死锁的区别,例如如下示意图中的锁等待并不构成死锁:

这里写图片描述

上图中的状况只能称为锁资源等待,这是由于当A事务完成处理后就会释放所占据的资源上的锁,这样B事务就能够继续进行处理。而且在这个过程当中没有任何因素阻止A事务完成,也没有任何因素阻止B事务在随后的操做中获取锁。可是,如下示意图中的两个事务就在相互等待对方已锁定的资源,这就称为死锁:

这里写图片描述

上图中A事务已为id1和id2这两个索引项加锁,当它准备为id4这个索引加锁时,却发现id4已经被事务B加锁,因而事务A进行等待过程。恰巧的是,B事务在为id四、id5加锁后,正在等待为id2这个索引项加锁。因而最后形成的结果就是事务A和事务B相互等待对方释放资源。注意,因为须要保证事务的ACID特性,因此A事务已经锁定的索引id一、id2在事务A的等待过程当中,是不会被释放的;一样事务B已经锁定的索引id四、id5在等待过程当中也不会被释放。很明显若是没有外部干预,这个互相等待的过程将一直持续下去。这就是一个典型的死锁现象。在实际应用场景中,每每会由超过两个事务共同构成死锁现象,甚至会出现强制终止某一个等待的事务后依然不能解除死锁的复杂状况。

4-3-3-二、死锁出现的缘由

死锁形成的根本缘由和上层MySQL服务和下层InnoDB引擎的协调方式有关:在上层MySQL服务和下层InnoDB引擎配合进行Update、Delete和Insert操做时, 对知足条件的索引加X锁的操做是逐步进行的

当InnoDB进行update、delete或者insert操做时,若是有多条记录知足操做要求,那么InnoDB引擎会锁定一条记录(其实是相应的索引)而后再对这条记录进行处理,完成后再锁定下一条记录进行处理。这样依次循环直到全部知足条件的数据被处理完,最后再统一释放事务中的全部锁。若是这个过程当中某个将要锁定的记录已经被其它事务抢先锁定,则本事务就进入等待状态,一直等待到锁定的资源被释放为止。

这里写图片描述

要知道在正式的生成环境中,可能会同时有多个事务对某一个数据表上同一个范围内的数据进行加锁(加X锁后进行写操做)操做。而InnoDB引擎和MySQL服务的交互采用的这种方式极可能使这些事务各自持有某些记录的行锁,但又不构成让事务继续执行下去的条件。那为何说在生产环境下,多数死锁状态的出现是由于表锁致使的呢?

  • 首先,表锁自己并不会致使死锁,它只是InnoDB中的一种机制。可是表锁会一次锁定数据表中的全部汇集索引项。这就增长了表锁所在事务须要等待前序事务执行完毕才能继续执行的概率。并且这种等待状态还极可能在一个事务中出现屡次——由于有多个事务在同时执行嘛。在这个过程当中因为表锁逐渐占据了聚簇索引上绝大多数的索引项,因此这又增长了和其它正在执行的事务抢占锁定资源的,最终增长了死锁发生的概率。

  • 因为须要进行表锁定的事务,须要将数据表中的全部汇集索引所有锁定后(若是在默认的事务级别下还要加GAP锁),才能完成事务的执行过程,因此这会致使后序事务所有进入等待状态。而InnoDB引擎根本没法预知表锁所在事务是否占据了后续资源须要使用的索引项。这就与以前的提到的状况同样,增长了死锁发生的概率。

相关文章
相关标签/搜索