环球科创网

2022年01月19日整理发布:今日份的深入理解MySQL锁类型和加锁原理

更新时间:2022-01-19 16:53:17

导读 下面给大家讲解一下今天对MySQL锁类型和锁原理的深入了解。相信朋友们也应该很关心这个话题。现在,让我们为朋友们谈谈今天对MySQL锁类型和

下面给大家讲解一下今天对MySQL锁类型和锁原理的深入了解。相信朋友们也应该很关心这个话题。现在,让我们为朋友们谈谈今天对MySQL锁类型和锁原理的深入了解。边肖还收集了关于今天深入了解MySQL锁类型和锁原理的相关信息。我希望你看到后会喜欢。

相关免费学习推荐:mysql教程

前言MySQL索引底层数据结构和算法MySQL性能优化原理-往期MySQL性能优化-练习1MySQL性能优化-练习2MySQL锁和事物隔离级别我们讲了MySQL数据库的底层数据结构和算法以及MySQL性能优化。最后一部分谈到了MySQL的行锁和事务隔离级别。本文重点介绍锁的类型和锁定原理。

首先,划分mysql锁:

按锁的粒度分类:行锁、表锁、页锁按锁的用途分类:共享锁、排他锁(悲观锁的一种实现)和两种思想锁:悲观锁和乐观锁。InnoDB中的行级锁有几种类型:记录锁、间隙锁、下一个键锁记录锁:锁定索引记录间隙锁:间隙锁下一个键锁:记录锁间隙锁表级锁是MySQL锁中最细粒度的锁,这意味着当前操作锁定整个表比行锁花费的资源更少,不会出现死锁,但锁冲突的概率很高。在大多数mysql引擎的支持下,MyISAM和InnoDB都支持表级锁,但InnoDB默认为行级锁。

表的锁是由MySQL服务器实现的。通常,当执行DDL语句时,整个表都会被锁定,例如ALTER TABLE。您也可以在执行SQL语句时显式指定锁定表。

锁使用一次性锁技术,也就是说,在会话开始时使用锁命令来锁定以后需要使用的所有表。在释放表之前,只能访问这些锁定的表,而不能访问其他表,直到最后通过解锁表释放所有的表锁。

当会话持有除使用解锁表显示释放锁之外的其他表锁时,执行锁表语句将释放会话之前持有的锁;当会话持有其他表锁时,执行启动事务或开始打开事务也将释放以前持有的锁。

共享锁的使用:

Locktable _ name [asalias _ name]读取复制代码独占锁定用法:

lock table _ name[asalia _ name][low _ priority]写拷贝代码解锁用法:

解锁表格;复制代码行锁行级锁是Mysql中锁定粒度最细的锁,这意味着只有当前操作的行被锁定。行锁定可以大大减少数据库操作的冲突。锁定粒度最小,但锁定成本也最大。可能会出现死锁。级别锁根据使用方式分为共享锁和排他锁。

不同存储引擎实现的行锁是不同的。除非另有说明,行锁是指由InnoDB实现的行锁。

在了解InnoDB的锁定原理之前,需要了解它的存储结构。InnoDB是一个聚集索引,即B树的叶节点同时存储主键索引和数据行。InnoDB的二级索引的叶子节点存储的是主键值,所以通过二级索引查询数据时,需要取对应的主键对索引进行聚类,再进行查询。MySQL索引的详细知识见《MySQL索引底层数据结构与算法》。

345.jpg

以两个SQL的执行为例,说明InnoDB对单行数据的锁定原理。

更新用户集年龄=10,其中id=49

更新用户集age=10,其中name=' Tom要复制代码的第一个SQL,只需要向主键索引id=49添加一个写锁,就可以用主键索引进行查询。

第二个SQL使用辅助索引进行查询。首先,给索引名=Tom添加一个写锁。然后,因为二级索引InnoDB需要根据主键索引再次查询,所以需要给主键索引id=49添加写锁,如上图所示。

也就是说,要使用主键索引,需要添加一个锁;要使用辅助索引,您需要向辅助索引和主键索引添加锁。

根据按索引更新单行数据的锁定原理,我们知道如果更新操作涉及多行该怎么办,比如下面的SQL执行场景。

更新用户设置年龄=10,其中id为49;复制代码

le/000/000/052/f6cbb72c43d0830799a7db7d92f53bb9-1.png" data-width="800" data-height="600"/>

这种场景下的锁的释放较为复杂有多种的优化方式我对这块暂时还没有了解还请知道的小伙伴在下方留言解释。

页锁

页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快但冲突多行级冲突少但速度慢。所以取了折衷的页级一次锁定相邻的一组记录。BDB支持页级锁。

共享锁/排他锁共享锁(Share Lock)

共享锁又称读锁是读取操作创建的锁。其他用户可以并发读取数据但任何事务都不能对数据进行修改(获取数据上的排他锁)直到已释放所有共享锁。

如果事务T对数据A加上共享锁后则其他事务只能对A再加共享锁不能加排他锁。获准共享锁的事务只能读数据不能修改数据。

用法

SELECT ... LOCK IN SHARE MODE;

在查询语句后面增加LOCK IN SHARE MODEMysql会对查询结果中的每行都加共享锁当没有其他线程对查询结果集中的任何一行使用排他锁时可以成功申请共享锁否则会被阻塞。其他线程也可以读取使用了共享锁的表而且这些线程读取的是同一个版本的数据。

排他锁(eXclusive Lock)

排他锁又称写锁如果事务T对数据A加上排他锁后则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据又能修改数据。

用法

SELECT ... FOR UPDATE;

在查询语句后面增加FOR UPDATEMysql会对查询结果中的每行都加排他锁当没有其他线程对查询结果集中的任何一行使用排他锁时可以成功申请排他锁否则会被阻塞。

乐观锁和悲观锁

在数据库的锁机制中介绍过数据库管理系统(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、deleteInnoDB会自动给涉及的数据加排他锁(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';复制代码image.png

innodb_locks_unsafe_for_binlog:默认

值为OFF即启用间隙锁。因为此参数是只读模式如果想要禁用间隙锁需要修改 my.cnf(windows是my.ini) 重新启动才行。

# 在 my.cnf 里面的[mysqld]添加 [mysqld] innodb_locks_unsafe_for_binlog = 1复制代码案例1:唯一索引的间隙锁

测试环境:

MySQL5.7InnoDB默认的隔离级别(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 唯一索引列上做以下测试:

image.png/* 开启事务1 */BEGIN;/* 查询 id 在 7 - 11 范围的数据并加记录锁 */SELECT * FROM `my_gap` WHERE `id` BETWEEN 5 AND 7 FOR UPDATE;/* 延迟30秒执行防止锁释放 */SELECT SLEEP(30); # 注意:以下的语句不是放在一个事务中执行而是分开多次执行每次事务中只有一条添加语句/* 事务2插入一条 id = 3name = '思聪3' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (3, '思聪3'); # 正常执行/* 事务3插入一条 id = 4name = '思聪4' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (4, '思聪4'); # 正常执行/* 事务4插入一条 id = 6name = '思聪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 = 3name = '小张' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (2, '小张'); # 阻塞/* 事务3插入一条 id = 4name = '小白' 的数据 */INSERT INTO `my_gap` (`id`, `name`) VALUES (4, '小白'); # 阻塞/* 事务4插入一条 id = 6name = '小东' 的数据 */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;复制代码

我们会发现有些语句可以正常执行有些语句被阻塞来。查看表中的数据:

image.png

这里可以看到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;复制代码

查看表中的数据;

image.png

这里有一个奇怪的现象:

事务3 添加 id = 6number = 8 的数据阻塞了;事务4 添加 id = 8number = 8 的数据正常执行了;事务7 将 id = 11number = 12 的数据修改为 id = 11 number = 5 的操作给阻塞了。

这是为什么我们来看看下面的图:

image.png

从图中库看出当 number 相同时会根据主键 id 来排序

事务 3 添加的 id = 6number = 8这条数据是在 (3,8) 的区间里边所以会阻塞;事务 4 添加的 id = 8number = 8这条数据实在 (8,12) 区间里边所以不会阻塞;事务 7 的修改语句相当于 在 (3,8) 的区间里边插入一条数据所以也被阻塞了。结论在普通索引列上不管是何种查询只要加锁都会产生间隙锁这跟唯一索引不一样;在普通索引跟唯一索引中数据间隙的分析数据行是优先根据普通普通索引排序再根据唯一索引排序。临键锁(Next-key Locks)

临键锁是记录锁(行锁)与间隙锁的组合它的锁范围即包含索引记录又包含索引区间。它指的是加在某条记录以及这条记录前面间隙上的锁。假设一个索引包含 15、18、20 304950 这几个值可能的 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)。

不同模式锁的兼容矩阵ISIXSXAIIS兼容兼容兼容兼容IX兼容兼容兼容S兼容兼容XAI兼容兼容

总结起来有下面几点:

意向锁之间互不冲突;S 锁只和 S/IS 锁兼容和其他锁都冲突;X 锁和其他所有锁都冲突;AI 锁只和意向锁兼容;锁的类型

根据锁的粒度可以把锁细分为表锁和行锁行锁根据场景的不同又可以进一步细分依次为 Next-Key LockGap Lock 间隙锁Record Lock 记录锁和插入意向 GAP 锁。

不同的锁锁定的位置是不同的比如说记录锁只锁住对应的记录而间隙锁锁住记录和记录之间的间隔Next-Key Lock 则所属记录和记录之前的间隙。不同类型锁的锁定范围大致如下图所示。

123.jpg不同类型锁的兼容矩阵RECORDGAPNEXT-KEYII GAPRECORD兼容兼容GAP兼容兼容兼容兼容NEXT-KEY兼容兼容II GAP兼容兼容

其中第一行表示已有的锁第一列表示要加的锁。插入意向锁较为特殊所以我们先对插入意向锁做个总结如下:

插入意向锁不影响其他事务加其他任何锁。也就是说一个事务已经获取了插入意向锁对其他事务是没有任何影响的;插入意向锁与间隙锁和 Next-key 锁冲突。也就是说一个事务想要获取插入意向锁如果有其他事务已经加了间隙锁或 Next-key 锁则会阻塞。

其他类型的锁的规则较为简单:

间隙锁不和其他锁(不包括插入意向锁)冲突;

记录锁和记录锁冲突Next-key 锁和 Next-key 锁冲突记录锁和 Next-key 锁冲突;

以上就是今日份的深入理解MySQL锁类型和加锁原理的详细内容!

来源:php中文网

免责声明:本文由用户上传,如有侵权请联系删除!