数据库锁是用于管理并发访问数据库的机制,其主要作用是控制多个事务对共享资源(如数据表、数据行)的访问,以确保数据的一致性和完整性。锁可以防止多个事务同时对同一资源进行不同的操作,从而避免并发问题。

从锁的粒度划分

  1. 行级锁(Row-level Lock): 行级锁是一种细粒度的锁,允许事务锁定表中的某一行或某几行,而不是整个表。这种锁的作用是减小锁的粒度,允许并发性更高,减少锁冲突。
  2. 表级锁(Table-level Lock): 表级锁是锁定整个表的锁,它的粒度比行级锁更粗,通常会阻塞其他事务的操作,因此在高并发情况下可能导致性能问题。表级锁的使用应该尽量避免,除非确实需要锁定整个表。
  3. 页级锁:是粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折中的页级,一次锁定相邻的一组记录。

从使用性质划分

InnoDB 实现了标准的行级锁,包括两种:共享锁(简称 s 锁)、排它锁(简称 x 锁)。

  1. 共享锁(Shared Lock): 也称为读锁。多个事务可以同时持有共享锁,并且不会阻塞其他事务的共享锁。共享锁用于读取操作,允许多个事务同时读取相同的资源,但阻止其他事务持有排他锁。
  2. 排他锁(Exclusive Lock): 也称为写锁。只有一个事务可以持有排他锁,其他事务无法同时持有共享锁或排他锁。排他锁用于写入操作,确保只有一个事务可以修改资源,防止其他事务读取或写入相同的资源。
  3. 意向锁(Intention Lock): 意向锁是用来表示事务打算对资源进行哪种类型的锁定(共享锁或排他锁)。意向锁不会阻止其他事务获取共享或排他锁,但它们提供了一种机制,让事务了解其他事务的锁定意图,以便更好地管理锁。

从主观上划分

  1. 乐观锁(Optimistic Lock):顾名思义,从主观上认定资源是不会被修改的,所以不加锁读取数据,仅当更新时用版本号机制等确认资源是否被修改。乐观锁适用于多读的应用类型,可以系统提高吞吐量。通常通过增加额外字段来实现,但仅仅体现一种思想,进行数据库操作时还是使用的mysql底层的锁(悲观锁)
  2. 悲观锁(Pessimistic Lock):正如其名,具有强烈的独占和排它特性,每次读取数据时都会认为会被其它事务修改,所以每次操作都需要加上锁。

InnoDB中的锁算法(加锁模式)

  • Record Lock:单个行记录上的锁。
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
  • Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身
  • 它们都是行级锁的实现

记录锁(Record Lock)

对表中的记录加锁(锁住索引记录,而不是真正的数据记录)叫做记录锁,简称行锁(区别于行级锁)。例如下面的sql会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行。注意:id 列必须为唯一索引列或主键列,同时查询语句必须为精准匹配(=),不能为 >、<、like 等,否则会退化成临键锁。

1
SELECT * FROM `test` WHERE `id`=1 FOR UPDATE;

间隙锁(Gap Locks)

间隙锁是一种特殊的锁,用于锁定范围内的间隙(两个值之间的空间),存在于非唯一索引中,以防止其他事务插入新行。这有助于避免幻读问题。当事务查询数据时,InnoDB可以自动获取间隙锁来保护查询范围。例如emp 表中只有 101 条记录,其 empid 的值分别是1, 2, …, 100, 101,下面的SQL不仅会对符合条件的 empid 值为 101 的记录加锁,也会对 empid 大于 101(这些记录并不存在)的 “间隙” 加锁。这时如果插入 empid 等于 102 的数据,如果那边事务还没有提交,就会处于等待状态,无法插入数据

1
SELECT * FROM emp WHERE empid > 100 FOR UPDATE

临键锁(Next-Key Locks)

Next-key 锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁,存在于非唯一索引中。 InnoDB使用Next-Key Locks来处理范围查询和避免幻读。这种锁将行级锁与间隙锁结合起来,确保不会有新行插入到查询范围内。可以理解为一种特殊的间隙锁。通过临建锁可以解决幻读的问题。每个数据行上的非唯一索引列(该索引里面的值允许重复)上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段 左开右闭 区间的数据。例如,假设有如下表,其中id 是主键,age是普通索引。图片损坏该表中 age 列潜在的临键锁有:(-∞, 10],(10, 24],(24, 32],(32, 45],(45, +∞]。

1
2
3
4
5
6
7
8
9
在事务 A 中执行如下命令之一:
-- 根据非唯一索引列 UPDATE 某条记录
UPDATE table SET name = Vladimir WHERE age = 24;
-- 或根据非唯一索引列 锁住某条记录
SELECT * FROM table WHERE age = 24 FOR UPDATE;

会导致以下命令被阻塞:
(事务A在对age为24的列进行UPDATE操作的同时也获取了(10, 32]这个区间内的临键锁)
INSERT INTO table VALUES(100, 26, 'tianqi');

这里对 记录锁、间隙锁、临键锁 做一个总结:
InnoDB 中的行锁的实现依赖于索引,一旦某个加锁操作没有使用到索引,那么该锁就会退化为表锁。
记录锁 存在于包括 主键索引 在内的唯一索引中,锁定 单条索引 记录。
间隙锁 存在于 非唯一索引 中,锁定开区间范围内的一段间隔,它是基于 临键锁 实现的。
临键锁 存在于 非唯一索引 中,该类型的每条记录的索引上都存在这种锁,它是 一种特殊的间隙锁,锁定一段 左开右闭 的索引区间。

拓展

意向锁

意向锁分为 意向共享锁(IS) 和 意向排他锁(IX),是表锁的实现,存在的目的是为了让 InnoDB 中的行锁和表锁更高效的共存

  • 意向共享锁(IS)和 意向排他锁(IX)都是 表锁
  • 意向锁是一种 不与行级锁冲突的表级锁,这一点非常重要。
  • 意向锁是 InnoDB 自动加的, 不需用户干预。
  • 意向锁是在 InnoDB 下存在的内部锁,对于MyISAM 而言没有意向锁之说。
  1. 当某个事务user表中的某行上了排他锁时(例如记录、间隙、临键等),另一个事务此时需要获取表锁,因为共享锁与排他锁互斥,所以事务B对user表加共享锁的时候,必须保证:当前没有其他事务持有 users 表的排他锁,当前没有其他事务持有 users 表中任意一行的排他锁 。后者必须通过遍历表中的每一行才能知道,这是一种低效的做法。
  2. 有了意向锁之后,事务 B 只要看表上有没有意向排他锁(某行上了排他锁时会自动为表获取),有则说明表中有些行被行锁锁住了,因此,事务 B 申请表的写锁会被阻塞。
  3. 意向锁与意向锁之间永远是兼容的,因为不论加行级的 X 锁或 S 锁,都会自动获取表级的 IX 锁或者 IS 锁。也就是有 10 个事务,对不同的 10 行加了行级 X 锁,那么这个时候就存在 10 个 IX 锁。这 10 个 IX 存在的作用就是假如这个时候有个事务,想对整个表加排它 X 锁,那它不需要遍历每一行是否存在 S 或 X 锁,而是看有没有存在意向锁,只要存在一个意向锁,那这个事务就加不了表级排它(X)锁,要等上面 10 个 IX 全部释放才行。

插入意向锁

  • 插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种间隙锁。
  • 虽然插入意向锁中含有意向锁三个字,但是它并不属于意向锁而属于间隙锁,因为意向锁是表锁,而插入意向锁是行锁。
  • 假设存在两条值分别为 4 和 7 的记录,两个不同的事务分别试图插入值为 5 和 6 的两条记录,每个事务在获取插入行上独占的(排他)锁前,都会获取(4,7]之间的间隙锁,但是因为数据行之间并不冲突,所以两个事务之间并不会产生冲突(阻塞等待)。
  • 插入意向锁是一种特殊的间隙锁 ,两个插入意向锁之间互不排斥,所以即使多个事务在同一区间插入多条记录,只要记录本身(主键、唯一索引)不冲突,那么事务之间就不会出现冲突等待。也就是说插入意向锁在锁定区间相同但记录行本身不冲突的情况下互不排斥。
  • 具体的说,当多个事务并发地进行插入操作时,如果它们的插入数据范围没有重叠,就可以避免插入意向锁的竞争,提高并发性能。但如果它们的插入数据范围有重叠,可能需要等待其他事务释放锁,以协调插入操作。
1
2
3
4
5
6
7
-- 事务1
START TRANSACTION;
INSERT INTO table_name (id, name) VALUES (5, 'John') LOCK IN SHARE MODE;

-- 事务2
START TRANSACTION;
INSERT INTO table_name (id, name) VALUES (7, 'Jane') LOCK IN SHARE MODE; -- 不会被阻塞,因为是不同的索引区间

总结

mysql锁分类:
按粒度:行级锁、表级锁、页级锁
按使用性质:共享锁、排他锁、意向锁
按主观:悲观锁、乐观锁
按加锁模式:记录锁、间隙锁、临键锁、意向锁、插入意向锁
其中粒度,使用性质,主观都是逻辑上的分类
行级锁有行锁(记录锁),间隙锁,临键锁
表级锁有表锁,意向锁,元数据锁(防止数据定义语句DDL数据操作语句DML起冲突)