推荐:mysql教程
前言
- MySQL索引底层数据结构与算法
- MySQL性能优化原理-前篇
- MySQL性能优化-实践篇1
- MySQL性能优化-实践篇2
- MySQL锁与事物隔离级别
前面我们讲了MySQL数据库底层的数据结构与算法、MySQL性能优化篇一些内容。以及上篇讲了MySQL的行锁与事务隔离级别。本篇再重点来讲讲锁类型和加锁原理。
首先对mysql锁进行划分:
- 按照锁的粒度划分:行锁、表锁、页锁
- 按照锁的使用方式划分:共享锁、排它锁(悲观锁的一种实现)
- 还有两种思想上的锁:悲观锁、乐观锁。
- InnoDB中有几种行级锁类型:Record Lock、Gap Lock、Next-key Lock
- Record Lock:在索引记录上加锁
- Gap Lock:间隙锁
- Next-key Lock:Record Lock+Gap Lock
表锁
表级锁是 MySQL 锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。被大部分的mysql引擎支持,MyISAM和InnoDB都支持表级锁,但是InnoDB默认的是行级锁。
表锁由 MySQL Server 实现,一般在执行 DDL 语句时会对整个表进行加锁,比如说 ALTER TABLE 等操作。在执行 SQL 语句时,也可以明确指定对某个表进行加锁。
表锁使用的是一次性锁技术,也就是说,在会话开始的地方使用 lock 命令将后续需要用到的表都加上锁,在表释放前,只能访问这些加锁的表,不能访问其他表,直到最后通过 unlock tables 释放所有表锁。
除了使用 unlock tables 显示释放锁之外,会话持有其他表锁时执行lock table 语句会释放会话之前持有的锁;会话持有其他表锁时执行 start transaction 或者 begin 开启事务时,也会释放之前持有的锁。
共享锁用法:
LOCK TABLE table_name [ AS alias_name ] READ复制代码
排它锁用法:
LOCK TABLE table_name [AS alias_name][ LOW_PRIORITY ] WRITE复制代码
解锁用法:
unlock tables;复制代码
行锁
行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。有可能会出现死锁的情况。 行级锁按照使用方式分为共享锁和排他锁。
不同存储引擎的行锁实现不同,后续没有特别说明,则行锁特指 InnoDB 实现的行锁。
在了解 InnoDB 的加锁原理前,需要对其存储结构有一定的了解。InnoDB 是聚簇索引,也就是 B+树的叶节点既存储了主键索引也存储了数据行。而 InnoDB 的二级索引的叶节点存储的则是主键值,所以通过二级索引查询数据时,还需要拿对应的主键去聚簇索引中再次进行查询。关于MySQL索引的详细知识可以查看《MySQL索引底层数据结构与算法》。
下面以两条 SQL 的执行为例,讲解一下 InnoDB 对于单行数据的加锁原理。
update user set age = 10 where id = 49; update user set age = 10 where name = 'Tom';复制代码
第一条 SQL 使用主键索引来查询,则只需要在 id = 49 这个主键索引上加上写锁;
第二条 SQL 则使用二级索引来查询,则首先在 name = Tom 这个索引上加写锁,然后由于使用 InnoDB 二级索引还需再次根据主键索引查询,所以还需要在 id = 49 这个主键索引上加写锁,如上图所示。
也就是说使用主键索引需要加一把锁,使用二级索引需要在二级索引和主键索引上各加一把锁。
根据索引对单行数据进行更新的加锁原理了解了,那如果更新操作涉及多个行呢,比如下面 SQL 的执行场景。
update user set age = 10 where id > 49;复制代码
这种场景下的锁的释放较为复杂,有多种的优化方式,我对这块暂时还没有了解,还请知道的小伙伴在下方留言解释。
页锁
页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB支持页级锁。
共享锁/排他锁
共享锁(Share Lock)
共享锁又称读锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。
如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
用法
SELECT ... LOCK IN SHARE MODE;
在查询语句后面增加LOCK IN SHARE MODE
,Mysql会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据。
排他锁(eXclusive Lock)
排他锁又称写锁,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
用法
SELECT ... FOR UPDATE;
在查询语句后面增加FOR UPDATE
,Mysql会对查询结果中的每行都加排他锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请排他锁,否则会被阻塞。
乐观锁和悲观锁
在数据库的锁机制中介绍过,数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。
乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。
无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。其实不仅仅是关系型数据库系统中有乐观锁和悲观锁的概念,像memcache、hibernate、tair等都有类似的概念。
针对于不同的业务场景,应该选用不同的并发控制方式。所以,不要把乐观并发控制和悲观并发控制狭义的理解为DBMS中的概念,更不要把他们和数据中提供的锁机制(行锁、表锁、排他锁、共享锁)混为一谈。其实,在DBMS中,悲观锁正是利用数据库本身提供的锁机制来实现的。
悲观锁
在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作对某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。
悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度(悲观),因此,在整个数据处理过程中,将数据处于锁定状态。 悲观锁的实现,往往依靠数据库提供的锁机制 (也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)
悲观锁的具体流程
- 在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking);
- 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。 具体响应方式由开发者根据实际需要决定;
- 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
- 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。
悲观锁的优点和不足
悲观锁实际上是采取了“先取锁在访问”的策略,为数据的处理安全提供了保证,但是在效率方面,由于额外的加锁机制产生了额外的开销,并且增加了死锁的机会。并且降低了并发性;当一个事物所以一行数据的时候,其他事物必须等待该事务提交之后,才能操作这行数据。
乐观锁
在关系数据库管理系统里,乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。
乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。
数据版本,为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。
乐观锁的优点和不足
乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。但如果直接简单这么做,还是有可能会遇到不可预期的结果,例如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题。
意向共享锁/意向排他锁
由于表锁和行锁虽然锁定范围不同,但是会相互冲突。所以当你要加表锁时,势必要先遍历该表的所有记录,判断是否加有排他锁。这种遍历检查的方式显然是一种低效的方式,MySQL 引入了意向锁,来检测表锁和行锁的冲突。
意向锁也是表级锁,也可分为读意向锁(IS 锁)和写意向锁(IX 锁)。当事务要在记录上加上读锁或写锁时,要首先在表上加上意向锁。这样判断表中是否有记录加锁就很简单了,只要看下表上是否有意向锁就行了。
意向锁之间是不会产生冲突的,也不和 AUTO_INC 表锁冲突,它只会阻塞表级读锁或表级写锁,另外,意向锁也不会和行锁冲突,行锁只会和行锁冲突。
意向锁是InnoDB自动加的,不需要用户干预。
对于insert、update、delete,InnoDB会自动给涉及的数据加排他锁(X);
对于一般的Select语句,InnoDB不会加任何锁,事务可以通过以下语句给显示加共享锁或排他锁。
意向共享锁(Intention Shared Lock)
意向共享锁(IS):表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁
意向排他锁(Exclusive Lock)
意向排他锁(IX):类似上面,表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。
记录锁(Record Lock)
记录锁是最简单的行锁,并没有什么好说的。上边描述 InnoDB 加锁原理中的锁就是记录锁,只锁住 id = 49 或者 name = 'Tom' 这一条记录。
当 SQL 语句无法使用索引时,会进行全表扫描,这个时候 MySQL 会给整张表的所有数据行加记录锁,再由 MySQL Server 层进行过滤。但是,在 MySQL Server 层进行过滤的时候,如果发现不满足 WHERE 条件,会释放对应记录的锁。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。
所以更新操作必须要根据索引进行操作,没有索引时,不仅会消耗大量的锁资源,增加数据库的开销,还会极大的降低了数据库的并发性能。
间隙锁(Gap Lock)
当我们使用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,InnoDB 也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
间隙锁是锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。
间隙锁在 InnoDB 的唯一作用就是防止其它事务的插入操作,以此来达到防止幻读的发生,所以间隙锁不分什么共享锁与排他锁。
要禁止间隙锁,可以把隔离级别降为读已提交,或者开启参数 innodb_locks_unsafe_for_binlog
show variables like 'innodb_locks_unsafe_for_binlog';复制代码
innodb_locks_unsafe_for_binlog
:默认
值为OFF,即启用间隙锁。因为此参数是只读模式,如果想要禁用间隙锁,需要修改 my.cnf(windows是my.ini) 重新启动才行。
# 在 my.cnf 里面的[mysqld]添加 [mysqld] innodb_locks_unsafe_for_binlog = 1复制代码
案例1:唯一索引的间隙锁
测试环境:
MySQL5.7,InnoDB,默认的隔离级别(RR)
示例表:
CREATE TABLE `my_gap` ( `id` int(1) NOT NULL AUTO_INCREMENT, `name` varchar(8) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;INSERT INTO `my_gap` VALUES ('1', '张三');INSERT INTO `my_gap` VALUES ('5', '李四');INSERT INTO `my_gap` VALUES ('7', '王五');INSERT INTO `my_gap` VALUES ('11', '赵六');复制代码
在进行测试之前,我们先看看 my_gap 表中存在的隐藏间隙:
- (-infinity, 1]
- (1, 5]
- (5, 7]
- (7, 11]
- (11, +infinity]
只使用记录锁(行锁),不会产生间隙锁
/* 开启事务1 */BEGIN;/* 查询 id = 5 的数据并加记录锁 */SELECT * FROM `my_gap` WHERE `id` = 5 FOR UPDATE;/* 延迟30秒执行,防止锁释放 */SELECT SLEEP(30); # 注意:以下的语句不是放在一个事务中执行,而是分开多次执行,每次事务中只有一条添加语句/* 事务2插入一条 name = '杰伦' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (4, '杰伦'); # 正常执行/* 事务3插入一条 name = '学友' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (8, '学友'); # 正常执行/* 提交事务1,释放事务1的锁 */COMMIT;复制代码
上述案例,由于主键是唯一索引,而且只使用一个索引查询,并且只锁定了一条记录,所以只会对 id = 5
的数据加上记录锁(行锁),而不会产生间隙锁。
产生间隙锁
恢复初始化的4条记录,继续在 id 唯一索引列上做以下测试:
/* 开启事务1 */BEGIN;/* 查询 id 在 7 - 11 范围的数据并加记录锁 */SELECT * FROM `my_gap` WHERE `id` BETWEEN 5 AND 7 FOR UPDATE;/* 延迟30秒执行,防止锁释放 */SELECT SLEEP(30); # 注意:以下的语句不是放在一个事务中执行,而是分开多次执行,每次事务中只有一条添加语句/* 事务2插入一条 id = 3,name = '思聪3' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (3, '思聪3'); # 正常执行/* 事务3插入一条 id = 4,name = '思聪4' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (4, '思聪4'); # 正常执行/* 事务4插入一条 id = 6,name = '思聪6' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (6, '思聪6'); # 阻塞/* 事务5插入一条 id = 8, name = '思聪8' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (8, '思聪8'); # 阻塞/* 事务6插入一条 id = 9, name = '思聪9' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (9, '思聪9'); # 阻塞/* 事务7插入一条 id = 11, name = '思聪11' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (11, '思聪11'); # 阻塞/* 事务8插入一条 id = 12, name = '思聪12' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (12, '思聪12'); # 正常执行/* 提交事务1,释放事务1的锁 */COMMIT;复制代码
从上面可以看到,(5,7]、(7,11] 这两个区间,都不可插入数据,其它区间,都可以正常插入数据。所以可以得出结论:当我们给(5,7] 这个区间加锁的时候,会锁住(5,7]、(7,11] 这两个区间。
恢复初始化的4条记录,我们再来测试如果锁住不存在的数据时,会如何?
/* 开启事务1 */BEGIN;/* 查询 id = 3 这一条不存在的数据并加记录锁 */SELECT * FROM `my_gap` WHERE `id` = 3 FOR UPDATE;/* 延迟30秒执行,防止锁释放 */SELECT SLEEP(30); # 注意:以下的语句不是放在一个事务中执行,而是分开多次执行,每次事务中只有一条添加语句/* 事务2插入一条 id = 3,name = '小张' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (2, '小张'); # 阻塞/* 事务3插入一条 id = 4,name = '小白' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (4, '小白'); # 阻塞/* 事务4插入一条 id = 6,name = '小东' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (6, '小东'); # 正常执行/* 事务5插入一条 id = 8, name = '大罗' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (8, '大罗'); # 正常执行/* 提交事务1,释放事务1的锁 */COMMIT;复制代码
从上面可以看出,指定查询某一条记录时,如果这条记录不存在,会产生间隙锁。
结论
- 对于指定查询某一条记录的加锁语句,如果该记录不存在,会产生记录锁(行锁)和间隙锁,如果记录存在,则只会产生记录锁(行锁);
- 对于查找某一范围内的查询语句,会产生间隙锁。
案例2:普通索引的间隙锁
示例表:id 是主键,在 number 上,建立了一个普通索引。
# 注意:number 不是唯一值CREATE TABLE `my_gap1` ( `id` int(1) NOT NULL AUTO_INCREMENT, `number` int(1) NOT NULL COMMENT '数字', PRIMARY KEY (`id`), KEY `number` (`number`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;INSERT INTO `my_gap1` VALUES (1, 1);INSERT INTO `my_gap1` VALUES (5, 3);INSERT INTO `my_gap1` VALUES (7, 8);INSERT INTO `my_gap1` VALUES (11, 12);复制代码
在进行测试之前,我们先来看看 my_gap1 表中 number 索引存在的隐藏间隙:
- (-infinity, 1]
- (1, 3]
- (3, 8]
- (8, 12]
- (12, +infinity]
测试1
我们执行以下的事务(事务1最后提交),分别执行下面的语句:
/* 开启事务1 */BEGIN;/* 查询 number = 3 的数据并加记录锁 */SELECT * FROM `my_gap1` WHERE `number` = 3 FOR UPDATE;/* 延迟30秒执行,防止锁释放 */SELECT SLEEP(30); # 注意:以下的语句不是放在一个事务中执行,而是分开多次执行,每次事务中只有一条添加语句/* 事务2插入一条 number = 0 的数据 */INSERT INTO `my_gap1` (`number`) VALUES (0); # 正常执行/* 事务3插入一条 number = 1 的数据 */INSERT INTO `my_gap1` (`number`) VALUES (1); # 被阻塞/* 事务4插入一条 number = 2 的数据 */INSERT INTO `my_gap1` (`number`) VALUES (2); # 被阻塞/* 事务5插入一条 number = 4 的数据 */INSERT INTO `my_gap1` (`number`) VALUES (4); # 被阻塞/* 事务6插入一条 number = 8 的数据 */INSERT INTO `my_gap1` (`number`) VALUES (8); # 正常执行/* 事务7插入一条 number = 9 的数据 */INSERT INTO `my_gap1` (`number`) VALUES (9); # 正常执行/* 事务8插入一条 number = 10 的数据 */INSERT INTO `my_gap1` (`number`) VALUES (10); # 正常执行/* 提交事务1 */COMMIT;复制代码
我们会发现有些语句可以正常执行,有些语句被阻塞来。查看表中的数据:
这里可以看到,number(1,8) 的间隙中,插入语句都被阻塞来,而不在这个范围内的语句,正常执行,这就是因为有间隙锁的原因。
测试2
我们再进行以下测试,这里将数据还原成初始化那样
/* 开启事务1 */BEGIN;/* 查询 number = 3 的数据并加记录锁 */SELECT * FROM `my_gap1` WHERE `number` = 3 FOR UPDATE;/* 延迟30秒执行,防止锁释放 */SELECT SLEEP(30);/* 事务1插入一条 id = 2, number = 1 的数据 */INSERT INTO `my_gap1` (`id`, `number`) VALUES (2, 1); # 阻塞/* 事务2插入一条 id = 3, number = 2 的数据 */INSERT INTO `my_gap1` (`id`, `number`) VALUES (3, 2); # 阻塞/* 事务3插入一条 id = 6, number = 8 的数据 */INSERT INTO `my_gap1` (`id`, `number`) VALUES (6, 8); # 阻塞/* 事务4插入一条 id = 8, number = 8 的数据 */INSERT INTO `my_gap1` (`id`, `number`) VALUES (8, 8); # 正常执行/* 事务5插入一条 id = 9, number = 9 的数据 */INSERT INTO `my_gap1` (`id`, `number`) VALUES (9, 9); # 正常执行/* 事务6插入一条 id = 10, number = 12 的数据 */INSERT INTO `my_gap1` (`id`, `number`) VALUES (10, 12); # 正常执行/* 事务7修改 id = 11, number = 12 的数据 */UPDATE `my_gap1` SET `number` = 5 WHERE `id` = 11 AND `number` = 12; # 阻塞/* 提交事务1 */COMMIT;复制代码
查看表中的数据;
这里有一个奇怪的现象:
- 事务3 添加 id = 6,number = 8 的数据,阻塞了;
- 事务4 添加 id = 8,number = 8 的数据,正常执行了;
- 事务7 将 id = 11,number = 12 的数据修改为 id = 11, number = 5 的操作,给阻塞了。
这是为什么?我们来看看下面的图:
从图中库看出,当 number 相同时,会根据主键 id 来排序
- 事务 3 添加的 id = 6,number = 8,这条数据是在 (3,8) 的区间里边,所以会阻塞;
- 事务 4 添加的 id = 8,number = 8,这条数据实在 (8,12) 区间里边,所以不会阻塞;
- 事务 7 的修改语句相当于 在 (3,8) 的区间里边插入一条数据,所以也被阻塞了。
结论
- 在普通索引列上,不管是何种查询,只要加锁,都会产生间隙锁,这跟唯一索引不一样;
- 在普通索引跟唯一索引中,数据间隙的分析,数据行是优先根据普通普通索引排序,再根据唯一索引排序。
临键锁(Next-key Locks)
临键锁,是记录锁(行锁)与间隙锁的组合,它的锁范围,即包含索引记录,又包含索引区间。它指的是加在某条记录以及这条记录前面间隙上的锁。假设一个索引包含 15、18、20 ,30,49,50 这几个值,可能的 Next-key 锁如下:
(-∞, 15],(15, 18],(18, 20],(20, 30],(30, 49],(49, 50],(50, +∞)复制代码
通常我们都用这种左开右闭区间来表示 Next-key 锁,其中,圆括号表示不包含该记录,方括号表示包含该记录。前面四个都是 Next-key 锁,最后一个为间隙锁。和间隙锁一样,在 RC 隔离级别下没有 Next-key 锁,只有 RR 隔离级别才有。还是之前的例子,如果 id 不是主键,而是二级索引,且不是唯一索引,那么这个 SQL 在 RR 隔离级别下就会加如下的 Next-key 锁 (30, 49](49, 50)
此时如果插入一条 id = 31 的记录将会阻塞住。之所以要把 id = 49 前后的间隙都锁住,仍然是为了解决幻读问题,因为 id 是非唯一索引,所以 id = 49 可能会有多条记录,为了防止再插入一条 id = 49 的记录。
注意:临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务隔离级别降级为 RC,临键锁则也会失效。
插入意向锁(Insert Intention Locks)
插入意向锁是一种特殊的间隙锁(简称II GAP)表示插入的意向,只有在 INSERT 的时候才会有这个锁。注意,这个锁虽然也叫意向锁,但是和上面介绍的表级意向锁是两个完全不同的概念,不要搞混了。
插入意向锁和插入意向锁之间互不冲突,所以可以在同一个间隙中有多个事务同时插入不同索引的记录。譬如在例子中,id = 30 和 id = 49 之间如果有两个事务要同时分别插入 id = 32 和 id = 33 是没问题的,虽然两个事务都会在 id = 30 和 id = 50 之间加上插入意向锁,但是不会冲突。
插入意向锁只会和间隙锁或 Next-key 锁冲突,正如上面所说,间隙锁唯一的作用就是防止其他事务插入记录造成幻读,正是由于在执行 INSERT 语句时需要加插入意向锁,而插入意向锁和间隙锁冲突,从而阻止了插入操作的执行。
插入意向锁的作用:
- 为来唤起等待。由于该间隙已经有锁,插入时必须阻塞,插入意向锁的作用具有阻塞功能;
- 插入意向锁是一种特殊的间隙锁,既然是一种间隙锁,为什么不直接使用间隙锁?间隙锁直接不相互排斥。不可以阻塞即唤起等待,会造成幻读。
- 为什么不实用记录锁(行锁)或 临键锁?申请了记录锁或临键锁,临键锁之间可能相互排斥,即影响 insert 的并发性。
自增锁(Auto-inc Locks)
AUTO_INC 锁又叫自增锁(一般简写成 AI 锁),是一种表锁,当表中有自增列(AUTO_INCREMENT)时出现。当插入表中有自增列时,数据库需要自动生成自增值,它会先为该表加 AUTO_INC 表锁,阻塞其他事务的插入操作,这样保证生成的自增值肯定是唯一的。AUTO_INC 锁具有如下特点:
- AUTO_INC 锁互不兼容,也就是说同一张表同时只允许有一个自增锁;
- 自增值一旦分配了就会 +1,如果事务回滚,自增值也不会减回去,所以自增值可能会出现中断的情况。
自增操作
使用AUTO_INCREMENT
函数实现自增操作,自增幅度通过 auto_increment_offset
和auto_increment_increment
这2个参数进行控制:
- auto_increment_offset 表示起始数字
- auto_increment_increment 表示调动幅度(即每次增加n个数字,2就代表每次+2)
通过使用last_insert_id()函数可以获得最后一个插入的数字
select last_insert_id();复制代码
自增锁
首先insert大致上可以分成三类:
- simple insert 如insert into t(name) values('test')
- bulk insert 如load data | insert into ... select .... from ....
- mixed insert 如insert into t(id,name) values(1,'a'),(null,'b'),(5,'c');
如果存在自增字段,MySQL 会维护一个自增锁,和自增锁相关的一个参数为(5.1.22 版本后加入) innodb_autoinc_lock_mode
,可以设定 3 值:
- 0 :traditonal (每次都会产生表锁)
- 1 :consecutive(会产生一个轻量锁,simple insert 会获得批量的锁,保证连续插入)
- 2 :interleaved (不会锁表,来一个处理一个,并发最高)
MyISam引擎均为 traditonal,每次均会进行表锁。但是InnoDB引擎会视参数不同产生不同的锁,默认为 1:consecutive。
show variables like 'innodb_autoinc_lock_mode';复制代码
traditonal
innodb_autoinc_lock_mode
为 0 时,也就是 traditional 级别。该自增锁时表锁级别,且必须等待当前 SQL 执行完毕后或者回滚才会释放,在高并发的情况下可想而知自增锁竞争时比较大的。
- 它提供来一个向后兼容的能力
- 在这一模式下,所有的 insert 语句(“insert like”)都要在语句开始的时候得到一个表级的 auto_inc 锁,在语句结束的时候才释放这把锁。注意,这里说的是语句级而不是事务级的,一个事务可能包含有一个或多个语句;
- 它能保证值分配的可预见性、可连续性、可重复性,这个也就是保证了 insert 语句在复制到 slave 的时候还能生成和 master 那边一样的值(它保证了基于语句复制的安全);
- 由于在这种模式下 auto_inc 锁一直要保持到语句的结束,所以这个就影响了并发的插入。
consecutive
innodb_autoinc_lock_mode 为 1 时,也就是 consecutive 级别。这是如果是单一的 insert SQL,可以立即获得该锁,并立即释放,而不必等待当前SQL执行完成(除非在其它事务中已经有 session 获取了自增锁)。另外当SQL是一些批量 insert SQL 时,比如 insert into ... select ...
, load data
, replace ... select ...
时,这时还是表级锁,可以理解为退化为必须等待当前 SQL 执行完才释放。可以认为,该值为 1 时相对比较轻量级的锁,也不会对复制产生影响,唯一的缺陷是产生自增值不一定是完全连续的。
- 这一模式下对 simple insert 做了优化,由于 simple insert 一次性插入的值的个数可以立马得到确定,所以 MyQL 可以一次生成几个连续的值,用于这个 insert 语句。总得来说这个对复制也是安全的(它保证了基于语句复制的安全);
- 这一模式也是MySQL的默认模式,这个模式的好处是 auto_inc 锁不要一直保持到语句的结束,只要语句得到了相应的值就可以提前释放锁。
interleaved
innodb_autoinc_lock_mode 为 2 时,也就是 interleaved 级别。所有 insert 种类的 SQL 都可以立马获得锁并释放,这时的效率最高。但是会引入一个新的问题:当 binlog_format 为 statement 时,这是复制没法保证安全,因为批量的 insert,比如 insert ... select ...
语句在这个情况下,也可以立马获取到一大批的自增 id 值,不必锁整个表, slave 在回放这个 SQL 时必然会产生错乱。
- 由于这个模式下已经没有了 auto_inc 锁,所以这个模式下的性能是最好的,但是也有一个问题,就是对于同一个语句来说它所得到的 auto_incremant 值可能不是连续的。
如果你的二进制文件格式是mixed | row 那么这三个值中的任何一个对于你来说都是复制安全的。
由于现在mysql已经推荐把二进制的格式设置成row,所以在binlog_format不是statement的情况下最好是innodb_autoinc_lock_mode=2 这样可能知道更好的性能。
总结
InnoDB锁的特性
- 在不通过索引条件查询的时候,InnoDB使用的确实是表锁!
- 由于 MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行 的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
- 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论 是使用主键索引、唯一索引或普通索引,InnoDB 都会使用行锁来对数据加锁。
- 即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同 执行计划的代价来决定的,如果 MySQL 认为全表扫 效率更高,比如对一些很小的表,它 就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。因此,在分析锁冲突时, 别忘了检查 SQL 的执行计划(explain查看),以确认是否真正使用了索引。
锁模式
锁的模式有:读意向锁,写意向锁,读锁,写锁和自增锁(auto_inc)。
不同模式锁的兼容矩阵
兼容IX兼容兼容
兼容S兼容
兼容
X
AI兼容兼容
总结起来有下面几点:
- 意向锁之间互不冲突;
- S 锁只和 S/IS 锁兼容,和其他锁都冲突;
- X 锁和其他所有锁都冲突;
- AI 锁只和意向锁兼容;
锁的类型
根据锁的粒度可以把锁细分为表锁和行锁,行锁根据场景的不同又可以进一步细分,依次为 Next-Key Lock,Gap Lock 间隙锁,Record Lock 记录锁和插入意向 GAP 锁。
不同的锁锁定的位置是不同的,比如说记录锁只锁住对应的记录,而间隙锁锁住记录和记录之间的间隔,Next-Key Lock 则所属记录和记录之前的间隙。不同类型锁的锁定范围大致如下图所示。
不同类型锁的兼容矩阵
兼容
兼容GAP兼容兼容兼容兼容NEXT-KEY
兼容
兼容II GAP兼容
兼容
其中,第一行表示已有的锁,第一列表示要加的锁。插入意向锁较为特殊,所以我们先对插入意向锁做个总结,如下:
- 插入意向锁不影响其他事务加其他任何锁。也就是说,一个事务已经获取了插入意向锁,对其他事务是没有任何影响的;
- 插入意向锁与间隙锁和 Next-key 锁冲突。也就是说,一个事务想要获取插入意向锁,如果有其他事务已经加了间隙锁或 Next-key 锁,则会阻塞。
其他类型的锁的规则较为简单:
- 间隙锁不和其他锁(不包括插入意向锁)冲突;
记录锁和记录锁冲突,Next-key 锁和 Next-key 锁冲突,记录锁和 Next-key 锁冲突;
以上就是今日份的深入理解MySQL锁类型和加锁原理的详细内容,更多请关注自由互联其它相关文章!