医院网站站内文章收录量多少,小制作手工废物利用,怎么让wordpress,医学招聘网站开发区MySQL学习大纲 我的数据库学习大纲 从不同维度对锁的分类#xff1a; 1.对数据操作的类型划分:读锁和写锁
1.1.读锁 与 写锁概述#xff1a; 1.对于数据库中并发事务的读-读情况并不会引起什么问题。对于写-写、读-写或写-读这些情况可能会引起一些问题#xff0c;需要使用… MySQL学习大纲 我的数据库学习大纲 从不同维度对锁的分类 1.对数据操作的类型划分:读锁和写锁
1.1.读锁 与 写锁概述 1.对于数据库中并发事务的读-读情况并不会引起什么问题。对于写-写、读-写或写-读这些情况可能会引起一些问题需要使用MVCC或者加锁的方式来解决它们 2.在使用加锁的方式解决问题时由于既要允许读-读情况不受影响又要使写-写、读-写或写-读情况中的操作相互阻塞所以MySQL实现一个由两种类型的锁组成的锁系统来解决。这两种类型的锁通常被称为共享锁(Shared LockS Lock)和排他锁(Exclusive LockX Lock)也叫读锁(readlock)和写锁(write lock) a.读锁
也称为 共享锁 、英文用 S 表示。针对同一份数据多个事务的读操作可以同时进行而不会互相影响相互不阻塞的。需要注意的是对于 InnoDB 引擎来说读锁和写锁可以加在表上也可以加在行上。
b.写锁
也称为 排他锁 、英文用 X 表示。当前写操作没有完成前它会阻断其他写锁和读锁。这样就能确保在给定的时间里只有一个事务能执行写入并防止其他用户读取正在写入的同一资源。 说明 注意对于InnoDB引擎来说读锁和写锁可以加在某个表上也可以加在某个行上MyInasm仅仅支持表级别锁 c.举例行级读写锁)︰
1.如果一个事务T1已经获得了某个行r的读锁那么此时另外的一个事务T2是可以去获得这个行r的读锁的因为读取操作并没有改变行r的数据;2.如果某个事务T3想获得行r的写锁则它必须等待事务T1、T2释放掉行r上的读锁才行
兼容情况X锁S锁X锁 (写锁)不兼容不兼容S锁(读锁)不兼容兼容 注意这里的兼容是指对同一张表或记录的锁的兼容性情况 1.2.锁定读 1.在采用加锁方式解决脏读、不可重复读、幻读这些问题时读取一条记录时需要获取该记录的S锁其实是不严谨的有时候需要在读取记录时就获取记录的X锁来禁止别的事务读写该记录为此MySQL提出了两种比较特殊的SELECT语句格式: a.对读取的记录加S锁∶
SELECT ... LOCK IN SHARE MODE;
#或
SELECT ... FOR SHARE;#(8.0新增语法)1.在普通的SELECT语句后边加LOCK IN SHARE MODE如果当前事务执行了该语句那么它会为读取到的记录加S锁这样允许别的事务继续获取这些记录的S锁2.比方说别的事务也使用SELECT …LOCK IN SHARE MODE语句来读取这些记录)但是不能获取这些记录的X锁(比如使用SELECT … FOR UPDATE语句来读取这些记录或者直接修改这些记录)。3.如果别的事务想要获取这些记录的X锁那么它们会阻塞直到当前事务提交之后将这些记录上的S锁释放掉
b.对读取的记录加X锁: SELECT ... FOR UPDATE;1.在普通的SELECT语句后边加FOR UPDATE如果当前事务执行了该语句那么它会为读取到的记录加X锁这样既不允许别的事务获取这些记录的S锁(比方说别的事务使用SELECT … LOCK IN SHARE MODE语句来读取这些记录)也不允许获取这些记录的X锁(比如使用SELECT … FOR UPDATE语句来读取这些记录或者直接修改这些记录)2.如果别的事务想要获取这些记录的S锁或者X锁那么它们会阻塞直到当前事务提交之后将这些记录上的X锁释放掉
c.MySQL8.0新特性:
1.在5.7及之前的版本SELECT …FOR UPDATE如果获取不到锁会一直等待直到innodb_lock_wait_timeout超时2.在8.0版本中在SELECT …FOR UPDATESELECT …FOR SHARE后添加NOWAIT、SKIP LOCKED语法跳过锁等待或者跳过锁定。通过添加NOWAIT、SKIP LOCKED语法能够立即返回。如果查询的行已经加锁: NOWAIT会立即报错返回SKIP LOCKED会立即返回只是返回的结果中不包含被锁定的行
d.演示的SQL
1.在客户端工具1执行如下SQL
#事务一
begin ;
select * from account for update;
/*
-- ----------- ------
| id | NAME| balance|
| 1 │ 张三| 40.00|
| 2 | 李四| 0.00 |
| 3 | 王五| 100.00|
--- -----------
*/2.在客户端工具2执行
#事务二
begin ;
select * from account for update nowait;
#报错
#ERROR 3572 (HYO00): Statement aborted because lock(s) could not be acquired immediately and NOMAIT is set.select * from account for update skip locked;
#Empty set (o.00 sec)commit;
#Query Ok,o rows affected ( sec)1.3.写操作 平常所用到的写操作无非是 DELETE、UPDATE、INSERT 这三种: a.DELETE:
对一条记录做DELETE操作的过程其实是先在B树中定位到这条记录的位置然后获取这条记录的X锁再执行delete mark.操作也可以把这个定位待删除记录在B树中位置的过程看成是一个获取X锁的锁定读
b.UPDATE︰ 在对一条记录做UPDATE操作时分为三种情况: 情况1:
没有修改该记录的键值不更改主键值且被更新的列占用的存储空间在修改前后未发生变化。则先在B树中定位到这条记录的位置然后再获取一下记录的X锁最后在原记录的位置进行修改操作。也可以把这个定位待修改记录在B树中位置的过程看成是一个获取X锁的锁定读
情况2∶
未修改该记录的键值且至少有一个被更新的列占用的存储空间在修改前后发生变化先在B树中定位到这条记录的位置然后获取一下记录的X锁将该记录彻底删除掉就是把记录彻底移入垃圾链表)最后再插入一条新记录。这个定位待修改记录在B树中位置的过程看成是一个获取X锁的锁定读新插入的记录由INSERT操作提供的隐式锁进行保护
情况3∶
修改了该记录的键值则相当于在原记录上做DELETE操作之后再来一次INSERT操作加锁操作就需要按照DELETE和INSERT的规则进行了
c.INSERT :
一般情况下新插入一条记录的操作并不加锁,通过一种称之为隐式锁的结构来保护这条新插入的记录在本事务提交前不被别的事务访问 2、按照数据操作的粒度划分表级锁、页级锁、行锁 1.为了尽可能提高数据库的并发度每次锁定的数据范围越小越好理论上每次只锁定当前操作的数据的方案会得到最大的并发度但是管理锁是很耗资源的事情涉及获取、检查、释放锁等动作)。因此数据库系统需要在高并发响应和系统性能两方面进行平衡这样就产生了“锁粒度〈Lock granularity)”的概念 2.对一条记录加锁影响的也只是这条记录而已我们就说这个锁的粒度比较细;其实一个事务也可以在表级别进行加锁自然就被称之为表级锁或者表锁对一个表加锁影响整个表中的记录我们就说这个锁的粒度比较粗。锁的粒度主要分为表级锁、页级锁和行锁 2.1.表锁Table Lock a.表级别的S锁、X锁: 1.概述 1.在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时InnoDB存储引擎是不会为这个表添加表级别的 S锁 或者X锁的。2.在对某个表执行一些诸如 ALTER TABLE 、 DROP TABLE 这类的 DDL 语句时其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。同理某个事务中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时在其他会话中对这个表执行 DDL 语句也会发生阻塞。这个过程其实是通过在 server层 使用一种称之为 元数据锁 英文名 Metadata Locks 简称 MDL 结构来实现的 InnoDB存储引擎中的表级别锁MDL锁 3.一般情况下不会使用InnoDB存储引擎提供的表级别的 S锁 和 X锁 。只会在一些特殊情况下比方说 崩溃恢复 过程中用到。比如在系统变量 autocommit0innodb_table_locks 1 时 手动 获取InnoDB存储引擎提供的表t 的 S锁 或者 X锁 可以这么写 LOCK TABLES t READ InnoDB存储引擎会对表 t 加表级别的 S锁 。LOCK TABLES t WRITE InnoDB存储引擎会对表 t 加表级别的 X锁 。 4.不过尽量避免在使用InnoDB存储引擎的表上使用 LOCK TABLES 这样的手动锁表语句它们并不会提供什么额外的保护只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的 行锁 关于InnoDB表级别的 S锁 和 X锁 大家了解一下就可以了。 2.举例:下面我们讲解MylSAM引擎下的表锁 1.步骤1:创建表并添加数据
CREATE TABLE mylock(id INT NOT NULL PRIMARY KEY auto_increment,NAME VARCHAR(20)
)ENGINE myisam;#存储引擎使用InnoDB也可以只是不建议#插入一条数据
INSERT INTO mylock(NAME) VALUES(a);#查询表中所有的数据
SELECT* FROM mylock;
/*
----------
| id | NAME |
----------
| 1 | a |
----------
*/
2.步骤2:查看表上加过的锁主要关注查询结果中的In_use字段的值
SHOW OPEN TABLES where In_use 0; -- 大于0的就是有表锁 #或者
SHOW OPEN TABLES;#主要关注In_use字段的值
/*部分输出
SHOW OPEN TABLES;
-------------------- ------------------------------------------------
| Database | Table | In_use | Name_locked |
--------------------------------------------------------------------
| atguigudb2 | mylock | 0 | 0 |
| mysql | routines | 0 | 0 |
| mysql | parameter_type_elements | 0 | 0 |
| performance_schema | session_variables | 0 | 0 |
| mysql | index_stats | 0 | 0 |
| atguigudb2 | user3 | 0 | 0 |
| atguigudb2 | type | 0 | 0 |
| atguigudb2 | test2
*/
3.步骤3:手动增加表锁命令lock tables mylock write然后再次查看
lock tables mylock write;SHOW OPEN TABLES where In_use 0;
/*mylock加了表锁
-----------------------------------------
| Database | Table | In_use | Name_locked |
-----------------------------------------
| atguigudb2 | mylock | 1 | 0 |
-----------------------------------------
*/
4.步骤4释放锁
#释放锁
unlock tables;#释放当前加锁的表
5.步骤5:添加表级别的读锁S锁为mylock表加read锁(读阻塞写)观察阻塞的情况流程如下: 6.步骤6∶添加表级别的写锁X锁为mylock表加write锁观察阻塞的情况流程如下: 1.MylSAM在执行查询语句(SELECT)前会给涉及的所有表加读锁在执行增删改操作前会给涉及的表加写锁。InnoDB存储引擎是不会为这个表添加表级别的读锁或者写锁的。2.MySQL的表级锁有两种模式以MyISAM表进行操作的演示 表共享读锁Table Read Lock表独占写锁Table Write Lock 3.两锁对比
b.意向锁 intention lock 1.意向锁概述 1.InnoDB 支持多粒度锁multiple granularity locking 它允许 行级锁 与 表级锁 共存而意向锁就是其中的一种表锁 意向锁是为了协调行锁和表锁的关系支持多粒度表锁和行锁的锁并存意向锁是一种不与行级锁冲突表级锁这一点很重要表明“某个事务正在某些行持有了锁或该事务准备去持有锁” 2.意向锁分为两种意向共享锁intention shared lock, IS 和 意向排他锁intention exclusive lock, IX 1.意向共享锁intention shared lock, IS事务有意向对表中的某些行加共享锁S锁2.意向排他锁intention exclusive lock, IX事务有意向对表中的某些行加排他锁X锁3.说明意向锁是由存储引擎自己维护的 用户无法手动操作意向锁在为数据行加共享 / 排他锁之前InooDB 会先获取该数据行 所在数据表的对应意向锁 2.意向锁要解决的问题 3.意向锁的并发性 1.意向锁不会与行级的共享 / 排他锁互斥正因为如此意向锁并不会影响到多个事务对不同数据行加排他锁时的并发性。不然我们直接用普通的表锁就行了2.我们扩展一下上面 teacher表的例子来概括一下意向锁的作用一条数据从被锁定到被释放的过程中可 能存在多种不同锁但是这里我们只着重表现意向锁。 4.从上面的案例可以得到如下结论 1.InnoDB 支持 多粒度锁 特定场景下行级锁可以与表级锁共存。2.意向锁之间互不排斥但除了 IS 与 S 兼容外 意向锁会与 共享锁 / 排他锁 互斥 。3.IXIS是表级锁不会和行级的X、S锁发生冲突。只会和表级的XS发生冲突4.意向锁在保证并发性的前提下实现了 行锁和表锁共存 且 满足事务隔离性 的要求
c.自增锁AUTO-INC锁 1.在使用MySQL过程中我们可以为表的某个列添加 AUTO_INCREMENT 属性。举例 2.由于这个表的id字段声明了AUTO_INCREMENT意味着在书写插入语句时不需要为其赋值SQL语句修改如下所示。 3.上边的插入语句并没有为id列显式赋值所以系统会自动为它赋上递增的值结果如下所示。 4.现在我们看到的上面插入数据只是一种简单的插入模式所有插入数据的方式总共分为三类分别是 “ Simple inserts ”“ Bulk inserts ”和“ Mixed-mode inserts ” “Simple inserts” 简单插入 可以 预先确定要插入的行数 当语句被初始处理时的语句。包括没有嵌套子查询的单行和多行 INSERT…VALUES() 和 REPLACE 语句。比如我们上面举的例子就属于该类插入已经确定要插入的行 数 “Bulk inserts” 批量插入 事先不知道要插入的行数 和所需自动递增值的数量的语句。比如 INSERT … SELECT REPLACE … SELECT 和 LOAD DATA 语句但不包括纯INSERT。 InnoDB在每处理一行为AUTO_INCREMENT列 分配一个新值 “Mixed-mode inserts” 混合模式插入 这些是“Simple inserts”语句但是指定部分新行的自动递增值。例如 INSERT INTO teacher (id,name) VALUES (1,a), (NULL,b), (5,c), (NULL,d); 只是指定了部分id的值。另一种类型的“混合模式插入”是 INSERT ... ON DUPLICATE KEY UPDATE 。 innodb_autoinc_lock_mode有三种取值分别对应与不同锁定模式 1innodb_autoinc_lock_mode 0(“传统”锁定模式) 在此锁定模式下所有类型的insert语句都会获得一个特殊的表级AUTO-INC锁用于插入具有 AUTO_INCREMENT列的表。这种模式其实就如我们上面的例子即每当执行insert的时候都会得到一个 表级锁(AUTO-INC锁)使得语句中生成的auto_increment为顺序且在binlog中重放的时候可以保证 master与slave中数据的auto_increment是相同的。因为是表级锁当在同一时间多个事务中执行insert的 时候对于AUTO-INC锁的争夺会 限制并发 能力。 2innodb_autoinc_lock_mode 1(“连续”锁定模式) 在 MySQL 8.0 之前连续锁定模式是 默认 的在这个模式下“bulk inserts”仍然使用AUTO-INC表级锁并保持到语句结束。这适用于所有INSERT … SELECTREPLACE … SELECT和LOAD DATA语句。同一时刻只有一个语句可以持有AUTO-INC锁。 对于“Simple inserts”要插入的行数事先已知则通过在 mutex轻量锁 的控制下获得所需数量的 自动递增值来避免表级AUTO-INC锁 它只在分配过程的持续时间内保持而不是直到语句完成。不使用 表级AUTO-INC锁除非AUTO-INC锁由另一个事务保持。如果另一个事务保持AUTO-INC锁则“Simple inserts”等待AUTO-INC锁如同它是一个“bulk inserts” 3innodb_autoinc_lock_mode 2(“交错”锁定模式) 从 MySQL 8.0 开始交错锁模式是 默认 设置在此锁定模式下自动递增值 保证 在所有并发执行的所有类型的insert语句中是 唯一 且 单调递增 的。但 是由于多个语句可以同时生成数字即跨语句交叉编号为任何给定语句插入的行生成的值可能 不是连续的
d.元数据锁MDL锁
1.MySQL5.5引入了meta data lock简称MDL锁属于表锁范畴。2.MDL 的作用是保证读写的正确性。比如如果一个查询正在遍历一个表中的数据而执行期间另一个线程对这个 表结构做变更 增加了一列那么查询线程拿到的结果跟表结构对不上肯定是不行的。3.因此当对一个表做增删改查操作的时候加 MDL读锁当要对表做结构变更操作的时候加 MDL 写 锁 2.2.InnoDB中的行锁
1.行锁也称之为记录锁也就是仅仅把一条记录锁上官方的类型名称为 LOCK_REC_NOT_GAP 比如我们把id值为8的那条记录加一个记录锁。仅仅是锁住了id值为8的记录对周围的数据没有影响2.MySQL服务器层并没有实现行锁机制行级锁只是在存储引擎层实现3.行锁优点锁的粒度小发生冲突频率低可以实现的并发度高4.缺点对于锁的开销比较大加锁会比较慢容易出现死锁 InnoDB与MyISAM的最大不同点就是事务支持和采用了行级锁 b.举例
1.记录锁是有S锁和X锁之分的称之为 S型记录锁 和 X型记录锁2.当一个事务获取了一条记录的S型记录锁后其他事务也可以继续获取该记录的S型记录锁但不可以继续获取X型记录锁3.当一个事务获取了一条记录的X型记录锁后其他事务既不可以继续获取该记录的S型记录锁也不可以继续获取X型记录锁
b.间隙锁Gap Locks
1.MySQL 在 REPEATABLE READ 隔离级别下是可以解决幻读问题的解决方案有两种MVCC 方案或 加锁 方案2.在使用加锁方案解决时有个大问题就是事务在第一次执行读取操作时那些幻影记录尚不存在我们无法给这些 幻影记录 加上 记录锁 。InnoDB提出了一种称之为Gap Locks 的锁官方的类型名称为 LOCK_GAP 我们可以简称为 gap锁 。3.比如把id值为8的那条记录加一个gap锁的示意图如下。 图中id值为8的记录加了gap锁意味着 不允许别的事务在id值为8的记录前边的间隙插入新记录 其实就是id列的值(3, 8)这个区间的新记录是不允许立即插入的。比如有另外一个事务再想插入一条id值为4的新记录它定位到该条新记录的下一条记录的id值为8而这条记录上又有一个gap锁所以就会阻塞插入操作直到拥有这个gap锁的事务提交了之后id列的值在区间(3, 8)中的新记录才可以被插入。gap锁的提出仅仅是为了防止插入幻影记录而提出的 c.临键锁Next-Key Locks
1.有时候我们既想 锁住某条记录 又想 阻止 其他事务在该记录前边的 间隙插入新记录 所以InnoDB就提出了一种称之为 Next-Key Locks 的锁官方的类型名称为 LOCK_ORDINARY 我们也可以简称为next-key锁 。2.Next-Key Locks是在存储引擎 innodb 、事务级别在 可重复读 的情况下使用的数据库锁innodb默认的锁就是Next-Key locks
d.插入意向锁Insert Intention Locks
1.我们说一个事务在 插入 一条记录时需要判断一下插入位置是不是被别的事务加了 gap锁 next-key锁也包含 gap锁 如果有的话插入操作需要等待直到拥有 gap锁 的那个事务提交。但是InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构表明有事务想在某个 间隙 中 插入 新记录但是现在在等待。InnoDB就把这种类型的锁命名为 Insert Intention Locks 官方的类型名称为LOCK_INSERT_INTENTION 我们称为 插入意向锁2.插入意向锁是一种 Gap锁 不是意向锁在insert操作时产生3.插入意向锁是在插入一条记录行前由 INSERT 操作产生的一种间隙锁4.事实上插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁 2.3.页锁
a.页锁概述
1.页锁就是在 页的粒度 上进行锁定锁定的数据资源比行锁要多因为一个页中可以有多个行记录。当我 们使用页锁的时候会出现数据浪费的现象但这样的浪费最多也就是一个页上的数据行。页锁的开销 介于表锁和行锁之间会出现死锁。锁定粒度介于表锁和行锁之间并发度一般。2.每个层级的锁数量是有限制的因为锁会占用内存空间 锁空间的大小是有限的 。当某个层级的锁数量 超过了这个层级的阈值时就会进行 锁升级 。锁升级就是用更大粒度的锁替代多个更小粒度的锁比如 InnoDB 中行锁升级为表锁这样做的好处是占用的锁空间降低了但同时数据的并发度也下降了 3、从对待锁的态度划分:乐观锁、悲观锁 从对待锁的态度来看锁的话可以将锁分成乐观锁和悲观锁从名字中也可以看出这两种锁是两种看待 数据并发的思维方式 。需要注意的是乐观锁和悲观锁并不是锁而是锁的 设计思想 3.1.悲观锁Pessimistic Locking
悲观锁是一种思想顾名思义就是很悲观对数据被其他事务的修改持保守态度会通过数据库自身 的锁机制来实现从而保证数据操作的排它性。 悲观锁总是假设最坏的情况每次去拿数据的时候都认为别人会修改所以每次在拿数据的时候都会上 锁这样别人想拿这个数据就会 阻塞 直到它拿到锁共享资源每次只给一个线程使用其它线程阻塞 用完后再把资源转让给其它线程。比如行锁表锁等读锁写锁等都是在做操作之前先上锁当 其他线程想要访问数据时都需要阻塞挂起。Java中 synchronized 和 ReentrantLock 等独占锁就是 悲观锁思想的实现。
3.2.乐观锁Optimistic Locking
a.乐观锁概述
1.乐观锁认为对同一数据的并发操作不会总发生属于小概率事件不用每次都对数据上锁但是在更新 的时候会判断一下在此期间别人有没有去更新这个数据也就是不采用数据库自身的锁机制而是通过 程序来实现。在程序上我们可以采用 版本号机制 或者 CAS机制 实现。乐观锁适用于多读的应用类型 这样可以提高吞吐量。在Java中 java.util.concurrent.atomic 包下的原子变量类就是使用了乐观锁 begin; select * from student where id 8 and id 3 for update; 的一种实现方式CAS实现的。
b.乐观锁的版本号机制
1.在表中设计一个 版本字段 version 第一次读的时候会获取 version 字段的取值。然后对数据进行更 新或删除操作时会执行 UPDATE … SET versionversion1 WHERE versionversion 。此时 如果已经有事务对这条数据进行了更改修改就不会成功。 2.乐观锁的时间戳机制 时间戳和版本号机制一样也是在更新提交的时候将当前数据的时间戳和更新之前取得的时间戳进行 比较如果两者一致则更新成功否则就是版本冲突。 你能看到乐观锁就是程序员自己控制数据并发操作的权限基本是通过给数据行增加一个戳版本号或 者时间戳从而证明当前拿到的数据是否最新。 3.3.两种锁的适用场景
从这两种锁的设计思想中我们总结一下乐观锁和悲观锁的适用场景
乐观锁 适合 读操作多 的场景相对来说写的操作比较少。它的优点在于 程序实现 不存在死锁 问题不过适用场景也会相对乐观因为它阻止不了除了程序以外的数据库操作。悲观锁 适合 写操作多 的场景因为写的操作具有 排它性 。采用悲观锁的方式可以在数据库层 面阻止其他事务对该数据的操作权限防止 读 - 写 和 写 - 写 的冲突。 4、按加锁的方式划分显式锁、隐式锁
4.1.隐式锁 情景一 1.对于聚簇索引记录来说有一个 trx_id 隐藏列该隐藏列记录着最后改动该记录的 事务 id 。那么如果在当前事务中新插入一条聚簇索引记录后该记录的 trx_id 隐藏列代表的的就是 当前事务的 事务id 如果其他事务此时想对该记录添加 S锁 或者 X锁 时首先会看一下该记录的 trx_id 隐藏列代表的事务是否是当前的活跃事务如果是的话那么就帮助当前事务创建一个 X 锁 也就是为当前事务创建一个锁结构 is_waiting 属性是 false 然后自己进入等待状态 也就是为自己也创建一个锁结构 is_waiting 属性是 true 。 情景二 1.对于二级索引记录来说本身并没有 trx_id 隐藏列但是在二级索引页面的 Page Header 部分有一个 PAGE_MAX_TRX_ID 属性该属性代表对该页面做改动的最大的 事务id 如 果 PAGE_MAX_TRX_ID 属性值小于当前最小的活跃 事务id 那么说明对该页面做修改的事务都已 经提交了否则就需要在页面中定位到对应的二级索引记录然后回表找到它对应的聚簇索引记 录然后再重复 情景一 的做法。 session 1: session 2: 执行下述语句输出结果 mysql begin; Query OK, 0 rows affected (0.00 sec) mysql insert INTO student VALUES(34,“周八”,“二班”); Query OK, 1 row affected (0.00 sec) mysql begin; Query OK, 0 rows affected (0.00 sec) mysql select * from student lock in share mode; #执行完当前事务被阻塞 隐式锁的逻辑过程如下 A. InnoDB的每条记录中都一个隐含的trx_id字段这个字段存在于聚簇索引的BTree中。 B. 在操作一条记录前首先根据记录中的trx_id检查该事务是否是活动的事务(未提交或回滚)。如果是活 动的事务首先将 隐式锁 转换为 显式锁 (就是为该事务添加一个锁)。 C. 检查是否有锁冲突如果有冲突创建锁并设置为waiting状态。如果没有冲突不加锁跳到E。 D. 等待加锁成功被唤醒或者超时。 E. 写数据并将自己的trx_id写入trx_id字段。
显式锁 通过特定的语句进行加锁我们一般称之为显示加锁例如 显示加共享锁 显示加排它锁
5、其它锁
5.1.全局锁
1.全局锁就是对 整个数据库实例 加锁。当你需要让整个库处于 只读状态 的时候可以使用这个命令之后 其他线程的以下语句会被阻塞数据更新语句数据的增删改、数据定义语句包括建表、修改表结 构等和更新类事务的提交语句。全局锁的典型使用 场景 是做 全库逻辑备份 。 全局锁的命令
5.2.死锁
1.死锁是指两个或多个事务在同一资源上相互占用并请求锁定对方占用的资源从而导致恶性循环。死 锁示例
mysql SELECT * FROM performance_schema.data_lock_waits\G;
*************************** 1. row ***************************
ENGINE: INNODB
REQUESTING_ENGINE_LOCK_ID: 140562531358232:7:4:9:140562535668584
REQUESTING_ENGINE_TRANSACTION_ID: 422037508068888
REQUESTING_THREAD_ID: 64
REQUESTING_EVENT_ID: 6
REQUESTING_OBJECT_INSTANCE_BEGIN: 140562535668584
BLOCKING_ENGINE_LOCK_ID: 140562531351768:7:4:9:140562535619104
BLOCKING_ENGINE_TRANSACTION_ID: 15902
BLOCKING_THREAD_ID: 64
BLOCKING_EVENT_ID: 6
BLOCKING_OBJECT_INSTANCE_BEGIN: 140562535619104
1 row in set (0.00 sec)
select .... lock in share mode
select .... for update
Flush tables with read lock
事务1 事务2
1
start transaction;
update account set money10 where id1;
start transaction;
2 update account set money10 where id2;
3 update account set money20 where id2;
4 update account set money20 where id1;这时候事务1在等待事务2释放id2的行锁而事务2在等待事务1释放id1的行锁。 事务1和事务2在互 相等待对方的资源释放就是进入了死锁状态。当出现死锁以后有 两种策略 一种策略是直接进入等待直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。 另一种策略是发起死锁检测发现死锁后主动回滚死锁链条中的某一个事务将持有最少行级 排他锁的事务进行回滚让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on 表示开启这个逻辑。 第二种策略的成本分析 方法1如果你能确保这个业务一定不会出现死锁可以临时把死锁检测关掉。但是这种操作本身带有 一定的风险因为业务设计的时候一般不会把死锁当做一个严重错误毕竟出现死锁了就回滚然后 通过业务重试一般就没问题了这是 业务无损 的。而关掉死锁检测意味着可能会出现大量的超时这是 业务有损 的。 方法2控制并发度。如果并发能够控制住比如同一行同时最多只有10个线程在更新那么死锁检测 的成本很低就不会出现这个问题。 这个并发控制要做在 数据库服务端 。如果你有中间件可以考虑在 中间件实现 甚至有能力修改MySQL 源码的人也可以做在MySQL里面。基本思路就是对于相同行的更新在进入引擎之前排队这样在 InnoDB内部就不会有大量的死锁检测工作了。 5. 锁的内存结构
InnoDB 存储引擎中的 锁结构 如下 结构解析
锁所在的事务信息 不论是 表锁 还是 行锁 都是在事务执行过程中生成的哪个事务生成了这个 锁结构 这里就记录这个 事务的信息。 此 锁所在的事务信息 在内存结构中只是一个指针通过指针可以找到内存中关于该事务的更多信息比 方说事务id等。索引信息 对于 行锁 来说需要记录一下加锁的记录是属于哪个索引的。这里也是一个指针。表锁行锁信息 表锁结构 和 行锁结构 在这个位置的内容是不同的 表锁 记载着是对哪个表加的锁还有其他的一些信息。 行锁 记载了三个重要的信息 Space ID 记录所在表空间。 Page Number 记录所在页号。 n_bits 对于行锁来说一条记录就对应着一个比特位一个页面中包含很多记录用不同 的比特位来区分到底是哪一条记录加了锁。为此在行锁结构的末尾放置了一堆比特位这个 n_bits 属性代表使用了多少比特位。 n_bits的值一般都比页面中记录条数多一些。主要是为了之后在页面中插入了新记录后 也不至于重新分配锁结构type_mode 这是一个32位的数被分成了 lock_mode 、 lock_type 和 rec_lock_type 三个部分如图所示 锁的模式 lock_mode 占用低4位可选的值如下 LOCK_IS 十进制的 0 表示共享意向锁也就是 IS锁 。 LOCK_IX 十进制的 1 表示独占意向锁也就是 IX锁 。 LOCK_S 十进制的 2 表示共享锁也就是 S锁 。 LOCK_X 十进制的 3 表示独占锁也就是 X锁 。 LOCK_AUTO_INC 十进制的 4 表示 AUTO-INC锁 。 在InnoDB存储引擎中LOCK_ISLOCK_IXLOCK_AUTO_INC都算是表级锁的模式LOCK_S和 LOCK_X既可以算是表级锁的模式也可以是行级锁的模式。 锁的类型 lock_type 占用第58位不过现阶段只有第5位和第6位被使用 LOCK_TABLE 十进制的 16 也就是当第5个比特位置为1时表示表级锁。 LOCK_REC 十进制的 32 也就是当第6个比特位置为1时表示行级锁。 行锁的具体类型 rec_lock_type 使用其余的位来表示。只有在 lock_type 的值为 LOCK_REC 时也就是只有在该锁为行级锁时才会被细分为更多的类型 LOCK_ORDINARY 十进制的 0 表示 next-key锁 。 LOCK_GAP 十进制的 512 也就是当第10个比特位置为1时表示 gap锁 。 LOCK_REC_NOT_GAP 十进制的 1024 也就是当第11个比特位置为1时表示正经 记录 锁 。 LOCK_INSERT_INTENTION 十进制的 2048 也就是当第12个比特位置为1时表示插入 意向锁。其他的类型还有一些不常用的类型我们就不多说了。 is_waiting 属性呢基于内存空间的节省所以把 is_waiting 属性放到了 type_mode 这个32 位的数字中 LOCK_WAIT 十进制的 256 当第9个比特位置为 1 时表示 is_waiting 为 true 也 就是当前事务尚未获取到锁处在等待状态当这个比特位为 0 时表示 is_waiting 为 false 也就是当前事务获取锁成功。其他信息 为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表。一堆比特位 如果是 行锁结构 的话在该结构末尾还放置了一堆比特位比特位的数量是由上边提到的 n_bits 属性 表示的。InnoDB数据页中的每条记录在 记录头信息 中都包含一个 heap_no 属性伪记录 Infimum 的 heap_no 值为 0 Supremum 的 heap_no 值为 1 之后每插入一条记录 heap_no 值就增1。 锁结 构 最后的一堆比特位就对应着一个页面中的记录一个比特位映射一个 heap_no 即一个比特位映射 到页内的一条记录。 6. 锁监控
关于MySQL锁的监控我们一般可以通过检查 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 系统启动后到现在总共等待的次数等待总次数 对于这5个状态变量比较重要的3个见上面橙色。 其他监控方法 MySQL把事务和锁的信息记录在了 information_schema 库中涉及到的三张表分别是 INNODB_TRX 、 INNODB_LOCKS 和 INNODB_LOCK_WAITS 。 MySQL5.7及之前 可以通过information_schema.INNODB_LOCKS查看事务的锁情况但只能看到阻塞事 务的锁如果事务并未被阻塞则在该表中看不到该事务的锁情况。 MySQL8.0删除了information_schema.INNODB_LOCKS添加了 performance_schema.data_locks 可 以通过performance_schema.data_locks查看事务的锁情况和MySQL5.7及之前不同 performance_schema.data_locks不但可以看到阻塞该事务的锁还可以看到该事务所持有的锁。 同时information_schema.INNODB_LOCK_WAITS也被 performance_schema.data_lock_waits 所代 替。 我们模拟一个锁等待的场景以下是从这三张表收集的信息 锁等待场景我们依然使用记录锁中的案例当事务2进行等待时查询情况如下 1查询正在被锁阻塞的sql语句。 重要属性代表含义已在上述中标注。 2查询锁等待情况 mysql show status like ‘innodb_row_lock%’; ±------------------------------±------ | Variable_name | Value | ±------------------------------±------ | Innodb_row_lock_current_waits | 0 | | Innodb_row_lock_time | 0 | | Innodb_row_lock_time_avg | 0 | | Innodb_row_lock_time_max | 0 | | Innodb_row_lock_waits | 0 | ±------------------------------±------ 5 rows in set (0.01 sec) SELECT * FROM information_schema.INNODB_TRX\G; SELECT * FROM data_lock_waits\G; *************************** 1. row *************************** ENGINE: INNODB REQUESTING_ENGINE_LOCK_ID: 139750145405624:7:4:7:139747028690608 REQUESTING_ENGINE_TRANSACTION_ID: 13845 #被阻塞的事务ID 3查询锁的情况 REQUESTING_THREAD_ID: 72 REQUESTING_EVENT_ID: 26 REQUESTING_OBJECT_INSTANCE_BEGIN: 139747028690608 BLOCKING_ENGINE_LOCK_ID: 139750145406432:7:4:7:139747028813248 BLOCKING_ENGINE_TRANSACTION_ID: 13844 #正在执行的事务ID阻塞了13845 BLOCKING_THREAD_ID: 71 BLOCKING_EVENT_ID: 24 BLOCKING_OBJECT_INSTANCE_BEGIN: 139747028813248 1 row in set (0.00 sec) mysql SELECT * from performance_schema.data_locks\G; *************************** 1. row *************************** ENGINE: INNODB ENGINE_LOCK_ID: 139750145405624:1068:139747028693520 ENGINE_TRANSACTION_ID: 13847 THREAD_ID: 72 EVENT_ID: 31 OBJECT_SCHEMA: atguigu OBJECT_NAME: user PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: NULL OBJECT_INSTANCE_BEGIN: 139747028693520 LOCK_TYPE: TABLE LOCK_MODE: IX LOCK_STATUS: GRANTED LOCK_DATA: NULL *************************** 2. row *************************** ENGINE: INNODB ENGINE_LOCK_ID: 139750145405624:7:4:7:139747028690608 ENGINE_TRANSACTION_ID: 13847 THREAD_ID: 72 EVENT_ID: 31 OBJECT_SCHEMA: atguigu OBJECT_NAME: user PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY OBJECT_INSTANCE_BEGIN: 139747028690608 LOCK_TYPE: RECORD LOCK_MODE: X,REC_NOT_GAP LOCK_STATUS: WAITING LOCK_DATA: 1 *************************** 3. row *************************** ENGINE: INNODB ENGINE_LOCK_ID: 139750145406432:1068:139747028816304 ENGINE_TRANSACTION_ID: 13846 THREAD_ID: 71 EVENT_ID: 28 OBJECT_SCHEMA: atguigu OBJECT_NAME: user PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: NULL OBJECT_INSTANCE_BEGIN: 139747028816304 LOCK_TYPE: TABLE 从锁的情况可以看出来两个事务分别获取了IX锁我们从意向锁章节可以知道IX锁互相时兼容的。所 以这里不会等待但是事务1同样持有X锁此时事务2也要去同一行记录获取X锁他们之间不兼容导 致等待的情况发生。 6. 附录
间隙锁加锁规则共11个案例 间隙锁是在可重复读隔离级别下才会生效的 next-key lock 实际上是由间隙锁加行锁实现的如果切换 到读提交隔离级别 (read-committed) 的话就好理解了过程中去掉间隙锁的部分也就是只剩下行锁 的部分。而在读提交隔离级别下间隙锁就没有了为了解决可能出现的数据和日志不一致问题需要把 binlog 格式设置为 row 。也就是说许多公司的配置为读提交隔离级别加 binlog_formatrow。业务不 需要可重复读的保证这样考虑到读提交下操作数据的锁范围更小没有间隙锁这个选择是合理 的。 next-key lock的加锁规则 总结的加锁规则里面包含了两个 “ “ 原则 ” ” 、两个 “ “ 优化 ” ” 和一个 “bug” 。
原则 1 加锁的基本单位是 next-key lock 。 next-key lock 是前开后闭区间。原则 2 查找过程中访问到的对象才会加锁。任何辅助索引上的锁或者非索引列上的锁最终 都要回溯到主键上在主键上也要加一把锁。优化 1 索引上的等值查询给唯一索引加锁的时候 next-key lock 退化为行锁。也就是说如果 InnoDB扫描的是一个主键、或是一个唯一索引的话那InnoDB只会采用行锁方式来加锁优化 2 索引上不一定是唯一索引的等值查询向右遍历时且最后一个值不满足等值条件的 时候 next-keylock 退化为间隙锁。一个 bug 唯一索引上的范围查询会访问到不满足条件的第一个值为止。 我们以表test作为例子建表语句和初始化语句如下其中id为主键索引 LOCK_MODE: IX LOCK_STATUS: GRANTED LOCK_DATA: NULL *************************** 4. row *************************** ENGINE: INNODB ENGINE_LOCK_ID: 139750145406432:7:4:7:139747028813248 ENGINE_TRANSACTION_ID: 13846 THREAD_ID: 71 EVENT_ID: 28 OBJECT_SCHEMA: atguigu OBJECT_NAME: user PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY OBJECT_INSTANCE_BEGIN: 139747028813248 LOCK_TYPE: RECORD LOCK_MODE: X,REC_NOT_GAP LOCK_STATUS: GRANTED LOCK_DATA: 1 4 rows in set (0.00 sec) ERROR: No query specified sessionA sessionB sessionC begin; update test set col2 col21 where id7; insert into test values(8,8,8) (blocked) update test set col2 col21 where id10; (Query OK) sessionA sessionB sessionC begin; select id from test where col1 5 lock in share mode; update test col2 col21 where id5; (Query OK) insert into test values(7,7,7) (blocked) 案例一唯一索引等值查询间隙锁 由于表 test 中没有 id7 的记录 根据原则 1 加锁单位是 next-key lock session A 加锁范围就是 (5,10] 同时根据优化 2 这是一个等 值查询 (id7) 而 id10 不满足查询条件 next-key lock 退化成间隙锁因此最终加锁的范围是 (5,10) 案例二非唯一索引等值查询锁 这里 session A 要给索引 col1 上 col15 的这一行加上读锁。根据原则 1 加锁单位是 next-key lock 左开右闭5是闭上的因此会给 (0,5] 加上 next-key lock 。要注意 c 是普通索引因此仅访问 c5 这一条记录是不能马上停下来的可能有col15的其他记 录需要向右遍历查到c10 才放弃。根据原则 2 访问到的都要加锁因此要给 (5,10] 加 next-key lock 。但是同时这个符合优化 2 等值判断向右遍历最后一个值不满足 col15 这个等值条件因此 退化成间隙锁 (5,10) 。 CREATE TABLE test ( id int(11) NOT NULL, col1 int(11) DEFAULT NULL, col2 int(11) DEFAULT NULL, PRIMARY KEY (id), KEY c (c) ) ENGINEInnoDB; insert into test values(0,0,0),(5,5,5), (10,10,10),(15,15,15),(20,20,20),(25,25,25); sessionA sessionB sessionC begin; select * from test where id 10 and id11 for update; insert into test values(8,8,8) (Query OK) insert into test values(13,13,13); (blocked) update test set clo2col21 where id15; (blocked)根据原则 2 只有访问到的对象才会加锁这个查询使用覆盖索引并不需要访问主键索引所 以主键索引上没有加任何锁这就是为什么 session B 的 update 语句可以执行完成。 但 session C 要插入一个 (7,7,7) 的记录就会被 session A 的间隙锁 (5,10) 锁住 这个例子说明锁是加在 索引上的。 执行 for update 时系统会认为你接下来要更新数据因此会顺便给主键索引上满足条件的行加上行 锁。 如果你要用 lock in share mode来给行加读锁避免数据被更新的话就必须得绕过覆盖索引的优化因为 覆盖索引不会访问主键索引不会给主键索引上加锁 案例三主键索引范围查询锁 上面两个例子是等值查询的这个例子是关于范围查询的也就是说下面的语句 这两条查语句肯定是等价的但是它们的加锁规则不太一样开始执行的时候要找到第一个 id10 的行因此本该是 next-key lock(5,10] 。 根据优化 1 主键 id 上的等值条件退化成行锁只加了 id10 这一行的行锁。它是范围查询 范围查找就往后继续找找到 id15 这一行停下来不满足条件因此需要加 next-key lock(10,15] 。 session A 这时候锁的范围就是主键索引上行锁 id10 和 next-key lock(10,15] 。首次 session A 定位查找 id10 的行的时候是当做等值查询来判断的而向右扫描到 id15 的时候用的是范围查询判断。 案例四非唯一索引范围查询锁 与案例三不同的是案例四中查询语句的 where 部分用的是字段 c 它是普通索引 这两条查语句肯定是等价的但是它们的加锁规则不太一样 select * from test where id10 for update select * from tets where id10 and id11 for update; sessionA sessionB sessionC begin; select * from test where col1 10 and col111 for update; insert into test values(8,8,8)(blocked) update test set clo2col21 where id15; (blocked) sessionA sessionB sessionC begin; select * from test where id 10 and id15 for update; update test set clo2col21 where id20; (blocked) insert into test values(16,16,16); (blocked) 在第一次用 col110 定位记录的时候索引 c 上加了 (5,10] 这个 next-key lock 后由于索引 col1 是非唯 一索引没有优化规则也就是 说不会蜕变为行锁因此最终 sesion A 加的锁是索引 c 上的 (5,10] 和 (10,15] 这两个 next-keylock 。 这里需要扫描到 col115 才停止扫描是合理的因为 InnoDB 要扫到 col115 才知道不需要继续往后 找了。 案例五唯一索引范围查询锁 bug session A 是一个范围查询按照原则 1 的话应该是索引 id 上只加 (10,15] 这个 next-key lock 并且因 为 id 是唯一键所以循环判断到 id15 这一行就应该停止了。 但是实现上 InnoDB 会往前扫描到第一个不满足条件的行为止也就是 id20 。而且由于这是个范围扫 描因此索引 id 上的 (15,20] 这个 next-key lock 也会被锁上。照理说这里锁住 id20 这一行的行为其 实是没有必要的。因为扫描到 id15 就可以确定不用往后再找了。 案例六非唯一索引上存在 等值 的例子 这里我给表 t 插入一条新记录insert into t values(30,10,30);也就是说现在表里面有两个c10的行 但是它们的主键值 id 是不同的分别是 10 和 30 因此这两个c10 的记录之间也是有间隙的。 sessionA sessionB sessionC begin; delete from test where col110; insert into test values(12,12,12); (blocked) update test set col2col21where c15; (blocked) sessionA sessionB begin; delete from test where col110 limit 2; insert into test values(12,12,12); (Query OK) 这次我们用 delete 语句来验证。注意 delete 语句加锁的逻辑其实跟 select … for update 是类似的 也就是我在文章开始总结的两个 “ 原则 ” 、两个 “ 优化 ” 和一个 “bug” 。 这时 session A 在遍历的时候先访问第一个 col110 的记录。同样地根据原则 1 这里加的是 (col15,id5) 到 (col110,id10) 这个 next-key lock 。 由于c是普通索引所以继续向右查找直到碰到 (col115,id15) 这一行循环才结束。根据优化 2 这是 一个等值查询向右查找到了不满足条件的行所以会退化成 (col110,id10) 到 (col115,id15) 的间隙 锁。 这个 delete 语句在索引 c 上的加锁范围就是上面图中蓝色区域覆盖的部分。这个蓝色区域左右两边都 是虚线表示开区间即 (col15,id5) 和 (col115,id15) 这两行上都没有锁 案例七 limit 语句加锁 例子 6 也有一个对照案例场景如下所示 session A 的 delete 语句加了 limit 2 。你知道表 t 里 c10 的记录其实只有两条因此加不加 limit 2 删 除的效果都是一样的。但是加锁效果却不一样 sessionA sessionB begin; select id from test where col110 lock in share mode; update test set col2col21 where c10; (blocked) insert into test values(8,8,8); ERROR 1213(40001):Deadlock found when trying to getlock;try restarting transaction 这是因为案例七里的 delete 语句明确加了 limit 2 的限制因此在遍历到 (col110, id30) 这一行之后 满足条件的语句已经有两条循环就结束了。因此索引 col1 上的加锁范围就变成了从 col15,id5) 到 col110,id30) 这个前开后闭区间如下图所示 这个例子对我们实践的指导意义就是 在删除数据的时候尽量加 limit 。 这样不仅可以控制删除数据的条数让操作更安全还可以减小加锁的范围。 案例八一个死锁的例子session A 启动事务后执行查询语句加 lock in share mode 在索引 col1 上加了 next-keylock(5,10] 和 间隙锁 (10,15) 索引向右遍历退化为间隙锁session B 的 update 语句也要在索引 c 上加 next-key lock(5,10] 进入锁等待 实际上分成了两步 先是加 (5,10) 的间隙锁加锁成功然后加 col110 的行锁因为sessionA上已经给这行加上了读 锁此时申请死锁时会被阻塞然后 session A 要再插入 (8,8,8) 这一行被 session B 的间隙锁锁住。由于出现了死锁 InnoDB 让 session B 回滚 案例九order by索引排序的间隙锁1 如下面一条语句 下图为这个表的索引id的示意图。首先这个查询语句的语义是 order by id desc 要拿到满足条件的所有行优化器必须先找到 “ 第 一个 id12 的值 ” 。这个过程是通过索引树的搜索过程得到的在引擎内部其实是要找到 id12 的这个值只是最终 没找到但找到了 (10,15) 这个间隙。 id15 不满足条件所以 next-key lock 退化为了间隙锁 (10, begin; select * from test where id9 and id12 order by id desc for update; sessionA sessionB begin; select * from test where col115 and c20 order by col1 desc lock in share mode; insert into test values(6,6,6); (blocked) sessionA sessionB begin; select col1 from test where col15 lock in share mode; update test set col11 where col15 (Query OK) update test set col15 where col11; (blocked)
。
然后向左遍历在遍历过程中就不是等值查询了会扫描到 id5 这一行又因为区间是左开右 闭的所以会加一个next-key lock (0,5] 。 也就是说在执行过程中通过树搜索的方式定位记录 的时候用的是 “ 等值查询 ” 的方法。 案例十order by索引排序的间隙锁2由于是 order by col1 desc 第一个要定位的是索引 col1 上 “ 最右边的 ”col120 的行。这是一个非 唯一索引的等值查询 左开右闭区间首先加上 next-key lock (15,20] 。 向右遍历col125不满足条件退化为间隙锁 所以会 加上间隙锁(20,25) 和 next-key lock (15,20] 。在索引 col1 上向左遍历要扫描到 col110 才停下来。同时又因为左开右闭区间所以 next-key lock 会加到 (5,10] 这正是阻塞session B 的 insert 语句的原因。在扫描过程中 col120 、 col115 、 col110 这三行都存在值由于是 select * 所以会在主键 id 上加三个行锁。 因此 session A 的 select 语句锁的范围就是索引 col1 上 (5, 25) 主键索引上 id15 、 20 两个行锁。 案例十一update修改数据的例子-先插入后删除 注意根据 col15 查到的第一个记录是 col110 因此不会加 (0,5] 这个 next-key lock 。 session A 的加锁范围是索引 col1 上的 (5,10] 、 (10,15] 、 (15,20] 、 (20,25] 和(25,supremum] 。 之后 session B 的第一个 update 语句要把 col15 改成 col11 你可以理解为两步插入 (col11, id5) 这个记录删除 (col15, id5) 这个记录。 通过这个操作 session A 的加锁范围变成了图 7 所示的样子 好接下来 session B 要执行 update t set col1 5 where col1 1 这个语句了一样地可以拆成两步插入 (col15, id5) 这个记录删除 (col11, id5) 这个记录。 第一步试图在已经加了间隙锁的 (1,10) 中插入数据所以就被堵住 了。