一、MySQL进阶
当10万用户同时抢购限量商品,库存却显示为负数——这不是技术故障,而是锁机制失效的悲剧。”
在2025年数据库故障报告中,68%的高并发系统崩溃源于锁设计缺陷。作为数据库工程师,你无法回避一个核心问题:如何在保障数据一致性的同时,让系统吞吐量飙升?
1. 锁
1.1 为何锁是数据库的“生死线”?——背景深度剖析
在电商、金融等高并发场景中,数据不一致的代价远超想象:
| 问题 | 业务影响 | 事故案例 |
|---|---|---|
| 库存超卖 | 用户支付成功但无货,导致退款率飙升 | 某电商大促期间损失1200万元 |
| 数据丢失 | 事务中断导致关键记录缺失 | 银行转账系统因锁冲突丢失5000笔交易 |
| 性能暴跌 | 阻塞式锁引发线程饥饿 | 10万QPS系统响应时间从20ms→500ms |
为什么必须掌握锁?
- 数据一致性是底线:ACID中的“一致性”(Consistency)依赖锁机制实现。
- 性能与安全的平衡点:锁粒度越小(行级),并发越高;锁粒度越大(全局),一致性越强。
- 行业铁律:高并发系统中,锁设计不当是性能瓶颈的80%根源(2025年《数据库架构白皮书》)。
💡核心认知:锁不是“性能敌人”,而是保障数据安全的“最小成本”。没有锁,数据库就是一盘散沙。
1.2 锁的核心定义:数据库的“交通规则”
锁是数据库管理系统(DBMS)控制并发访问共享资源的机制,通过加锁(Lock)和解锁(Unlock)确保事务执行的隔离性(Isolation)。
关键特性:
- 排他性:一个事务持有锁时,其他事务无法修改数据。
- 粒度:锁的范围(全局/表/行)决定并发能力。
- 模式:共享锁(Read Lock,允许多事务读) vs. 排他锁(Write Lock,独占写)。
✅与存储函数/触发器的本质区别:
- 函数/触发器:封装逻辑,不改变数据状态。
- 锁:直接控制数据访问,是并发安全的基石。
1.3 锁的全类型深度解析:从全局到行级
1. 全局锁(Global Lock)
- 定义:锁定整个数据库实例,所有表变为只读。
- 语法:
FLUSH TABLES WITH READ LOCK; -- 全库只读 UNLOCK TABLES; -- 解锁DDL:操作数据库 / 表的结构(创建、修改、删除),代表语句CREATE/ALTER/DROP,执行后自动提交;
DML:操作表中的数据(增、删、改),代表语句INSERT/UPDATE/DELETE,可手动提交 / 回滚;
补充:SELECT属于 DQL(数据查询语言),是查询数据的核心语句,常和 DML 配合使用。
- 使用场景:
- 全库逻辑备份(如
mysqldump)。 - 需要全局一致性快照的维护操作(如版本升级)。
- 全库逻辑备份(如
- 致命缺点:
阻塞所有写操作!生产环境严禁使用,会导致服务不可用。
(实际案例:某公司因误用全局锁导致1小时宕机)
2. 表级锁(Table-level Lock)
表级锁是锁定整张表的锁机制,锁定粒度大,发生锁冲突的概率最高,并发度最低。它分为三类:表锁、元数据锁、意向锁。
表锁(Table Lock)
定义:显式锁定整张表,分为共享读锁和独占写锁。注意都是对外客户端来说
-- 加锁 LOCK TABLES 表名 READ; -- 共享读锁 LOCK TABLES 表名 WRITE; -- 独占写锁 -- 释放锁 UNLOCK TABLES;| 锁类型 | 允许其他事务操作 | 阻塞其他事务操作 | 适用场景 |
|---|---|---|---|
| 共享读锁 | 读操作(SELECT) | 写操作(INSERT/UPDATE/DELETE) | 低并发报表查询 |
| 独占写锁 | 无(仅当前事务可操作) | 读和写操作 | 数据批量导入/导出 |
💡关键结论:
读锁不阻塞读,但阻塞写;写锁阻塞所有操作。这是表锁的核心特性。
元数据锁(MDL, Meta Data Lock)
定义:系统自动控制的锁,无需显式使用,在访问表时自动添加,用于维护表元数据(表结构)的一致性。
由来:MySQL 5.5引入,为避免DML(数据操作)与DDL(数据定义)冲突。
作用:当表上有活动事务时,禁止修改表结构(如ALTER TABLE)。
锁类型与触发场景:
| 锁类型 | 触发操作 | 与MDL锁的兼容性 |
|---|---|---|
| MDL读锁 | SELECT、INSERT、UPDATE、DELETE | 允许多个MDL读锁共存 |
| MDL写锁 | ALTER TABLE、DROP TABLE、CREATE INDEX | 与所有MDL锁互斥 |
典型场景:
- 当执行
SELECT * FROM orders时,自动加MDL读锁。 - 当执行
ALTER TABLE orders ADD COLUMN new_col INT时,需等待所有MDL读锁释放。
💡核心价值:
MDL锁是数据一致性与DDL操作的"守门人"。没有它,表结构变更会破坏正在执行的查询。
意向锁(Intention Locks)
定义:表级锁,表示事务打算对表中的某些行加锁,但不会直接锁定数据行本身。
由来:为了解决表锁与行锁冲突的问题(避免逐行检查行锁)。
下表线程A对表进行操作之后,对操作行3进行行锁锁定,此时会接着对这张表添加一个意向锁,线程B并发执行操作该表时,对这张表添加表锁,首先会去检查这张表是否意向锁和意向锁的类型,通过意向锁的决定能不能加锁成功,有意向锁时,此时操作会进入阻塞状态,直到线程A的意向锁行锁解锁时才进行操作。
意向锁类型:
| 锁类型 | 触发操作 | 与表锁兼容性 |
|---|---|---|
| 意向共享锁(IS) | SELECT ... LOCK IN SHARE MODE | 与表共享读锁(read)兼容,与表独占写锁(write)互斥 |
| 意向排他锁(IX) | INSERT、UPDATE、DELETE、SELECT ... FOR UPDATE | 与表共享读锁和独占写锁都互斥 |
| 意向锁之间 | 无冲突 | 意向锁之间不会互斥(IS与IX兼容) |
💡关键认知:
意向锁是InnoDB行锁与表锁的"桥梁",它让表锁检查从"逐行扫描"优化为"快速判断",极大提升性能。
3. 行级锁
行级锁是锁定表中特定行的锁机制,由InnoDB存储引擎实现(MyISAM不支持行级锁)。
关键特性:
- 粒度最小:仅锁定需要操作的行,其他行可并发操作。
- 基于索引:行级锁实际是加在索引记录上,而非数据行本身。
- 自动管理:由InnoDB自动加锁,无需显式声明(除
SELECT ... FOR UPDATE外)。
为什么是"基于索引"?
核心原理:行锁是索引锁,不是数据行锁
"InnoDB的行锁是通过索引项加锁实现的,不是直接对数据行加锁。"
这一特性决定了行锁的行为与索引设计息息相关。
关键结论:
- 必须使用索引:若查询未命中索引,InnoDB将升级为表锁(全表锁定)。
- 索引键冲突:即使访问不同行,若使用相同索引键,也会发生锁冲突。
- 执行计划影响:MySQL会根据执行计划决定是否使用索引,若全表扫描更高效,将使用表锁。
Record Lock(记录锁)
定义:记录锁是锁定索引中的一条具体记录的锁。即使表没有定义索引,InnoDB 也会创建一个隐式的聚簇索引来锁定。
作用:它是最基本的行锁,确保在事务结束前,其他事务无法修改或删除(某些情况下也无法读取,取决于隔离级别)这条被锁定的记录。
第一点: 共享锁之间是兼容的,共享锁与排他锁之间是互斥的。
第二点: 假如第一个事务获取到了某一行数据的排他锁,那么其他的事务就不能在获取这一行数据的共享以及排他锁。
对于我们一些常见的增删改查的一些操作分别加的是什么类型的行锁呢?
示例:
-- 假设 id 是主键(或唯一索引) SELECT * FROM users WHERE id = 5 FOR UPDATE;如果
id = 5这条记录存在,这条语句就会在id=5的索引记录上加一个记录锁。其他事务如果要UPDATE、DELETE或以FOR UPDATE方式读取这条记录都会被阻塞。
✅关键结论:
Record Lock是行级锁的基础,但无法解决范围查询中的幻读问题。
间隙锁(Gap Locks)
定义:间隙锁是锁定索引记录之间的间隙,即一个区间,但不包括记录本身。它锁定的是“不存在记录的范围”。
作用:防止其他事务在间隙中插入新的记录,从而解决“幻读”问题。这是实现“可重复读”隔离级别的关键。
-- 假设 age 有一个普通索引,并且存在 age=10 和 age=20 的记录 SELECT * FROM users WHERE age = 15 FOR UPDATE;由于age=15的记录不存在,这条语句会锁定(10, 20)这个开区间。此时,其他事务尝试插入age=12、age=15、age=18的记录都会被阻塞,但更新已存在的age=10的记录通常不受影响(除非该更新导致其移出此间隙)。
重要特点:
间隙锁可以跨越单个、多个索引值,甚至是无限大(如
(20, +∞))。间隙锁是“共享”的。多个事务可以在同一个间隙上持有间隙锁,目的都是为了防止插入。这不会冲突。
唯一索引(或主键)的等值查询,如果记录不存在,也会加间隙锁。
临键锁(Next-Key Locks)
定义:临键锁是记录锁和间隙锁的组合。它锁定一条索引记录以及该记录之前的间隙。它是一个左开右闭的区间
(previous_record, current_record]。作用:这是 InnoDB 在“可重复读”隔离级别下的默认行锁算法。它结合了记录锁和间隙锁的优点,既能锁定现有记录防止修改,又能锁定间隙防止插入,从而彻底避免幻读。
-- 假设 id 是主键,表中有 id=5, id=10, id=15 的记录 SELECT * FROM users WHERE id > 10 AND id <= 15 FOR UPDATE;这条语句会锁定:
记录
id=15(记录锁)以及
id=15之前的间隙(10, 15)(间隙锁)
所以,它实际锁定的是临键锁区间(10, 15]。
其他事务不能修改
id=15的记录。也不能在
(10, 15)这个区间内插入任何新的记录(如id=12)。工作方式:
当使用索引进行范围查询或等值查询时,InnoDB 会扫描并锁定所有匹配的索引记录和间隙。
对于最后一个扫描到的索引记录,其后的间隙也可能被锁定(如上例中
id=15之后的(15, +∞)部分区间是否锁定取决于具体查询和索引)。
| 特性 | 记录锁 | 间隙锁 | 临键锁 |
|---|---|---|---|
| 锁定对象 | 单条索引记录 | 记录之间的间隙(不锁记录) | 记录 + 其前的间隙(区间) |
| 主要目的 | 防止修改或删除已存在的记录 | 防止在间隙中插入新记录(防幻读) | 两者兼防,实现完整的行锁保护 |
| 锁冲突 | 与其它记录的记录锁/临键锁不冲突,与同记录的锁冲突。 | 共享的,多个事务可持有同一间隙锁。但与插入操作冲突。 | 与涉及该区间的记录锁、插入意向锁冲突。 |
| 常见触发场景 | 对唯一索引进行等值查询且记录存在。 | 等值查询记录不存在,或范围查询。 | 范围查询,或非唯一索引的等值查询(RR级别)。 |
| 与隔离级别关系 | 所有支持行锁的隔离级别(RC, RR) | 主要是 RR 隔离级别,RC级别下一般禁用间隙锁以提升并发。 | RR 隔离级别的默认算法 |