锁
本地测试的环境:InnoDB 存储引擎;数据库隔离级别:repeatable read;
为什么需要锁呢?
并发下,多个事务对同一个数据进行修改,需要通过锁来保证事务的隔离性和一致性。
数据库锁分类
1、共享锁、排他锁(从数据库管理的角度对锁进行划分)
共享锁 share lock
读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。
如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
select ... lock in share mode;
排他锁 exclusive lock
如果事务T对数据A加上排他锁后,则其他事务不能再对A加任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
select ... for update;
意向锁
意向锁是表级锁,其设计目的主要是为了在一个事务中揭示下一行将要被请求锁的类型。InnoDB中的两个表锁:
意向共 享锁(IS):表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁
意向排他锁(IX):类似上面,表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。
2、乐观锁、悲观锁(从程序员的角度对锁进行划分)
乐观锁和悲观锁并不是锁,而是锁的设计思想。
乐观锁
乐观锁(Optimistic Locking)认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁, 也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用版本号机制或者时间戳机制实现。
-
乐观锁的版本号机制 在表中设计一个版本字段 version,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时, 会执行
UPDATE ... SET version=version+1 WHERE version=version
。此时如果已经有事务对这条数据进行了更改, 修改就不会成功。 -
乐观锁的时间戳机制 时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较, 如果两者一致则更新成功,否则就是版本冲突。
悲观锁
悲观锁(Pessimistic Locking)也是一种思想,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现 , 从而保证数据操作的排它性。
适用场景
- 乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁问题, 不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
- 悲观锁适合写操作多的场景,因为写的操作具有排它性。采用悲观锁的方式,可以在数据库层面阻止 其他事务对该数据的操作权限,防止读 - 写和写 - 写的冲突。
3、行级锁、表级锁、页级锁(从数据库管理的角度对锁进行划分)
- 行级锁 innodb 引擎,只当前操作的行进行加锁。行级锁分为
共享锁
和排他锁
- 表级锁 MyISAM 引擎,innodb 引擎,对当前操作的整张表加锁。表级锁定分为
表共享锁
和表独占写锁
- 页级锁 BDB 引擎,一次锁定相邻的一组记录。
InnoDB 行锁
是通过给索引项加锁来实现的。只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB
将使用表锁!
- 在不通过索引条件查询的时候,InnoDB 确实使用的是表锁,而不是行锁。
- 由于 MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
- 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不用的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB 都会使用行锁来对数据加锁。
- 即便在条件中使用了索引字段,但**是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的 **, 如果 MySQL 认为全表扫 效率更高,比如对一些很小的表,它 就不会使用索引,这种情况下 InnoDB 将使用表锁, 而不是行锁。因此,在分析锁冲突时, 别忘了检查 SQL 的执行计划,以确认是否真正使用了索引。
死锁
MyISAM 是不会出现死锁问题的,因为它总是一次性获取所需的全部锁,要么全部满足,要么全部等待。但是在 InnoDB 中,锁是逐步获取的,这就造成了死锁的可能。
MySQL 中,行级锁并不是直接锁记录,而是锁索引。索引分为主键索引和非主键索引两种。 如果一条 sql 语句操作了主键索引,MySQL 就会锁定这条主键索引。 如果一条 sql 语句操作了非主键索引,MySQL 会先锁定该非主键索引,再锁定相关的主键索引。
在 update、delete 语句中,MySQL 不仅锁定 where 条件扫描的所有索引记录,而且会锁定相邻的键值,即所谓的 next-key locking.
当两个事务同时执行,一个锁住了主键索引,在等待其他相关索引。另一个锁定了非主键索引,在等待主键索引。这就会发送死锁。
发送死锁后,InnoDB 一般都可以检测到,并使一个事务回退,另一个获取锁完成事务。
避免死锁
- 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁几率。
- 在同一个事务中,尽可能做到一次锁定所需要的所有资源。
- 对于非常容易死锁的业务部分,尝试使用表级锁。
行级锁
不同的隔离级别,行级锁的种类是不同的。
- read uncommitted , 只有记录锁,也就是仅仅把一条记录锁上。
- repeatable read , 记录锁,间隙锁,
行级锁的种类
- Record Lock 记录锁,也就是仅仅把一条记录锁上;
- S 锁:共享锁,一个事务对一条记录加了 S 锁之后,允许其他事务继续对这条记录加 S 锁,不允许加 X 锁。
- X 锁:排它锁,一个事务对一条记录加了 X 锁之后,不允许其他事务对这条记录加任何锁。
- Gap Lock 间隙锁,锁定一个范围,但是不包含记录本身。只存在于 repeatable read 隔离级别中
- 前开后开
- 是为了解决可重复读隔离级别下的幻读问题。(通过锁定两个值之间的间隙,确保 InnoDB 在这些间隙内不会插入新的记录,从而避免幻读)
- 间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系 ,因为间隙锁的目的是防止插入幻影记录而提出的。 (间隙锁就是为了防止幻读的,使用了间隙锁锁定了一定范围的记录,那么其他事务就不能在这个范围内新增记录了,这就防止了幻读问题。)
- Next-key Lock : Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
- 前开后闭
- Next-key Lock 即能保护该记录,又能阻止其他事务将新记录插入到被保护记录前面的间隙中。
- 如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。
MySQL 是如何加锁的。
加锁的对象是索引。加锁的基本单位是 next-key lock
next-key lock 在一些场景下会退化成记录锁或间隙锁。
在能使用记录锁或者间隙锁就能避免幻读现象的场景下, next-key lock 就会退化成记录锁或间隙锁。 例如,如果一个事务只查询一个索引列,那么 InnoDB 只需要对这个索引列加锁,而不需要加 next-key lock。
唯一索引等值查询
当我们用唯一索引进行等值查询的时候,查询的记录存不存在,加锁的规则也会不同:
- 当查询的记录是「存在」的,在索引树上定位到这一条记录后,将该记录的索引中的 next-key lock 会退化成「记录锁」。
- 当查询的记录是「不存在」的,在索引树找到第一条大于该查询记录的记录后,将该记录的索引中的 next-key lock 会退化成「间隙锁」。
记录存在
begin;
select * from user where id = 1 for update;
-- 查看事务执行过程中加了什么锁。
select * from performance_schema.data_locks\G;
为什么唯一索引等值查询并且查询记录存在的场景下,该记录的索引中的 next-key lock 会退化成 record lock 呢?
原因就是在唯一索引等值查询并且查询记录存在的场景下,仅靠记录锁也能避免幻读的问题。
- 主键具有唯一性,不允许重复添加
- 对主键加记录锁,避免删除主键。
记录不存在
begin;
select * from user where id = 2 for update;
select * from performance_schema.data_locks\G;
next-key lock
会退化成 gap lock
。
会在主键在 (1,5) 范围区间内加上间隙锁。
- 在 (1,5) 增加数据会被阻塞
- 增加主键为 5 的数据会被禁止,主键具有唯一性。
为什么唯一索引等值查询并且查询记录不存在的场景下,在索引树找到第一条大于该查询记录的记录后, 要将该记录的索引中的 next-key lock 退化成间隙锁?
原因就是在唯一索引等值查询并且查询记录不存在的场景下,仅靠间隙锁也能避免幻读的问题。
- 为什么 id = 5 记录上的主键索引的锁不是 next-key lock?
因为仅靠 gap lock 就可以解决幻读问题。使用 next-key lock 会把 id = 5 的记录使用记录锁锁住,其他事务就不能删除这条数据, 但这样做是没有必要的,因为 id = 5 的记录删除与不删除,都不会影响 id = 2 的记录的查询结果。
- 为什么不可以针对不存在的记录加锁呢 ?
因为锁是加载索引上的
总结
- 记录存在,next-key lock 会退化成 record lock
- 记录不存在,大于当前等值条件的第一个值,在这个值的索引上加间隙锁。 (此时,next-key lock 退化为 gap lock).
唯一索引范围查询
当唯一索引进行范围查询时,会对每一个扫描到的索引加 next-key lock
- 针对
>=
的范围查询,因为存在等值查询的条件,那么如果等值查询的记录时存在于表中,那么该记录 的索引中的 next-key lock 会退化成 record lock。- 针对
<
<=
的范围查询,要看条件值的记录是否存在于表中
- 如果不存在,扫描到终止范围查询的记录时,该记录的索引的 next-key lock 会退回成间隙锁,其他扫描到的记录,都是在
这些记录的索引上加 next-key lock
- 如果存在
- 条件是
<
的范围查询,扫描到终止范围查询记录时,该记录的索引的 next-key lock 会退化成 gap lock,其他扫描到的记录都是在这些记录的索引上加 next-key lock
- 条件是
<=
的范围查询,扫描到的记录都是加 next-key lock
supremum pseudo-record
在 MySQL InnoDB 存储引擎中,每个索引的 B+
树结构的最末端,存在一个特殊的记录,称为 supremum pseudo-record
(最大伪记录)。
这个记录不对应任何表中的实际数据行,而是一个内部机制,用于辅助 InnoDB 进行各种操作,特别是与锁定机制和隔离级别相关的操作。
- 边界标记
- 锁定机制
- 查询优化
>
范围查询
边界值存在
begin;
select * from user where id > 15 for update;
select * from performance_schema.data_locks\G;
边界值不存在
begin;
select * from user where id > 14 for update;
select * from performance_schema.data_locks\G;
总结
- 边界值存不存在,都是加 next-key lock
>=
范围查询
边界值存在
begin;
select * from user where id >= 15 for update;
select * from performance_schema.data_locks\G;
边界值不存在
begin;
select * from user where id >= 14 for update;
select * from performance_schema.data_locks\G;
总结
- 如果边界值存在,边界值上的锁会退化成记录锁。其他扫描到的值会加上 next-key lock
- 如果边界值不存在,所有扫描到的值都加上 next-key lock
<
范围查询
边界值不存在
begin;
select * from user where id <6 for update;
select * from performance_schema.data_locks\G;
边界值存在
begin;
select * from user where id <5 for update;
select * from performance_schema.data_locks\G;
总结
在执行 <
范围查询的时候,对于被查找到的值,会加上 next-key lock, 一直扫描到超过边界范围,找到第一个超过边界值的记录,
给这个记录加上间隙锁。
<=
范围查询
边界值不存在
begin;
select * from user where id <= 6 for update;
select * from performance_schema.data_locks\G;
边界值存在
begin;
select * from user where id <= 5 for update;
select * from performance_schema.data_locks\G;
总结
在执行 <=
范围查询的时候,对于被查找到的值,会加上 next-key lock
,一直扫描到边界值,如果边界值存在,给边界值加上 next-key lock
后
停止扫描,否则的话,继续往后扫描。知道找到超过边界值的第一条记录,给这个记录加上 gap lock
.