今天我要跟你聊聊MySQL的锁。数据库锁设计的初衷是处理并发问题。
并发事务访问相同记录的情况大致可以划分以下几种:
作为多用户共享的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则。
而锁就是用来实现这些访问规则的重要数据结构。
根据加锁的范围,MySQL 的锁可以分为全局锁、表锁和行锁。
“对于MyISAM、MEMORY、MERGE这些存储引擎来说,它们只支持表级锁,而且这些引擎并不支持事务,所以使用这些存储引擎的锁一般都是针对当前会话来说的。”
一、全局锁
什么是全局锁?干嘛用的?
全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。
当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞。
全局锁的适应场景之一,做全库的逻辑备份。把整个库的表数据都查出来存储为文本。
让整库都只读,在备份期间都不能执行更新,业务基本上就得停摆。这怎么办?
是的。
不加锁产生的问题
比如手机卡,购买套餐信息。这里分为两张表 u_acount (用于余额表),u_pricing (资费套餐表)。
1)u_account 表中数据 用户 A 余额:300;u_pricing 表中数据 用户 A 套餐:空
2)发起备份,备份过程中先备份 u_account 表,备份完了这个表,这个时候 u_account 用户余额是 300。
3)这个时候套用户购买了一个资费套餐 100,餐购买完成,写入到 u_print 套餐表购买成功,备份期间的数据。
4)备份完成。
可以看到备份的结果是,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修饰的列递增赋值的原理主要是两个:
1)采用表级别AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,在该语句执行结束后,再把AUTO-INC锁释放掉。
2)轻量级的锁,这种方式可以避免锁定表,可以提升插入性能:
三、行锁
行锁,也称为记录锁,顾名思义就是在记录上加的锁。这是最复杂的锁,前面的只是开胃菜。
一个行锁玩出了各种花样,也就是把行锁分成了各种类型。
MySQL 的行级锁是 InnoDB 存储引擎实现高并发的核心技术之一。它允许在保证数据一致性的同时,大幅提升数据库的并发处理能力。
下面这张表格汇总了行级锁的主要类型和核心特点,可以帮助你快速建立整体印象。
1、共享锁与排他锁
从锁的互斥性来看,行级锁分为共享锁和排他锁,它们的兼容关系是理解锁冲突的基础。
1)共享锁(Shared Lock, S Lock):也称为“读锁”。
2)排他锁(Exclusive Lock, X Lock):也称为“写锁”。
它们的兼容关系可以总结为下表:
2、行锁的底层实现和特性
1)行锁基于索引实现
这是理解行锁最核心的一点。InnoDB 的行锁是加在索引项上的,而不是直接加在物理数据行上的。这意味着:
2)两阶段锁协议(Two-Phase Locking, 2PL)
InnoDB 遵循此协议,锁的操作分为两个阶段:
3)意向锁(Intention Locks)
意向锁是表级锁,用于快速判断表内是否有被锁定的行,从而避免为了检查行锁而需要遍历每一行的低效操作。
3、行锁类型可视化
理解行锁的关键在于区分其三种基本类型。下图通过一个数据索引的例子,清晰展示了三种锁的锁定范围差异,这是理解所有高级锁概念的基础。
图解说明:
以下通过示例和场景进一步解释这三种锁。
1)记录锁(Record Lock)
它锁住的是索引项。例如,执行 SELECT * FROM users WHERE id = 10 FOR UPDATE;会在 id=10这个索引项上加一个排他型的记录锁,防止其他事务修改或删除这行数据。
2)间隙锁(Gap Lock)
它锁住的是索引项之间的“空隙”,以防止其他事务在这个空隙中插入新数据,从而解决“幻读”问题。
间隙锁只在可重复读(REPEATABLE READ)及以上隔离级别生效。
示例:假设一张表 users的 id字段有值 5, 10, 15。
3)临键锁(Next-Key Lock)
它是 InnoDB 在可重复读(REPEATABLE READ)隔离级别下默认使用的锁算法。
它相当于一个记录锁 + 间隙锁,锁定一个左开右闭的区间 (previous_index, current_index]。
示例:同样对于 id值为 5, 10, 15 的表。
四、死锁与最佳实践
“死锁是如何产生的呢?”
行级锁虽然提升了并发度,但也带来了死锁的风险。当两个或多个事务互相等待对方释放锁时,就会形成死锁。
理解 MySQL 中事务的加锁流程以及死锁如何形成,是构建高并发应用的基石。
下面我们通过一个清晰的流程图来展示一个安全的事务加锁/解锁全过程,然后深入剖析几种典型的死锁场景。
1、事务加锁与解锁完成流程
首先要明确一个核心概念:两阶段锁协议。它规定锁的操作分为两个阶段:
1)加锁阶段:在事务执行过程中,根据需要逐步获取锁。
2)解锁阶段:直到事务提交(COMMIT)或回滚(ROLLBACK)时,一次性释放所有在该事务中获取的锁。
下面的序列图清晰地展示了一个安全、无冲突的事务加锁与解锁流程。
流程解读:
这个流程是理想状态下的。但当多个事务并发执行且锁的获取顺序出现环状依赖时,死锁就发生了。
2、典型死锁场景详解
这个流程是理想状态下的。但当多个事务并发执行且锁的获取顺序出现环状依赖时,死锁就发生了。
1)场景 1:共享锁升级导致的死锁
这是非常经典的死锁情况,常发生在先读后写的业务逻辑中。
死锁形成:
2)场景 2:顺序交叉访问导致的死锁
当多个事务以不同的顺序访问和锁定资源时,极易发生死锁。
死锁形成:
3)场景 3:Gap 锁冲突导致的死锁
在可重复读(REPEATABLE READ)隔离级别下,MySQL 会使用间隙锁(Gap Lock)来防止幻读,这也可能引发更复杂的死锁。
假设 accounts表 id 有 1, 5, 10 三个值,存在间隙 (1,5), (5,10)。
死锁形成:
3、死锁的处理与预防
nnoDB 存储引擎内置了死锁检测机制。当检测到死锁时,它会选择一个回滚代价较小的事务(通常是影响行数较少的事务)进行回滚,并让另一个事务继续执行。
被回滚的事务会收到 ERROR 1213 (40001): Deadlock found错误。
4、核心预防策略
1)固定访问顺序
在应用设计中,保证所有事务以相同的顺序访问数据行。例如,约定更新账户时总是按 id 从小到大处理,可以避免场景二的死锁
2)避免在事务中加锁
如果业务允许,使用乐观锁(如版本号机制)替代悲观锁,从根本上减少锁竞争
3)保持事务小巧且快速
大事务会长时间持有锁,增加死锁概率。应将大事务拆分为小事务
4)为查询创建合适的索引
如果查询条件未使用索引,InnoDB 可能会锁住更多记录(甚至全表),大幅增加死锁风险。
5)使用较低的隔离级别
如果业务能接受,将隔离级别设为读已提交(READ COMMITTED),此级别下 InnoDB 不会使用间隙锁(Gap Lock),可减少死锁。
事务的加锁解锁遵循“两阶段锁协议”,提交或回滚时释放所有锁。死锁的本质是事务间形成了对锁资源的循环等待。
通过理解其原理并采用固定的资源访问顺序、使用乐观锁、减小事务粒度等预防措施,可以显著降低死锁发生概率。
希望这些图解和说明能帮助你更深入地理解 MySQL 的锁机制。
作者丨李健青
来源丨公众号:码哥跳动(ID:MageByte)
dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn
如果字段的最大可能长度超过255字节,那么长度值可能…
只能说作者太用心了,优秀
感谢详解
一般干个7-8年(即30岁左右),能做到年入40w-50w;有…
230721