概述
本章,您将学习到 MySQL 8 中有关锁机制的知识,内容包括:
- 锁机制的分类
- 演示 InnoDB 存储引擎的行锁
- 演示 InnoDB 存储引擎的表锁
- 锁的查看
- 有关乐观锁、全局锁和死锁的内容
- MVCC
由于内容较多,本文档仅来说明乐观锁、全局锁和死锁的内容。
乐观锁
乐观锁:又名乐观并发控制(optimistic concurrency control,缩写 "OCC"),也是一种并发控制的方法。和悲观锁相反,乐观锁假定认为操作的数据在一般情况下不会造成冲突,所以不会对操作的数据进行加锁,只有到数据提交时才通过一种机制(常见的是对记录的数据进行版本比对)来验证数据是否冲突。同样的,悲观锁是一种机制思想,并不是实际操作过程中特指的某一个锁。
乐观锁的这种机制思想适用于那种读多写少的场景。
一般而言,乐观锁需要开发者自己手动去实现,常见的有:
-
版本号
实现步骤如下:
- 在表中添加一个 version 字段(整数类型)
- 读取数据时,同时获取 version 值
- 更新数据时,检查当前 version 是否与读取时一致。若一致,执行更新操作并 version + 1;若不一致,说明数据已被其他事务修改,更新失败
# 读取数据 SELECT id,data,version FROM example WHERE id = 1; # 更新数据 UPDATE example SET data = 'new data', version = version + 1 WHERE id = 1 AND version = 5; -
时间戳
实现步骤如下:
- 在数据表中添加 timestamp 字段(DATETIME 类型)
- 读取数据时,同时获取 timestamp 值
- 更新数据时,检查当前 timestamp 是否与读取时一致。若一致,执行更新操作并让时间戳字段更新到当前时间;若不一致,说明数据已被其他事务修改,更新失败
# 读取数据 SELECT id,name,timestamp FROM user WHERE id = 1; # 更新数据 UPDATE user SET name = 'New Name', timestamp = NOW() WHERE id = 1 AND timestamp = '2025-12-17 14:00:00'; -
版本号 + 时间戳
全局锁
全局锁:对当前 MySQL 实例中的所有表进行加锁,使它们变成只读状态,后续的 DDL、DML 以及涉及到事务的提交都会被阻塞,仅允许 DQL。
当要对全库进行逻辑备份时,为了保证数据的完整性和一致性,通常都会使用全局锁。需要注意的是,全局锁会让业务中断,因此在绝大部分的场景下,都应该尽量避免使用全局锁。
相关语法:
# 开启全局锁
flush tables with read lock;
# 解除全局锁
unlock tables;
逻辑备份:即备份的数据以 SQL 语句的形式进行存储
死锁
死锁:指两个或多个事务在同一个资源上相互占用,事务之间都在等待对方释放锁,这种死循环的现象就被称为死锁。
死锁几乎出现在每个关系型数据库当中,在 MySQL 8 的 InnoDB 存储引擎中,死锁的错误提示为 —— "Deadlock found when trying to get lock; try restarting transaction"
下面演示了死锁的出现以及存储引擎遇到死锁现象时的策略(依旧是使用前文的 locks 库):
use locks;
desc tlock1;
+-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | varchar(10) | YES | | NULL | |
+-------+-------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)
select * from tlock1;
+----+------+
| id | name |
+----+------+
| 1 | kone |
| 7 | john |
| 15 | lee |
+----+------+
3 rows in set (0.00 sec)
| Session 1 | Session 2 |
|---|---|
| use locks; set autocommit=0; start transaction; update tlock1 set name='jack' where id=1; |
|
| use locks; set autocommit=0; start transaction; update tlock1 set name='zhang' where id=7; |
|
| update tlock1 set name='juliet' where id=7; | |
| update tlock1 set name='mike' where id=1; | |
| Query OK, 1 row affected (23.96 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
|
| rollback; | |
| rollback; |
- Session 1 中,
update tlock1 set name='jack' where id=1;表示添加了 MDL 读锁(行级排他锁) - Session 1 中,
update tlock1 set name='juliet' where id=7;会阻塞,虽然 Session 2 中update tlock1 set name='zhang' where id=7;也是 MDL 读锁(行级排他锁),但是由于更新的是同一行数据,因此会阻塞 - Session 2 中,
update tlock1 set name='mike' where id=1;执行时出现了死锁,并输出 "ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction" - 由于发现了死锁,因此 Session 1 中的
update tlock1 set name='juliet' where id=7;会执行成功,因为存储引擎发现了死锁并应用了相应的策略
当 MySQL 8 的 InnoDB 存储引擎出现死锁现象后,会有两种可选的应对策略:
- 进入到等待时间 - 等待的时间由系统变量
innodb_lock_wait_timeout决定,默认 50s 。注意!该系统变量不仅是发生阻塞时的最长等待时间,也是死锁的最长等待时间。超时之后不会发生回滚,因为系统变量innodb_rollback_on_timeout的默认值为 OFF(0) - 死锁检测并回滚 - 当发生死锁时,主动回滚某一个事务,优先回滚小事务(数据修改量最小的事务),若事务修改的数据量相同,则回滚等待时间最长的事务。由系统变量
innodb_deadlock_detect的值来决定是否打开死锁检测,默认值为 ON(1)
select @@global.innodb_lock_wait_timeout;
+-----------------------------------+
| @@global.innodb_lock_wait_timeout |
+-----------------------------------+
| 50 |
+-----------------------------------+
1 row in set (0.00 sec)
select @@global.innodb_rollback_on_timeout;
+-------------------------------------+
| @@global.innodb_rollback_on_timeout |
+-------------------------------------+
| 0 |
+-------------------------------------+
1 row in set (0.00 sec)
select @@global.innodb_deadlock_detect;
+---------------------------------+
| @@global.innodb_deadlock_detect |
+---------------------------------+
| 1 |
+---------------------------------+
1 row in set (0.00 sec)










