MySQL锁机制
1. MySQL锁机制
锁机制的概念:
数据库是多用户共享的系统,为了应对并发访问,MySQL 设计了多种锁机制,用于协调用户对资源的访问,避免数据不一致或冲突。
按作用范围分类的锁类型:
锁类型 | 加锁对象 | 粒度 | 特点 |
全局锁 | 整个数据库实例 | 最粗粒度 | 会阻塞所有线程的写操作 |
表级锁 | 某一张表 | 中等粒度 | 包括表锁、元数据锁 |
行级锁 | 表中的某一行 | 最细粒度 | 并发性好,开销大 |
1.1. 全局锁(Global Lock)
1. 命令用法
FLUSH TABLES WITH READ LOCK;
- 简称 FTWRL
- 当前会话持有全局读锁时,其它线程无法进行写操作或表结构变更
2. 加锁效果
- 所有表变为只读状态
- 阻塞操作:
- 所有 DML(如 INSERT、UPDATE、DELETE)
- 所有 DDL(如 ALTER、DROP)
- 事务提交(因为事务可能带有写入操作)
3. 使用场景:
全库逻辑备份
- 为了避免备份时数据不一致,需要确保在备份过程中没有其他写操作。
- 若不加锁:可能某些表的快照来自不同时间点,导致恢复后数据逻辑不一致。
举例:
mysqldump -A --lock-all-tables
此参数会自动执行 FTWRL
来确保一致性备份。
❌ 不加锁的风险
假设有两个表:
- 用户表
users
- 课程表
courses
先备份 users
,然后用户购买了课程,再备份 courses
,恢复时:
- 用户存在,但课程购买记录不存在
- 数据逻辑不一致
更优备份方案(推荐方式)
使用事务一致性备份,single-transaction 方法:
mysqldump -A --single-transaction --quick --master-data=2
前提条件:
- 所有表均为 InnoDB(支持事务)
- 使用的是 RR(可重复读)隔离级别
在备份开始时启动一个事务,依赖 MVCC(多版本并发控制) 拿到一致性快照,即使期间有更新,也不影响当前事务读到的数据。
SET GLOBAL readonly=1
与 FTWRL 对比
项目 |
|
|
加锁方式 | 设置系统变量 | 显式命令加锁 |
持久性 | 需手动恢复 | 会话断开自动释放 |
影响 | 所有客户端都无法写 | 当前会话可读,其它会话禁止写 |
场景 | 主备切换保护主库只读 | 逻辑备份等临时场景更合适 |
风险 | 客户端异常断开后仍为只读 | 自动释放风险小 |
1.2. 表级锁(Table-level Lock)
表级锁:
1. 表锁
- 定义:表锁是对整个数据库表进行加锁,限制了其他事务对该表的访问。表锁的语法是 lock tables … read/write。需要显示使用
- 作用:当一个事务需要对整个表进行操作时,可以申请表级锁,确保在操作过程中其他事务不能修改或访问该表。
- 粒度:粗粒度锁,锁定整个表,因此可能会影响到其他事务对表中其他行的操作。
- 并发性:低,并发度较差,因为其他事务需要等待锁释放才能访问整个表。
2. 元数据锁MDL(metadata lock)
- 定义:元数据锁是用于保护数据库的元数据(例如表结构、索引信息等)的锁。MDL 不需要显式使用,在对一个表做增删改查操作的时候会被自动加上。MDL 的作用是,保证读写的正确性
- 作用:当一个事务需要修改表结构等元数据信息时,会申请元数据锁,确保在修改过程中其他事务不能同时修改相同的元数据。
- 粒度:较细粒度,锁定特定的元数据对象,例如一个表的结构。
- 并发性:相对较好,因为不同事务可能修改不同的元数据对象,不会相互阻塞。
表锁:
表锁一般是在数据库引擎不支持行锁的时候才会被用到的。
使用方式:
LOCK TABLES table1 READ, table2 WRITE;
-- 操作表
UNLOCK TABLES;
锁类型:
- READ:本线程只能读,其他线程只能读,不能写
- WRITE:本线程能读写,其他线程不能读也不能写
注意:
- 当前会话只能访问被锁定的表
- 如果忘记
UNLOCK
,其他线程将被阻塞
InnoDB 中不推荐使用:因为它本身支持行锁,粒度更细并发性更好
元数据锁:
作用:
保护表的结构元数据,防止以下问题:
- 查询时结构变化 → 报错或数据错乱
- 修改结构时有读写并发 → 破坏一致性
加锁行为(自动触发):
- 读锁:任何访问表数据的操作(SELECT、UPDATE、INSERT 等)都会加上
- 写锁:任何修改表结构的操作(ALTER、DROP 等)
锁冲突规则:
当前锁类型 | 新请求锁类型 | 是否冲突 |
读锁 | 读锁 | 否 |
读锁 | 写锁 | 是 |
写锁 | 任意 | 是 |
1.3. 行锁
行锁作用范围: 行级锁作用于数据表中的行记录,是 InnoDB 的重要特性之一。
引擎支持情况:
- InnoDB:✅ 支持行锁,支持高并发。
- MyISAM:❌ 不支持行锁,只支持表级锁,容易成为并发瓶颈。
优势: 行锁粒度小,锁冲突少,适合高并发场景。
两阶段协议
两阶段协议的概念:
在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。
加锁时机: 事务在执行语句时需要资源才加锁(即时加锁)。
释放时机: 所有加的锁都等到事务提交或回滚时统一释放(延迟释放)。
设计建议:
- 如果事务需要锁多行,尽量把可能造成锁冲突的语句放在后面执行,缩短热点行的持锁时间,提升并发性能。
在线购票系统中,更新影院账户余额的语句最可能造成冲突,建议将其放在事务末尾执行。
死锁与死锁检测
死锁定义:
- 死锁是指多个事务因持有并等待对方资源而进入无限等待的状态。
举例:
事务 A 锁了记录 1,等待记录 2;
事务 B 锁了记录 2,等待记录 1;
二者形成循环依赖,系统进入死锁。
死锁处理策略:
- 等待超时退出
含义:直接进入等待,直到超时。
参数:innodb_lock_wait_timeout(默认 50 秒)
缺点:响应时间不可控,容易引发业务超时失败。
- 主动死锁检测(推荐)
含义:发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。
参数:innodb_deadlock_detect=on(默认开启)
优点:可快速发现死锁并中断其中一个事务。
成本:每次加锁时都要做图遍历判断是否存在环路,时间复杂度为 O(n),代价大。
热点行引发的性能问题与优化方案
问题现象:
多个事务同时更新同一热点行(如影院账户),即使没有真正死锁,也会因频繁做死锁检测而导致:CPU 利用率高,但实际 TPS(每秒事务数)很低。
解决方案:
1. 临时关闭死锁检测(不推荐)
- 仅适用于能明确保证业务无死锁的情况。
- 设置:
innodb_deadlock_detect=off
- 缺点:死锁不会被自动处理,会等待到超时,影响业务体验。
2. 控制并发度
- 在数据库服务端或中间件中,对热点记录的访问请求排队处理,减少并发冲突。
- 客户端控制并发不可靠,因客户端数量不可控。
3. 数据结构改造:热点行拆分为多行
- 例:将影院账户拆分为 10 个记录,每次随机更新其中一条。
- 优点:减少锁竞争,降低死锁检测的频率。
- 注意事项:若涉及金额减少或查询总额,需额外逻辑处理。