Mysql的锁
从粒度上分:全局锁、表锁和行锁
全局锁
全局锁:是针对整个数据库的锁,最常用的是读锁和写锁。
**读锁(共享锁)😗*它阻止其他用户更新数据,但是允许他们读取数据,这在一段时间内程序保持数据一致性很有用。
**写锁(排他锁)😗*它阻止其他用户更新数据和添加读锁。这在你需要大批量修改数据库数据,但是不希望其他的用户这段时间内来干扰比较有用。
**读锁加锁:**flush tables with read lock (简称:FTWRL) 来添加读锁。这将阻止其他线程的更新操作。
**读锁解锁:**unlock tables
InnoDB引擎:MVCC解决并发读写的问题:
mysqldump --single-transaction -u[用户名] -p[密码] [表名] > [文件名] # 不需要加全局锁也可以保证数据一致性.
# 查询锁定情况
select * from `performance_schema`.data_locks;
全局锁的开销是非常大的,因为它会组织所有的数据库修改操作,并且在高并发应用中,会使很多线程阻塞。
避免在生产环境使用全局锁或尽量减少全局锁的持有时间。
**使用场景:**进行一些需要确保整个数据库一致性的操作时。
例如:
- 全局导出
- 全局备份
- 主从复制等
表锁
表锁: 数据库粒度适中的一种锁,也是mysql中 最基本的锁策略,是mysql 最早采用的锁策略.
MyISAM 只支持表锁
**特点: **
- 开销小,
- 加锁快
- 不会死锁
- 锁冲突的概率最高,并发读最低.
总类:
- 读锁/表共享读锁(table read lock)/ S锁: 表读锁,允许所有事务对加锁的表进行读取操作,但是只有加锁的事务可以进行写操作,读锁之间不会互相阻塞.
- 写锁/表排他写锁(table write lock)/X锁: 表写锁,只有加锁的事务可以进行共享锁添加操作和写操作,其他事务只能阻塞或者普通读取
- 写锁会阻塞其他所有的所 包括读锁和写锁.
mysql
MyISAM引擎表的读操作,会自动加 读锁,对于MyISAM表的写操作会自动加 写锁.因为MyISAM 没有事务.
InnoDB引擎在必要情况下会使用表锁,但默认情况下还是使用 行锁来实行多版本并发版本控制(MVCC),行锁可以提供更好的并发性能和更少的锁冲突.
**总结: **表锁适用于读操作比较多,写操作比较少的应用.当并发竞争并不是特别激烈,以及 记录级锁并发控制开销大于访问冲突开销的情况.
使用场景:
- 读密集应用
- 写操作不频繁应用
- 数据量不大的简单应用.
- 全表更新或者删除.
**触发命令:**mysql那些命令会触发表锁呢?
-
alter table # 用于修改表结构,执行这个命令时会添加表锁 -
drop table / truncate table # 删除表/清空表的时候 -
lock tables # 锁定表的时候,这个命令会显式的给指定的一个或者多个表加上表锁 例如: lock tables t1 write,t2 read; # 给t1加表写锁 t2加表读锁 -
**全表扫描或者大范围扫描:**MyISAM引擎会触发表锁
-
flush tables with read lock # 给数据库中的所有表加上全局读锁.
风险点:
- **性能下降:**表锁锁定整张表,所以在高并发的应用中,锁定整张表的操作会导致大量的请求阻塞,从而降低性能.对于独写密集的应用,表锁会是性能瓶颈.
- 并发性能差:
- 可能导致锁等待和超时:
- **写操作影响太大:**执行写操作时,其他应用读都做不到.
- **死锁的风险:**表锁本身不会出现死锁,但是在多表操作中,如果不按照一定顺序获取锁,就会出现死锁.
页锁
MySQL的页锁(Page-Level Locking)是介于表锁和行锁之间的锁定机制,以数据页(通常为16KB)为最小粒度进行加锁。其核心特性与应用场景如下:
🔒 一、页锁的核心特性
- 锁定粒度
- 每次锁定整个数据页(含多条记录),而非单行或整表。
- 页大小通常为16KB(MySQL默认),页内数据物理连续。
- 性能开销
- 加锁速度快于行锁但慢于表锁,死锁概率介于表锁与行锁之间26。
- 锁冲突概率低于表锁但高于行锁(因同页内不同行的并发修改可能互斥)。
- 死锁风险
- 可能出现死锁(如事务A锁页1后请求页2,事务B反之)
⚙️ 二、支持引擎与应用场景
适用存储引擎
- BDB引擎:默认支持页锁(也兼容表锁)。
- InnoDB:早期版本支持页锁,现代版本(5.5+)以行锁为主,页锁仅内部用于特殊操作
典型使用场景
- 批量更新连续数据:如按主键范围更新某页内的多行记录
- 空间受限场景:需平衡锁粒度与内存开销时(已少见)
页锁是MySQL在特定历史阶段(BDB引擎)的折中方案,随着行锁成为主流(InnoDB),其实际价值已大幅降低。理解页锁机制有助于深入掌握数据库锁的演进逻辑,但在现代架构中应优先使用行锁优化并
行锁
行锁:对于表中的单独一行进行锁定,InnoDB默认的锁策略,相比与表锁和页锁,是更细粒度的锁,拥有更高的并发性能和更少的锁冲突,所以需要更多的CPU资源和内存
分类:
- 共享锁(行读锁):S锁 任何事务都可以读取被加了共享锁的行,但是只有加了共享锁的事务才能进行写操作.
- 排他锁(行写锁):X锁 除了加了排他锁的事务,其他事务都不能对这一行进行写操作和共享锁的添加. 并不是不能进行读取操作,正常的读取是没有问题的,但是如果想要加上
lock in share mode共享锁的操作会被阻塞,然后insert update delete都会添加排他锁,所以也会被阻塞。 - **间隙锁特性:**间隙锁不仅锁定当前行,还会锁定一个这个行前后的间隙,即这一行之前的行和之后的行的空间,间隙锁会阻止新事务插入内容到锁定行的前后,从而解决一些并发问题.
**注:**行级锁只在 事务中生效,即一个事务begin 到 事务 commit 或者 rollback 之间,才能对数据进行锁定,在非事务环境在语句执行完毕 会立即释放行锁.
使用场景:
- 高并发独写操作:允许多个事务操作不同的行
- 单行操作: 对于需要操作单行的sql 语句,行级锁可以提供较好的性能和并发性.
- 短期锁: 对数据进行短期锁定,行级锁可以防止长时间阻塞事务.
- 实现并发控制
- 复杂的事务处理.
触发命令:
# 1.
select .... for update # 会对选定的行添加排他锁,其他事务既不能修改这写行,也不能添加共享锁
# 2.
select ... lock in share mode # 会对选定的行添加共享锁 其他事务不能修改这些行,但是可以添加共享锁
# 3.
insert ... # 排他锁
# 4.
update ... # 排他锁
# 5.
delete ... # 排他锁
# 6. 普通的select 语句不会加锁。
**注:**锁定范围取决于 where子句 中用到的索引,如果使用了主键索引匹配到了那就会对单行添加行锁,但是如果没有用到唯一索引,那么就会可能锁定更多的行甚至整张表,引起锁冲突和性能问题
问题:
- 锁升级: 如果一个事务试图锁定很多个行,InnoDB可能升级锁为表锁
- **锁等待:**如果又大量事务都在等待一些被锁定的行,会造成性能瓶颈.
- 死锁:当两个事务互相等待对方释放行资源的时候,就会发生死锁.
- 资源消耗:比其他的锁更消耗cpu资源和内存.
- 调试和调查问题的成本上升:因为行锁锁定资源较小,所以出现问题,会比较难以排查
- 事务隔离级别: 不同的事务隔离级别,会影响所得性能和行为.
意向锁
意向锁是表锁,是为了协调行锁和表锁的关系,支持多粒度锁并存.
作用:
事务A 对于表中的某一行添加了行锁,那么会自动为这个表添加一个意向锁,然后事务B想要对整个表添加表写锁的时候,不需要遍历每一行是否存在行锁,只需要知道这个表是否存在意向锁就可以,增加了性能.
意向锁是如何支持表锁、行锁并存的
- 首先明确一点:并存的概念是指数据库同时支持表锁和行锁,而不是任何情况下都支持一个表中同时有一个事务A 持有行锁,一个事务B持有表锁,因为一个表如果已经上了任意一个级别的写锁,那么肯定是不能上其他任意级别的锁(写锁、读锁)了
- 如果事务A对于一个表上了行锁,其他事务肯定不能修改这一行了,与“事务B持有这个表的表写锁就能修改任意的一行”相冲突。所以,没有意向锁的情况下,加锁的判断条件就比较复杂。意向锁出现之后,加锁的条件判断就比较简单,提升了性能。
根据 操作的行锁类别 意向锁分为:
- 意向共享锁(IS):行锁为共享锁时添加,并不影响其他事务添加表读锁。
- 意向排他锁(IX):行锁为排他锁时添加,其他事务加不了任何表锁和全局锁。
临键锁和记录锁
临键锁(next-key)可以理解是一种特殊的算法,可以解决幻读的问题。每个数据行的非唯一索引列都会存在一把临键锁(也就是说一行可能存在多个临键锁),当事务持有该数据行的临键锁的时候,会锁定一个 左开右闭区间的数据。
临键锁的组成:行锁(记录锁)+间隙锁构成。
**注:**InnoDB的行锁是基于索引实现的,临键锁只与非唯一索引有关,唯一索引列上面不会存在临键锁。
| id(主键) | age | name |
|---|---|---|
| 1 | 10 | 张三 |
| 2 | 24 | 李四 |
| 3 | 36 | 王五 |
| 4 | 48 | 赵六 |
age列 潜在的 临键锁:
- (-∞,10]
- (10,24]
- (24,36]
- (36,48]
- (48,+∞]
测试:
start transaction;
select * from user where age = 24 for update; # 将age=24的行上面添加行写锁
commit;
# 另一个页面
insert into user values (100,26,'里斯');
insert into user values (100,15,'里斯');
# 执行都会阻塞。
# 原因 24-36 之间会有临键锁,间隙被锁定了,无法进行插入操作。并且加锁情况可能超乎我们的想象
# 原本以为 就添加了一个age = 24的行写锁,但是发现是添加了很多个:
表名 锁类型 锁记录
user IX
user X supremum pseudo-record # 最大值
user X 2 # id=2
user X 3
user X 4
user X 101
user X 100
user X,GAP 1 # id=1 的间隙锁
# 如果使用的语句是:
start TRANSACTION;
select * from `user` where id = 1 for update;
commit;
# 会发现加锁情况为:
user IX
user X,REC_NOT_GAP 1 # 非间隙锁。
# 证明间隙锁和唯一索引无关。
**记录锁:**就是行写锁 使用 select … for update来添加。
临键锁的使用场景:
-
**防止幻读:**临键锁可以防止锁定范围内插入新的数据,从而避免幻读。
-
范围查询和修改: 在执行范围查询和修改操作时,临键锁可以确保数据一致性。
-
比如:当你在一个事务中执行了以下操作:
select * from Orders where orderId between 100 and 200 for update;
此时 100到200 之间的所有记录都加上了行锁,然后对这个范围内的间隙加上间隙锁,就构成了临键锁,这种 范围查询 哪怕条件orderId是唯一索引,也是会构成临键锁的。因为间隙锁是不会关注是否是唯一索引的。如果是等于操作的话,没有范围,唯一索引不会触发间隙锁,那么就构不成临键锁了。
-
**注:**临键锁只在 可重复读 和 串行化的事务隔离级别下才能使用,读未提交和读已提交下,InnoDB不会使用临键锁。
缺点:
- 性能问题:临键锁不允许其他事务对当前事务的间隙进行更新操作,会一定程度的影响并发性能。
- 过度锁定:临键锁锁定的行比实际需要的行要多。可能会影响其他事务。
- 复杂性
- 死锁风险
间隙锁
间隙锁是 MySql InnoDB的一种锁定机制。锁定的内容不是具体的行,而是两个索引(区间)的间隙,防止新的记录被插入到该间隙,确保了数据的一致性和事务的隔离性。然后结合记录锁组成 临键锁 就可以防止区间内的数据被修改/删除/新增了。
类型:
-
区间-区间 间隙锁:锁定两个索引键之间的间隙,或者是第一个索引之前的间隙。
-
区间-记录 间隙锁:锁定一个索引键(可能对着多行)和一个记录行(确定的行)之间的间隙。
-
记录-区间 间隙锁:锁定一个记录行和一个索引键之间的间隙。
-
例如:
age= 100 and id = 10, age = 100 可能有很多个行 id = 10 是确定的行,间隙锁就会锁定 这么多行组合的一个区间内的所有间隙。
**作用:**解决幻读。
**注:**因为间隙锁会锁定范围,如果并发事务较多,并且设计的数据范围有交集,可能会出现性能问题,甚至引发死锁。
举例:
start transaction;
select * from `user` where id > 1 and id < 100 for update;
commit
| id | age | name |
|---|---|---|
| 2 | 24 | 李四 |
| 3 | 36 | 王五 |
| 4 | 48 | 赵六 |
加锁情况
lock_mode lock_type table_name loca_date
IX TABLE user
X,GAP RECORD user 100 # GAP 间隙锁
X RECORD user 2
X RECORD user 3
X RECORD user 4
为什么 记录100 加了间隙锁?
因为mysql的加锁机制:
在 RR的隔离级别下,Mysql的加锁最小单位是 临键锁(next-key lock),锁定 左开右闭区间。但是会有一个锁退化机制,临键锁是会退化为间隙锁的
具体到查询条件id>1 and id<100,它首先会定位到第一个大于1的记录(即id=2),然后扫描直到第一个不满足id<100的记录(即id=100,注意:虽然id=100不在范围内,但是扫描会到id=100为止,因为id=100是第一个大于等于100的值,而条件要求小于100,所以扫描到id=100就停止)。
因此,加锁过程如下:
- 第一个记录是id=2,对id=2加行锁(next-key lock本来是锁定(1,2]的,但是因为id=2是记录,所以实际上next-key lock会被拆分成间隙锁(1,2)和记录锁2)。
同时可以看到下边界的间隙锁已经被合并到行锁中了:在第一个扫描到的记录id=2时,已经加了一个next-key lock(即(1,2]),这个next-key lock包括了对间隙(1,2)的锁和对记录id=2的行锁。 - 然后继续向后扫描,直到id=100(因为id=100不满足条件id<100,所以停止扫描)。
- 扫描过程中,遇到了id=3和4,同样加行锁(记录锁)。
- 在扫描到id=100时,由于id=100不满足条件,根据原则,会对id=100之前的间隙(即(4,100))加间隙锁(即next-key lock退化为间隙锁,因为id=100不满足条件,所以锁定的区间是(4,100))
汇总:在RR隔离级别下,范围查询 id > 1 AND id < 100 的间隙锁机制中,下边界(id=1附近)实际已被锁定,但未显式标注为独立间隙锁记录的原因如下:
- 下边界间隙锁被合并到行锁中
- 扫描过程首先定位到第一个大于1的记录(id=2),此时会对其加 next-key lock 即(1, 2]区间锁)。该锁包含两部分:
- **记录锁(行锁)**:锁定
id=2的行。 - 间隙锁:隐式锁定
id=1到id=2的间隙(1, 2)。
- **记录锁(行锁)**:锁定
- 因此,间隙
(1, 2)的锁已涵盖在id=2的next-key lock中,无需单独显示为间隙锁记录。
- 上边界间隙锁需独立标注
- 扫描终止于第一个不满足条件的值(id=100)此时:
- 间隙锁独立生效:
id=100前的区间(4, 100)无实际记录,需显式加纯间隙锁(X,GAP),锁定4到100之间的空隙。 - 退化机制:因
id=100不满足查询条件(id<100),next-key lock退化为间隙锁,故独立标注在id=100上。
- 间隙锁独立生效:
- 锁信息显示的差异
- 下边界间隙锁(
(1, 2))与id=2的行锁合并为next-key lock,锁模式显示为X(含间隙锁成分)。 - 上边界间隙锁(
(4, 100))因无实际记录可依附,需显式标注在id=100上,锁模式为X,GAP。
总结:
下边界间隙锁((1, 2))已通过id=2的next-key lock实现锁定,而上边界((4, 100))因无记录支撑需独立标注。这是由InnoDB的加锁规则决定的:
- 扫描到的记录(如
id=2,3,4)会触发next-key lock(合并行锁+间隙锁)。 - 终止点(
id=100)触发纯间隙锁独立显示。
8836

被折叠的 条评论
为什么被折叠?



