文章目录
- 一、锁的类型
- 二、全局锁
- 三、表级锁
-
- 1.表锁
- [2.元数据锁(meta data lock,MDL)](#2.元数据锁(meta data lock,MDL))
- [3. 意向锁](#3. 意向锁)
- 四、行级锁
- 五、补充
一、锁的类型
MySQL 中的锁按照锁粒度分为以下三类:
- 全局锁:锁定数据库中的所有表。
- 表级锁:每次操作锁住整张表。
- 行级锁:每次操作锁住对应的行数据。
二、全局锁
全局锁是对整个数据库实例加锁,加锁后整个实例处于只读状态,后续的 DML 写语句、DDL 语句以及更新操作的事务提交语句都将被阻塞。
- 其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。
- 如下图例所示,如果不进行加锁,那么在备份过程中进行了业务操作会导致前后数据不一致,加全局锁后,再进行DML和DDL会阻塞,但进行DQL不会阻塞。


sql
-- 加全局锁
flush tables with read lock ;
--进行数据备份
mysqldump -uroot -p1234 itcast > itcast.sql
-- 解锁
unlock tables ;
全局锁带来的问题
数据库中加全局锁是一个比较重的操作,存在以下问题:
- 加锁期间业务停滞:如果在主库上备份,备份期间不能执行更新,业务基本停摆。
- 从库备份导致主从延迟:如果在从库上备份,备份期间从库不能执行主库同步过来的二进制日志(binlog),会导致主从延迟。
在InnoDB引擎中,我们可以在备份时加上参数 --single-transaction 参数来完成不加锁的一致性数据备份。
sql
mysqldump --single-transaction -uroot -p123456 itcast > itcast.sql
三、表级锁
表级锁每次操作锁住整张表,锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在 MyISAM、InnoDB、BDB 等存储引擎中。
表级锁主要分为以下三类:
- 表锁
- 元数据锁(Meta Data Lock,MDL)
- 意向锁
1.表锁
对于表锁,分为两类:
- 表共享读锁(read lock)
- 表独占写锁(write lock)
语法:
- 加锁:lock tables 表名... read/write。
- 释放锁:unlock tables / 客户端断开连接。
- 加读锁后,当前客户端和其他客户端都可以读,但都不能写。
- 加写锁后,当前客户端可以读写,其他客户端不能读写。
读锁不会阻塞其他客户端的读,但会阻塞写;写锁既会阻塞其他客户端的读,也会阻塞写。


2.元数据锁(meta data lock,MDL)
MDL 加锁过程由系统自动控制,无需显式使用,在访问一张表时会自动加上。MDL 锁的主要作用是维护表元数据的数据一致性,在表上有活动事务时,不允许对元数据进行写入操作,从而避免 DML 与 DDL 冲突,保证读写的正确性。MySQL 5.5 中引入了 MDL。规则如下:
- 对一张表进行增删改查时,加 MDL 读锁(共享)。
- 对表结构进行变更操作时,加 MDL 写锁(排他)。
| 操作类型 | 对应SQL示例 | MDL锁类型 | 说明 |
|---|---|---|---|
| 查询(DQL) | SELECT ... | SHARED_READ | 允许其他事务同时读,但阻止DDL 修改表结构 |
| 加共享锁读 | SELECT ... LOCK IN SHARE MODE | SHARED_READ | 同上,显式加共享锁 |
| 数据修改(DML) | INSERT、UPDATE、DELETE、SELECT ... FOR UPDATE | SHARED_WRITE | 允许其他事务的读和写(行锁粒度控制),但同样阻止 DDL |
| 表结构变更(DDL) | ALTER TABLE、DROP TABLE、CREATE INDEX 等 | EXCLUSIVE | 排他锁,阻塞所有其他对该表的读写操作 |
SELECT ... FOR UPDATE是对数据进行查询,顺便给这些行加锁
SHARED_READ 和 SHARED_WRITE 之间是兼容的(即可以并发执行),它们都与 EXCLUSIVE 互斥
查看元数据锁:
sql
select object_type,object_schema,object_name,lock_type,lock_duration from performance_schema.metadata_locks ;
3. 意向锁
客户端1 要更新一条数据(UPDATE),会给这一行加上行锁。随后客户端2 要给表加表锁,此时需要一行一行判断每一行是否有行锁,效率很低。
对于上面的问题,InnoDB 引入了意向锁,意向锁是 InnoDB 在加行锁之前自动给表加的一种标志锁。,使得表锁不用检查每行数据是否加锁,而是通过意向锁快速判断。意向锁用来减少表锁与行锁的冲突检查开销。
- 意向共享锁(IS):由 SELECT ... LOCK IN SHARE MODE 添加。
- 意向排他锁(IX):由 INSERT、UPDATE、DELETE、SELECT ... FOR UPDATE 添加。
兼容性:
- 意向共享锁(IS):与表锁共享锁(READ)兼容,与表锁排他锁(WRITE)互斥。
- 意向排他锁(IX):与表锁共享锁(READ)和表锁排他锁(WRITE)都互斥。
- 意向锁本身不阻塞任何事务(除了与表锁的冲突),且意向锁之间不会互斥。
普通的 SELECT(不加 LOCK IN SHARE MODE 或 FOR UPDATE)不会加任何意向锁,因为它是快照读,不需要行锁。
加锁后客户端1 更新数据时,会给数据行加行锁,同时给表添加意向锁。客户端2 尝试加表锁时,会先检查意向锁的兼容性:如果兼容则加锁,否则阻塞。
可以通过以下SQL,查看意向锁及行锁的加锁情况:
sql
SELECT object_schema, object_name, index_name, lock_type, lock_mode, lock_data
FROM performance_schema.data_locks;
四、行级锁
行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB存储引擎中。
InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。对于行级锁,主要分为以下三类:
- 行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行update和delete。在RC、RR隔离级别下都支持。
- 间隙锁(Gap Lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR隔离级别下都支持。
- 临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。在RR隔离级别下支持。
1.行锁
InnoDB实现了以下两种类型的行锁:
- 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁。
- 排它锁(X):允许获取排它锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排它锁。
| 当前锁类型 \ 请求锁类型 | S(共享锁) | X(排它锁) |
|---|---|---|
| S(共享锁) | 兼容 | 冲突 |
| X(排它锁) | 冲突 | 冲突 |
| SQL | 行锁类型 | 说明 |
|---|---|---|
| INSERT ... | 排他锁 | 自动加锁 |
| UPDATE ... | 排他锁 | 自动加锁 |
| DELETE ... | 排他锁 | 自动加锁 |
| SELECT(正常) | 不加任何锁 | |
| SELECT ... LOCK IN SHARE MODE | 共享锁 | 需要手动在SELECT之后加LOCK IN SHARE MODE |
| SELECT ... FOR UPDATE | 排他锁 | 需要手动在SELECT之后加FOR UPDATE |
默认情况下,InnoDB在 REPEATABLE READ事务隔离级别运行,InnoDB使用 next-key 锁进行搜索和索引扫描,以防止幻读。
优化为行锁针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会自动优化为行锁。假设 id 是主键(唯一索引),执行等值查询 id = 3 时,只锁住 id=3 这一行,不锁间隙。
sql
-- 客户端1
BEGIN;
SELECT * FROM stu WHERE id = 3 LOCK IN SHARE MODE; -- 只对 id=3 加行锁
sql
-- 客户端2
BEGIN;
INSERT INTO stu VALUES (2, 'Ruby', 2); -- 不阻塞,因为 id=2 不在锁范围内
UPDATE stu SET name = 'Java' WHERE id = 3; -- 阻塞,因为 id=3 已被加共享锁
没有所有会升级为表锁:InnoDB的行锁是针对于索引加的锁,不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,此时就会升级为表锁。现有表格数据如下,客户端2的 name 没有索引,第一个客户端中升级为表锁,导致客户端2阻塞,此时给name创建索引后,变成两个行锁,就互不影响了
| id | name | age |
|---|---|---|
| 1 | Java | 1 |
| 3 | Java | 3 |
| 8 | rose | 8 |
| 11 | jetty | 11 |
| 19 | lily | 19 |
| 25 | luci | 25 |
sql
-- 客户端1
BEGIN;
UPDATE stu SET name = 'Lei' WHERE name = 'lily';
-- 执行成功
sql
-- 客户端2
BEGIN;
UPDATE stu SET name = 'FHP' WHERE id = 3;
-- 阻塞!因为 name 没有索引,升级为表锁
可以通过以下SQL,查看意向锁及行锁的加锁情况:
sql
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;
2.间隙锁/临键锁
默认情况下,InnoDB在 REPEATABLE READ事务隔离级别运行,InnoDB使用 next-key 锁进行搜索和索引扫描,以防止幻读。
索引上的等值查询(唯一索引),给不存在的记录加锁时, 优化为间隙锁。对于下面的表数据来说,where查询的id = 5是不存在的记录,此时就会锁3和8之间的数据。如果不锁另一个线程添加了id=5的数据会出现幻读问题。
| id | name | age |
|---|---|---|
| 1 | Java | 1 |
| 3 | Java | 3 |
| 8 | rose | 8 |
| 11 | jetty | 11 |
| 19 | lily | 19 |
| 25 | luci | 25 |
sql
--客户端1
begin;
update stu set age = 10 where id = 5;
-- id=5 不存在,加间隙锁,锁住 (3,8) 间隙
sql
--客户端2
begin;
insert into stu values(7,'Ruby',7);
-- 阻塞,因为 7 在间隙 (3,8) 内
索引上的等值查询(普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁。表数据同样如上例所示,对 age 建立普通索引,查询 age = 3(存在该记录)。InnoDB 会锁住该记录及其前面的间隙,但向右遍历到 age=8 时不满足,因此 (3,8) 之间的间隙也会被锁。即锁(1,3)、3、(3,8),防止在3之前和之后插入age=3的数据。
PS:不用防止在1之前或者3之后插入数据,因为这里是对索引加锁,数据是顺序的,因此插入age=3一定会在3的前后
sql
-- 创建普通索引
CREATE INDEX idx_stu_age ON stu(age);
-- 客户端1
BEGIN;
SELECT * FROM stu WHERE age = 3 LOCK IN SHARE MODE; -- age=3 存在
-- 此时锁住:age=3 的行锁 + (age 最小值, 3) 的间隙 + (3, 8) 的间隙
sql
-- 客户端2
BEGIN;
INSERT INTO stu VALUES (2, 'Tom', 2); -- 阻塞,因为 2 在间隙 (最小值,3) 内
INSERT INTO stu VALUES (5, 'Jerry', 5); -- 阻塞,因为 5 在间隙 (3,8) 内
INSERT INTO stu VALUES (8, 'Bob', 8); -- 不阻塞(8 是边界值,间隙锁不包含 8 本身?实际测试不阻塞)
3.索引上的范围查询(唯一索引),会访问到不满足条件的第一个值为止。使用同样使用上面的表数据,执行 id >= 19 的范围查询。
sql
-- 客户端1
BEGIN;
SELECT * FROM stu WHERE id >= 19 LOCK IN SHARE MODE;
-- 锁住:
-- 1. id=19 的行锁
-- 2. 间隙 (19, 25)
-- 3. 间隙 (25, +∞)(下一个值不存在,锁到正无穷)
sql
-- 客户端2
BEGIN;
INSERT INTO stu VALUES (20, 'Amy', 20); -- 阻塞,20 在间隙 (19,25) 内
INSERT INTO stu VALUES (30, 'Leo', 30); -- 阻塞,30 在间隙 (25, +∞) 内
UPDATE stu SET name = 'Lily' WHERE id = 19; -- 阻塞,因为 id=19 加了行锁
注意:间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁
五、补充
1.MySQL数据库的隔离级别
| 隔离级别 | 脏读(Dirty Read) | 不可重复读(Non-Repeatable Read) | 幻读(Phantom Read) |
|---|---|---|---|
| READ UNCOMMITTED(读未提交) | 可能 | 可能 | 可能 |
| READ COMMITTED(读已提交) | 不可能 | 可能 | 可能 |
| REPEATABLE READ(可重复读,默认) | 不可能 | 不可能 | 可能(InnoDB 通过 MVCC + 间隙锁避免) |
| SERIALIZABLE(串行化) | 不可能 | 不可能 | 不可能 |
2.DDL\DML\DQL\TCL
- DDL(Data Definition Language)数据定义语言:定义、修改数据库对象的结构(如库、表、视图、索引等)。
- CREATE -- 创建数据库、表、索引等
- ALTER -- 修改表结构(添加/删除列、修改列类型等)
- DROP -- 删除数据库、表、索引等
- TRUNCATE -- 清空表数据(保留结构,不能回滚)
- RENAME -- 重命名表或列
- DML(Data Manipulation Language) 数据操作语言:对表中的数据进行增、删、改操作。
- INSERT -- 插入数据
- UPDATE -- 修改数据
- DELETE -- 删除数据
- DQL(Data Query Language)数据查询语言:查询表中的数据。通常认为主要就是 SELECT 语句。
- SELECT -- 查询数据
- TCL(Transaction Control Language)事务控制语言:管理事务的提交、回滚、保存点等。
- COMMIT -- 提交事务
- ROLLBACK -- 回滚事务
- SAVEPOINT -- 设置保存点
- START TRANSACTION 或 BEGIN -- 开始一个事务