MySQL锁相关总结|悲观锁、乐观锁、读锁、写锁、表锁、行锁、页面锁、间隙锁、临键锁

作者:微信小助手

发布时间:2024-09-14T08:26:49

MySQL锁总体结构

总结

MySQL 的锁可以分成三类:总体、类型、粒度。

  1. 总体上分成两种:乐观锁和悲观锁
  2. 类型上也是两种:读锁和写锁
  3. 锁的粒度上可以分成五种:表锁,行锁,页面锁,间隙锁,临键锁

下面我们就来详细讲一下这些锁

1. 悲观锁

悲观锁对于数据库中数据的读写持悲观态度,即在整个数据处理的过程中,他会悲观认为数据不会保持一致性,所以是会将相应的数据锁定。在数据库中,悲观锁的实现是依赖数据库提供的锁机制。

如果加上了悲观锁,那么就无法对这些数据进行读取操作。

2. 乐观锁

乐观锁对于数据库的数据的读写持乐观态度,即在整个数据处理的过程中,他会很乐观的认为数据会保持一致性,所以不加锁,而是通过数据版本记录机制实现。

MySQL中的MVCC多版本控制就是乐观锁的一种实现方式。

  1. 往往会在数据表中增加 一个类型version的版本号字段
  2. 在查询数据库中的数据时,会将版本号字段的值一起读取出来。
  3. 当更新数据时,会令 版本号字段的值加1。将提交数据的版本与数据库表对应记录的版本进行对比。
  4. 如果提交的数据版本号 大于数据表中当前要修改的数据的版本号,则数据进行修改操作。
  5. 否则不修改数据库表中的数据。

3. 读锁

读写又称为共享锁或者S锁(Shared Lock),针对同一份数据,可以加多个读锁而互不影响。

4. 写锁

写锁又称为排他锁或者X锁(Exclusive Lock),如果当前写锁未释放,他会阻塞其他的写锁和读锁。

5. 表锁

表锁也称为表级锁,就是在整个数据表上对数据进行加锁和释放锁。特点:开销小,加速快,粒度大,并发度最低,发生锁冲突概率高。

在MySQL中,有两种表锁模式:一种是表共享锁(Table Shard Lock),另一种是表独占写锁(Table Write Lock)

当一个线程获取到一个表的读锁后,其他线程仍然可以进行读操作,但不能对表进行写操作。那么对应的如果一个线程获取到一个表的写锁后,只有这个线程可以进行读写操作,其他线程无法对表进行读写操作,直到写锁被释放为止。

在mysql中可以通过以下命令手动添加表锁

LOCK TABLE 表名称 read(write);

eg: 添加读表锁

LOCK TABLE user_table read;

eg: 添加写表锁

LOCK TABLE user_table write;

使用如下命令可以查看数据表上增加的锁

SHOW OPEN TABLES;

删除表锁:

UNLOCK TABLES;

6. 行锁

行锁也称为行级别,就是在数据行上对数据进行加锁和释放锁。特点:开销大,加锁慢,粒度小,并发度高,锁冲突概率最小。

在mysql的InnoDB存储引擎中有两种行锁,排他锁和共享锁。

  • 共享锁:允许一个事务读取一行数据,但 不允许一个事务对加了共享锁的当前行增加排他锁
  • 排他锁:允许当前事务对数据行进行增删改查操作,不允许 其他事务对增加了排他锁的数据行增加共享锁和排他锁

注意:

  1. 行锁主要加在索引上,如果 对非索引字段设置条件进行更新,行锁可能会变成表锁。例如 UPDATE user_table SET number = 2 WHERE name = 'fanone' 如果name没有加索引,那么可能会进行表锁。所以我们一般建议使用主键id作为更新数据的查询条件。
  2. InnoDB的行锁是针对索引加锁,不是针对记录加锁,并且加锁的索引 不能失效,否则行锁也有可能变成表锁。而导致索引失效的有很多,比如联合索引不遵循最左匹配原则会失效、OR会失效等等...
  3. 锁定某一行时,可以使用 lock in share mode命令来指定共享锁,使用 for update命令来指定排他锁。
UPDATE user_table SET number = 2 WHERE name = 'fanone' LOCK IN SHARE MODE;
UPDATE user_table SET number = 2 WHERE name = 'fanone' FOR UPDATE;

7. 页面锁

页级锁定是 MySQL 中比较独特的一种锁定级别。特点:锁定颗粒度介于行级锁定与表级锁之间,锁开销和加锁时间界于表锁和行锁之间,并发处理能力也同样是介于上面二者之间,并发度一般。

不过,随着锁定资源颗粒度的减小,应用程序的访问请求遇到锁等待的可能性也会随之降低,系统整体并发度也随之提升。

使用页级锁定的主要是 BerkeleyDB 存储引擎。

8. 间隙锁

在mysql中使用范围查询的时,如果请求共享锁或者排他锁,InnoDB会给符合条件的已有数据的索引项加锁。如果键值在条件范围内,而这个范围内并不存在记录,而认为此时出现了间隙,InnoDB会对这个间隙进行加锁,这也称为间隙锁。

eg:

SELECT FROM user_user;
+
----+-------+-------+
|id  |  name |  sex  |
+
----+-------+-------+
| 1  |zhangsan| 1    |
| 2  |lisi    | 2    |
| 3  |lisi2   | 2    |
| 7  |lisi3   | 2    |
| 10 |lisi4   | 2    |
| 21 |lisi5   | 2    |
+
----+-------+-------+

上面出现了间隙有 (3,7], (7,10], (10,21],(21,+∞] 的三个区间。

如果执行以下sql

UPDATE user_user SET sex = 1 WHERE id > 8 AND id < 18;

那么其他事务就无法在 (7,21] 这个区间内插入或者修改任何数据。间隙锁会锁住  (7,10], (10,21] 这两个间隙。不过间隙锁只会在 可重复读事务隔离级别 下才会生效。

9. 临键锁

临键锁就是行锁和间隙锁的组合,也可以理解为一种特殊的间隙锁。通过临建锁可以解决幻读的问题。 

每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据

需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关 ,在唯一索引列(包括主键列)上不存在临键锁。

上面的(7,21]就是临键锁。

参考:

[1] https://www.jianshu.com/p/478bc84a7721