读者你好,我是《Redis 高手心法》畅销书作者,可以叫我码哥,手拿菜刀砍电线,一路火花带闪电的靓仔。
“余弦:码哥,我今天面试被问到 “事务并发执行会带来什么问题,并发安全如何解决呢?MySQL 有哪些锁?”
今天我要跟你聊聊 MySQL 的锁。数据库锁设计的初衷是处理并发问题。
并发事务访问相同记录的情况大致可以划分以下几种:
-
读-读情况:即并发事务相继读取相同的记录。读取操作本身不会对记录有一毛钱影响,并不会引起什么问题,所以允许这种情况的发生。 -
写-写情况:即并发事务相继对相同的记录做出改动。
作为多用户共享的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则。
而锁就是用来实现这些访问规则的重要数据结构。
根据加锁的范围,MySQL 的锁可以分为全局锁、表锁和行锁。
“对于
MyISAM、MEMORY、MERGE这些存储引擎来说,它们只支持表级锁,而且这些引擎并不支持事务,所以使用这些存储引擎的锁一般都是针对当前会话来说的。
全局锁
“余弦:什么是全局锁?干嘛用的?
全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。
当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞。
-
数据更新语句(数据的增删改) -
数据定义语句(包括建表、修改表结构等) -
更新类事务的提交语句。
全局锁的适应场景之一,做全库的逻辑备份。把整个库的表数据都查出来存储为文本。
“余弦:让整库都只读,在备份期间都不能执行更新,业务基本上就得停摆。这怎么办?
是的。
-
如果你在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆; -
如果你在从库上备份,那么备份期间从库不能执行主库同步过来的 binlog,会导致主从延迟。
不加锁产生的问题
比如手机卡,购买套餐信息。这里分为两张表 u_acount (用于余额表),u_pricing (资费套餐表)。
-
u_account 表中数据 用户 A 余额:300;u_pricing 表中数据 用户 A 套餐:空 -
发起备份,备份过程中先备份 u_account 表,备份完了这个表,这个时候 u_account 用户余额是 300。 -
这个时候套用户购买了一个资费套餐 100,餐购买完成,写入到 u_print 套餐表购买成功,备份期间的数据。 -
备份完成。
可以看到备份的结果是,u_account 表中的数据没有变, u_pricing 表中的数据 已近购买了资费套餐 100.
哪这时候用这个备份文件来恢复数据的话,用户 A 赚了 100 ,用户是不是很舒服啊。但是你的想想公司利益啊。
也就是说,不加锁的话,备份系统备份的得到的库不是一个逻辑时间点,这个数据是逻辑不一致的。
表级锁
MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。
表锁的语法是 lock tables … read/write。与 FTWRL 类似,可以用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。
另一类表级的锁是 MDL(metadata lock)。MDL 不需要显式使用,在访问一个表的时候会被自动加上。
当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
-
读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。 -
读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。
不过请尽量避免在使用InnoDB存储引擎的表上使用LOCK TABLES这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。
InnoDB的厉害之处还是实现了更细粒度的行锁,关于表级别的锁大家了解一下就罢了。
“余弦:在使用
MySQL过程中,我们可以为表的某个列添加AUTO_INCREMENT属性。之后在插入记录时,可以不指定该列的值,系统会自动为它赋上递增的值。这是什么锁?
比方说我们有一个表:
CREATE TABLE t (
id INT NOT NULL AUTO_INCREMENT,
c VARCHAR(100),
PRIMARY KEY (id)
) Engine=InnoDB CHARSET=utf8;
系统实现这种自动给AUTO_INCREMENT修饰的列递增赋值的原理主要是两个:
-
采用表级别
AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,在该语句执行结束后,再把AUTO-INC锁释放掉。 -
这样一个事务在持有 AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。 -
如果我们的插入语句在执行前不可以确定具体要插入多少条记录(无法预计即将插入记录的数量),比方说使用 INSERT ... SELECT、REPLACE ... SELECT或者LOAD DATA这种插入语句,一般是使用AUTO-INC锁为AUTO_INCREMENT修饰的列生成对应的值。 -
轻量级的锁,这种方式可以避免锁定表,可以提升插入性能:
-
在为插入语句生成 AUTO_INCREMENT修饰的列的值时获取一下这个轻量级锁,然后生成本次插入语句需要用到的AUTO_INCREMENT列的值之后,就把该轻量级锁释放掉,并不需要等到整个插入语句执行完才释放锁。 -
如果我们的插入语句在执行前就可以确定具体要插入多少条记录,比方说我们上边举的关于表 t的例子中,在语句执行前就可以确定要插入 2 条记录,那么一般采用轻量级锁的方式对AUTO_INCREMENT修饰的列进行赋值。
在这里先介绍下我根据 10 年工作经验和面试经验亲手打造的面试专栏 -》《互联网大厂面试高手心法 58 讲》专栏,原价 89元, 首发 10 元,现在已经涨价到 20元,满 400 人立马涨价,不要再错过。
我向官方申请了 10 张优惠券,前 10 名还能再优惠下单,不要错过!!!
行锁
行锁,也称为记录锁,顾名思义就是在记录上加的锁。这是最复杂的锁,前面的只是开胃菜。
一个行锁玩出了各种花样,也就是把行锁分成了各种类型。
MySQL 的行级锁是 InnoDB 存储引擎实现高并发的核心技术之一。它允许在保证数据一致性的同时,大幅提升数据库的并发处理能力。
下面这张表格汇总了行级锁的主要类型和核心特点,可以帮助你快速建立整体印象。
|
|
|
|
|
|
|---|---|---|---|---|
| 记录锁 |
|
|
|
SELECT ... FOR UPDATE
|
| 间隙锁 |
|
|
|
WHERE id BETWEEN 5 AND 10)或查询不存在的唯一记录时
|
| 临键锁 |
|
记录锁 + 间隙锁
|
|
|
共享锁与排他锁
从锁的互斥性来看,行级锁分为共享锁和排他锁,它们的兼容关系是理解锁冲突的基础。
-
共享锁(Shared Lock, S Lock):也称为“读锁”。 -
作用:允许多个事务同时读取同一行数据。 -
互斥性:一个事务持有共享锁后,其他事务可以继续加共享锁来读取数据,但不能加排他锁来修改数据 -
加锁方式:使用 SELECT ... LOCK IN SHARE MODE;或SELECT ... FOR SHARE; -
排他锁(Exclusive Lock, X Lock):也称为“写锁”。 -
使用 SELECT ... FOR UPDATE;显式加锁。 -
自动加锁: UPDATE,DELETE,INSERT语句会自动对其操作的记录加排他锁。 -
作用:允许一个事务读写某行数据。 -
互斥性:一个事务持有排他锁后,其他事务不能再对该行加任何类型的锁(包括共享锁和其他排他锁),直到该锁被释放。 -
加锁方式:
它们的兼容关系可以总结为下表:
|
|
|
|
|---|---|---|
| 共享锁 (S) |
|
|
| 排他锁 (X) |
|
|
行锁的底层实现和特性
行锁基于索引实现
这是理解行锁最核心的一点。InnoDB 的行锁是加在索引项上的,而不是直接加在物理数据行上的。这意味着:
-
有效使用行锁的前提:你的 WHERE条件必须能够有效命中索引。 -
否则退化为表锁:如果查询条件不能使用索引,MySQL 将进行全表扫描,从而对所有记录加锁,实际效果等同于锁住了整个表,会严重降低并发性能
两阶段锁协议(Two-Phase Locking, 2PL)
InnoDB 遵循此协议,锁的操作分为两个阶段:
-
加锁阶段:在事务执行过程中,根据需要逐步获取锁。 -
解锁阶段:直到事务提交(COMMIT)或回滚(ROLLBACK)时,一次性释放所有在该事务中获取的锁。 -
重要提示:由于锁是在事务结束后才释放,为了减少锁冲突和提高并发性,应尽量将最可能引起冲突的写操作(如 SELECT ... FOR UPDATE)放在事务的后面执行,以缩短排他锁的持有时间。
意向锁(Intention Locks)
意向锁是表级锁,用于快速判断表内是否有被锁定的行,从而避免为了检查行锁而需要遍历每一行的低效操作。
-
意向共享锁(IS):表示事务准备在表中的某些行上加共享锁。 -
意向排他锁(IX):表示事务准备在表中的某些行上加排他锁。意向锁之间是兼容的(例如,多个事务可以同时对一个表持有 IX 锁),但它们与表级共享锁(S)和排他锁(X)存在互斥关系。意向锁由 InnoDB 自动管理,无需用户干预
行锁类型可视化
理解行锁的关键在于区分其三种基本类型。下图通过一个数据索引的例子,清晰展示了三种锁的锁定范围差异,这是理解所有高级锁概念的基础。
图解说明:
-
记录锁(Record Lock):像一把钥匙只锁一个特定的座位(如 id=10)。它确保在更新或删除某条确切存在的记录时,其他事务无法修改或删除它。 -
间隙锁(Gap Lock):锁住的是座位与座位之间的“空档”(如 (10, 15))。它的存在仅仅是为了防止插入(防止幻读),但不会阻止其他事务修改这个区间内已存在的记录(例如,如果id=12存在,你仍然可以修改它)。 -
临键锁(Next-Key Lock):这是 InnoDB 在可重复读(RR)隔离级别下默认的锁算法。它是记录锁和间隙锁的结合,锁定一个左开右闭的区间。例如, (5, 10]意味着它锁定了id=10这条记录,也锁定了5到10之间的间隙。这能有效防止在10之前插入新数据(解决幻读),同时保护10这条记录本身。
以下通过示例和场景进一步解释这三种锁。
记录锁(Record Lock)
它锁住的是索引项。例如,执行 SELECT * FROM users WHERE id = 10 FOR UPDATE;会在 id=10这个索引项上加一个排他型的记录锁,防止其他事务修改或删除这行数据。
间隙锁(Gap Lock)
它锁住的是索引项之间的“空隙”,以防止其他事务在这个空隙中插入新数据,从而解决“幻读”问题。
间隙锁只在可重复读(REPEATABLE READ)及以上隔离级别生效
-
示例:假设一张表 users的id字段有值 5, 10, 15。 -
事务 A 执行: SELECT * FROM users WHERE id BETWEEN 10 AND 15 FOR UPDATE; -
它不仅会锁住 id=10和15的记录,还会锁住它们之间的间隙(10, 15)。 -
此时事务 B 尝试执行 INSERT INTO users (id) VALUES (12);会被阻塞,因为12落在了被锁定的间隙内。
临键锁(Next-Key Lock)
它是 InnoDB 在可重复读(REPEATABLE READ)隔离级别下默认使用的锁算法。
它相当于一个 记录锁 + 间隙锁,锁定一个左开右闭的区间 (previous_index, current_index]。
-
示例:同样对于 id值为 5, 10, 15 的表。 -
事务 A 执行: SELECT * FROM users WHERE id > 10 FOR UPDATE; -
它可能锁住的区间包括 (10, 15]和(15, +∞)。 -
这既防止了在 (10, 15)区间内插入新的id=12,也防止了修改或删除id=15的现有记录,同时还防止了插入任何大于 15 的新 ID。
死锁与最佳实践
“死锁是如何产生的呢?
行级锁虽然提升了并发度,但也带来了死锁的风险。当两个或多个事务互相等待对方释放锁时,就会形成死锁。
理解 MySQL 中事务的加锁流程以及死锁如何形成,是构建高并发应用的基石。
下面我们通过一个清晰的流程图来展示一个安全的事务加锁/解锁全过程,然后深入剖析几种典型的死锁场景。
事务加锁与解锁完成流程
首先要明确一个核心概念:两阶段锁协议。它规定锁的操作分为两个阶段:
-
加锁阶段:在事务执行过程中,根据需要逐步获取锁。 -
解锁阶段:直到事务提交( COMMIT)或回滚(ROLLBACK)时,一次性释放所有在该事务中获取的锁。
下面的序列图清晰地展示了一个安全、无冲突的事务加锁与解锁流程。
流程解读:
-
加锁:事务 A 首先开启事务,然后执行一条 SELECT ... FOR UPDATE语句,意图锁定某行数据(例如行 1)进行更新。此时,它会向锁管理器申请该行的排他锁(X Lock) -
执行:成功获得锁后,事务 A 可以安全地执行修改操作。在此期间,其他事务(如事务 B)如果尝试获取同一行的排他锁或共享锁,都会被阻塞。 -
解锁:当事务 A 提交( COMMIT)后,进入解锁阶段,一次性释放它持有的所有锁。 -
后续:锁被释放后,之前被阻塞的事务 B 会被唤醒,并获得它所需要的锁,继续执行。
这个流程是理想状态下的。但当多个事务并发执行且锁的获取顺序出现环状依赖时,死锁就发生了。
典型死锁场景详解
这个流程是理想状态下的。但当多个事务并发执行且锁的获取顺序出现环状依赖时,死锁就发生了。
场景 1:共享锁升级导致的死锁
这是非常经典的死锁情况,常发生在先读后写的业务逻辑中。
|
|
|
|
|---|---|---|
|
|
SELECT * FROM accounts WHERE id=1 LOCK IN SHARE MODE;
|
|
|
|
|
SELECT * FROM accounts WHERE id=1 LOCK IN SHARE MODE;
|
|
|
UPDATE accounts SET balance = balance - 100 WHERE id=1;
|
|
|
|
|
UPDATE accounts SET balance = balance - 50 WHERE id=1;
|
死锁形成:
-
在 T3 时刻,事务 A 需要事务 B 释放 S 锁才能升级。 -
在 T4 时刻,事务 B 需要事务 A 释放 S 锁才能升级。 -
双方互相等待,形成循环等待,死锁产生。
场景 2:顺序交叉访问导致的死锁
当多个事务以不同的顺序访问和锁定资源时,极易发生死锁。
|
|
|
|
|---|---|---|
|
|
UPDATE accounts SET ... WHERE id=1;
|
|
|
|
|
UPDATE accounts SET ... WHERE id=2;
|
|
|
UPDATE accounts SET ... WHERE id=2;
|
|
|
|
|
UPDATE accounts SET ... WHERE id=1;
|
死锁形成:
-
事务 A 在等待事务 B 释放 id=2 的锁。 -
事务 B 在等待事务 A 释放 id=1 的锁。 -
循环等待再次形成,死锁发生。
场景 3:Gap 锁冲突导致的死锁
在可重复读(REPEATABLE READ)隔离级别下,MySQL 会使用间隙锁(Gap Lock)来防止幻读,这也可能引发更复杂的死锁。
假设 accounts表 id 有 1, 5, 10 三个值,存在间隙 (1,5), (5,10)。
|
|
|
|
|---|---|---|
|
|
SELECT * FROM accounts WHERE id=3 FOR UPDATE;
|
|
|
|
|
SELECT * FROM accounts WHERE id=4 FOR UPDATE;
|
|
|
INSERT INTO accounts (id, ...) VALUES (3, ...);
|
|
|
|
|
INSERT INTO accounts (id, ...) VALUES (4, ...);
|
死锁形成:
-
事务 A 的插入在等待事务 B 释放(1,5)间隙上的锁。 -
事务 B 的插入在等待事务 A 释放(1,5)间隙上的锁。 -
循环等待形成,死锁发生。
死锁的处理与预防
nnoDB 存储引擎内置了死锁检测机制。当检测到死锁时,它会选择一个回滚代价较小的事务(通常是影响行数较少的事务)进行回滚,并让另一个事务继续执行。
被回滚的事务会收到 ERROR 1213 (40001): Deadlock found错误。
核心预防策略
-
固定访问顺序:在应用设计中,保证所有事务以相同的顺序访问数据行。例如,约定更新账户时总是按 id 从小到大处理,可以避免场景二的死锁 -
避免在事务中加锁:如果业务允许,使用乐观锁(如版本号机制)替代悲观锁,从根本上减少锁竞争 -
保持事务小巧且快速:大事务会长时间持有锁,增加死锁概率。应将大事务拆分为小事务 -
为查询创建合适的索引:如果查询条件未使用索引,InnoDB 可能会锁住更多记录(甚至全表),大幅增加死锁风险。 -
使用较低的隔离级别:如果业务能接受,将隔离级别设为读已提交(READ COMMITTED),此级别下 InnoDB 不会使用间隙锁(Gap Lock),可减少死锁。
事务的加锁解锁遵循“两阶段锁协议”,提交或回滚时释放所有锁。死锁的本质是事务间形成了对锁资源的循环等待。
通过理解其原理并采用固定的资源访问顺序、使用乐观锁、减小事务粒度等预防措施,可以显著降低死锁发生概率。
希望这些图解和说明能帮助你更深入地理解 MySQL 的锁机制。
我的畅销书介绍
最后介绍我人生的第一本书《Redis 高手心法》本书基于 Redis 7.0 版本,将复杂的概念与实际案例相结合,以简洁、诙谐、幽默的方式揭示了Redis的精髓。
本书完美契合你对一个具体技术学习的期望: Redis 核心原理、关键细节、应用场景以及如何取舍......
从 Redis 的第一人称视角出发,拟人故事化方式和诙谐幽默的言语与各路“神仙”对话,配合 158 张图,由浅入深循序渐进的讲解 Redis 的数据结构实现原理、开发技巧、运维技术和高阶使用,让人轻松愉快地学习。
如下图所示,上市后得到了许多读者的较好口碑评价,而且上过京东榜单!其中还有一些业界大佬、公司 CTO 的推荐。
往期推荐
面试必问:SQL性能调优如何精准定位并解决问题?
人生不过3万天,一辈子不长,别想太多
2w 字 MySQL面试真题(图文并茂版):事务、索引、锁、主从同步、日志、性能优化、分库分表
38 张图详解 Redis:核心架构、发布订阅机制、9大数据类型底层原理、RDB和AOF 持久化、高可用架构、性能问题排查和调优
京东面试官揪着问的 InnoDB如何用MVCC和Next-Key Lock实现RR隔离?看完顿悟!
500万并发不崩溃?InnoDB 内存架构的秘密曝光!

