MySQL锁机制:从全局锁到行锁详解

Mysql的锁

从粒度上分:全局锁、表锁和行锁

全局锁

全局锁:是针对整个数据库的锁,最常用的是读锁写锁

**读锁(共享锁)😗*它阻止其他用户更新数据,但是允许他们读取数据,这在一段时间内程序保持数据一致性很有用。

**写锁(排他锁)😗*它阻止其他用户更新数据和添加读锁。这在你需要大批量修改数据库数据,但是不希望其他的用户这段时间内来干扰比较有用。

**读锁加锁:**flush tables with read lock (简称:FTWRL) 来添加读锁。这将阻止其他线程的更新操作。

**读锁解锁:**unlock tables

InnoDB引擎:MVCC解决并发读写的问题

mysqldump --single-transaction -u[用户名] -p[密码] [表名] > [文件名] # 不需要加全局锁也可以保证数据一致性.
# 查询锁定情况
select * from `performance_schema`.data_locks;

全局锁的开销是非常大的,因为它会组织所有的数据库修改操作,并且在高并发应用中,会使很多线程阻塞。
避免在生产环境使用全局锁或尽量减少全局锁的持有时间。

**使用场景:**进行一些需要确保整个数据库一致性的操作时。
例如:

  1. 全局导出
  2. 全局备份
  3. 主从复制等

表锁

表锁: 数据库粒度适中的一种锁,也是mysql中 最基本的锁策略,是mysql 最早采用的锁策略.

MyISAM 只支持表锁

**特点: **

  1. 开销小,
  2. 加锁快
  3. 不会死锁
  4. 锁冲突的概率最高,并发读最低.

总类:

  1. 读锁/表共享读锁(table read lock)/ S锁: 表读锁,允许所有事务对加锁的表进行读取操作,但是只有加锁的事务可以进行写操作,读锁之间不会互相阻塞.
  2. 写锁/表排他写锁(table write lock)/X锁: 表写锁,只有加锁的事务可以进行共享锁添加操作和写操作,其他事务只能阻塞或者普通读取
  3. 写锁会阻塞其他所有的所 包括读锁和写锁.

mysql

MyISAM引擎表的读操作,会自动加 读锁,对于MyISAM表的写操作会自动加 写锁.因为MyISAM 没有事务.

InnoDB引擎在必要情况下会使用表锁,但默认情况下还是使用 行锁来实行多版本并发版本控制(MVCC),行锁可以提供更好的并发性能和更少的锁冲突.

**总结: **表锁适用于读操作比较多,写操作比较少的应用.当并发竞争并不是特别激烈,以及 记录级锁并发控制开销大于访问冲突开销的情况.

使用场景:

  1. 读密集应用
  2. 写操作不频繁应用
  3. 数据量不大的简单应用.
  4. 全表更新或者删除.

**触发命令:**mysql那些命令会触发表锁呢?

  1. alter table # 用于修改表结构,执行这个命令时会添加表锁
    
  2. drop table / truncate table # 删除表/清空表的时候
    
  3. lock tables # 锁定表的时候,这个命令会显式的给指定的一个或者多个表加上表锁 例如:
    lock tables t1 write,t2 read; # 给t1加表写锁 t2加表读锁
    
  4. **全表扫描或者大范围扫描:**MyISAM引擎会触发表锁

  5. flush tables with read lock # 给数据库中的所有表加上全局读锁.
    

风险点:

  1. **性能下降:**表锁锁定整张表,所以在高并发的应用中,锁定整张表的操作会导致大量的请求阻塞,从而降低性能.对于独写密集的应用,表锁会是性能瓶颈.
  2. 并发性能差:
  3. 可能导致锁等待和超时:
  4. **写操作影响太大:**执行写操作时,其他应用读都做不到.
  5. **死锁的风险:**表锁本身不会出现死锁,但是在多表操作中,如果不按照一定顺序获取锁,就会出现死锁.

页锁

MySQL的页锁(Page-Level Locking)是介于表锁和行锁之间的锁定机制,以数据页(通常为16KB)为最小粒度进行加锁。其核心特性与应用场景如下:

🔒 ‌一、页锁的核心特性
  1. 锁定粒度
    • 每次锁定‌整个数据页‌(含多条记录),而非单行或整表。
    • 页大小通常为16KB(MySQL默认),页内数据物理连续。
  2. 性能开销
    • 加锁速度‌快于行锁但慢于表锁‌,死锁概率‌介于表锁与行锁之间‌26。
    • 锁冲突概率低于表锁但高于行锁(因同页内不同行的并发修改可能互斥)。
  3. 死锁风险
    • 可能出现死锁(如事务A锁页1后请求页2,事务B反之)
⚙️ ‌二、支持引擎与应用场景

适用存储引擎

  • BDB引擎‌:默认支持页锁(也兼容表锁)。
  • InnoDB‌:早期版本支持页锁,现代版本(5.5+)‌以行锁为主‌,页锁仅内部用于特殊操作

典型使用场景

  • 批量更新连续数据‌:如按主键范围更新某页内的多行记录
  • 空间受限场景‌:需平衡锁粒度与内存开销时(已少见)

页锁是MySQL在特定历史阶段(BDB引擎)的折中方案,随着行锁成为主流(InnoDB),其实际价值已大幅降低。理解页锁机制有助于深入掌握数据库锁的演进逻辑,但在现代架构中‌应优先使用行锁优化并

行锁

行锁:对于表中的单独一行进行锁定,InnoDB默认的锁策略,相比与表锁和页锁,是更细粒度的锁,拥有更高的并发性能和更少的锁冲突,所以需要更多的CPU资源和内存

分类:

  1. 共享锁(行读锁):S锁 任何事务都可以读取被加了共享锁的行,但是只有加了共享锁的事务才能进行写操作.
  2. 排他锁(行写锁):X锁 除了加了排他锁的事务,其他事务都不能对这一行进行写操作和共享锁的添加. 并不是不能进行读取操作,正常的读取是没有问题的,但是如果想要加上lock in share mode共享锁的操作会被阻塞,然后 insert update delete都会添加排他锁,所以也会被阻塞。
  3. **间隙锁特性:**间隙锁不仅锁定当前行,还会锁定一个这个行前后的间隙,即这一行之前的行和之后的行的空间,间隙锁会阻止新事务插入内容到锁定行的前后,从而解决一些并发问题.

**注:**行级锁只在 事务中生效,即一个事务begin 到 事务 commit 或者 rollback 之间,才能对数据进行锁定,在非事务环境在语句执行完毕 会立即释放行锁.

使用场景:

  1. 高并发独写操作:允许多个事务操作不同的行
  2. 单行操作: 对于需要操作单行的sql 语句,行级锁可以提供较好的性能和并发性.
  3. 短期锁: 对数据进行短期锁定,行级锁可以防止长时间阻塞事务.
  4. 实现并发控制
  5. 复杂的事务处理.

触发命令:

# 1. 
select .... for update # 会对选定的行添加排他锁,其他事务既不能修改这写行,也不能添加共享锁
# 2. 
select ... lock in share mode # 会对选定的行添加共享锁 其他事务不能修改这些行,但是可以添加共享锁
# 3. 
insert ... # 排他锁
# 4. 
update ... # 排他锁
# 5. 
delete ... # 排他锁
# 6. 普通的select 语句不会加锁。

**注:**锁定范围取决于 where子句 中用到的索引,如果使用了主键索引匹配到了那就会对单行添加行锁,但是如果没有用到唯一索引,那么就会可能锁定更多的行甚至整张表,引起锁冲突和性能问题

问题:

  1. 锁升级: 如果一个事务试图锁定很多个行,InnoDB可能升级锁为表锁
  2. **锁等待:**如果又大量事务都在等待一些被锁定的行,会造成性能瓶颈.
  3. 死锁:当两个事务互相等待对方释放行资源的时候,就会发生死锁.
  4. 资源消耗:比其他的锁更消耗cpu资源和内存.
  5. 调试和调查问题的成本上升:因为行锁锁定资源较小,所以出现问题,会比较难以排查
  6. 事务隔离级别: 不同的事务隔离级别,会影响所得性能和行为.

意向锁

意向锁是表锁,是为了协调行锁和表锁的关系,支持多粒度锁并存.

作用:

事务A 对于表中的某一行添加了行锁,那么会自动为这个表添加一个意向锁,然后事务B想要对整个表添加表写锁的时候,不需要遍历每一行是否存在行锁,只需要知道这个表是否存在意向锁就可以,增加了性能.

意向锁是如何支持表锁、行锁并存的

  1. 首先明确一点:并存的概念是指数据库同时支持表锁和行锁,而不是任何情况下都支持一个表中同时有一个事务A 持有行锁,一个事务B持有表锁,因为一个表如果已经上了任意一个级别的写锁,那么肯定是不能上其他任意级别的(写锁、读锁)了
  2. 如果事务A对于一个表上了行锁,其他事务肯定不能修改这一行了,与“事务B持有这个表的表写锁就能修改任意的一行”相冲突。所以,没有意向锁的情况下,加锁的判断条件就比较复杂。意向锁出现之后,加锁的条件判断就比较简单,提升了性能。

根据 操作的行锁类别 意向锁分为:

  1. 意向共享锁(IS):行锁为共享锁时添加,并不影响其他事务添加表读锁。
  2. 意向排他锁(IX):行锁为排他锁时添加,其他事务加不了任何表锁和全局锁。

临键锁和记录锁

临键锁(next-key)可以理解是一种特殊的算法,可以解决幻读的问题。每个数据行的非唯一索引列都会存在一把临键锁(也就是说一行可能存在多个临键锁),当事务持有该数据行的临键锁的时候,会锁定一个 左开右闭区间的数据。

临键锁的组成:行锁(记录锁)+间隙锁构成。

**注:**InnoDB的行锁是基于索引实现的,临键锁只与非唯一索引有关,唯一索引列上面不会存在临键锁。

id(主键)agename
110张三
224李四
336王五
448赵六

age列 潜在的 临键锁:

  1. (-∞,10]
  2. (10,24]
  3. (24,36]
  4. (36,48]
  5. (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来添加。

临键锁的使用场景:

  1. **防止幻读:**临键锁可以防止锁定范围内插入新的数据,从而避免幻读。

  2. 范围查询和修改: 在执行范围查询和修改操作时,临键锁可以确保数据一致性。

    1. 比如:当你在一个事务中执行了以下操作:

      select * from Orders where orderId between 100 and 200 for update; 
      

    此时 100到200 之间的所有记录都加上了行锁,然后对这个范围内的间隙加上间隙锁,就构成了临键锁,这种 范围查询 哪怕条件orderId是唯一索引,也是会构成临键锁的。因为间隙锁是不会关注是否是唯一索引的。如果是等于操作的话,没有范围,唯一索引不会触发间隙锁,那么就构不成临键锁了。

**注:**临键锁只在 可重复读 和 串行化的事务隔离级别下才能使用,读未提交和读已提交下,InnoDB不会使用临键锁。

缺点:

  1. 性能问题:临键锁不允许其他事务对当前事务的间隙进行更新操作,会一定程度的影响并发性能。
  2. 过度锁定:临键锁锁定的行比实际需要的行要多。可能会影响其他事务。
  3. 复杂性
  4. 死锁风险

间隙锁

间隙锁是 MySql InnoDB的一种锁定机制。锁定的内容不是具体的行,而是两个索引(区间)的间隙,防止新的记录被插入到该间隙,确保了数据的一致性和事务的隔离性。然后结合记录锁组成 临键锁 就可以防止区间内的数据被修改/删除/新增了。

类型:

  1. 区间-区间 间隙锁:锁定两个索引键之间的间隙,或者是第一个索引之前的间隙。

  2. 区间-记录 间隙锁:锁定一个索引键(可能对着多行)和一个记录行(确定的行)之间的间隙。

  3. 记录-区间 间隙锁:锁定一个记录行和一个索引键之间的间隙。

  4. 例如:

    age= 100 and id = 10, age = 100 可能有很多个行 id = 10 是确定的行,间隙锁就会锁定 这么多行组合的一个区间内的所有间隙。
    

**作用:**解决幻读。

**注:**因为间隙锁会锁定范围,如果并发事务较多,并且设计的数据范围有交集,可能会出现性能问题,甚至引发死锁。

举例:

start transaction;

select * from `user` where id > 1 and id < 100 for update;
commit
idagename
224李四
336王五
448赵六

加锁情况

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. 下边界间隙锁被合并到行锁中
  • 扫描过程首先定位到第一个大于1的记录(id=2),此时会对其加 ‌next-key lock 即(1, 2]区间锁)。该锁包含两部分:
    • ‌**记录锁(行锁)**‌:锁定id=2的行。
    • 间隙锁‌:隐式锁定id=1id=2的间隙(1, 2)
  • 因此,间隙(1, 2)的锁已涵盖在id=2next-key lock中,无需单独显示为间隙锁记录。
  1. 上边界间隙锁需独立标注
  • 扫描终止于第一个不满足条件的值(id=100)此时:
    • 间隙锁独立生效‌:id=100前的区间(4, 100)无实际记录,需显式加纯间隙锁(X,GAP),锁定4100之间的空隙。
    • 退化机制‌:因id=100不满足查询条件(id<100),next-key lock退化为间隙锁,故独立标注在id=100上。
  1. 锁信息显示的差异
  • 下边界间隙锁((1, 2))与id=2的行锁合并为next-key lock,锁模式显示为X(含间隙锁成分)。
  • 上边界间隙锁((4, 100))因无实际记录可依附,需显式标注在id=100上,锁模式为X,GAP

总结:

下边界间隙锁((1, 2))已通过id=2next-key lock实现锁定,而上边界((4, 100))因无记录支撑需独立标注。这是由InnoDB的加锁规则决定的:

  • 扫描到的记录(如id=2,3,4)会触发next-key lock(合并行锁+间隙锁)。
  • 终止点(id=100)触发纯间隙锁独立显示。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值