Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL全局锁、表锁、行锁、间隙锁、临键锁

MySQL全局锁、表锁、行锁、间隙锁、临键锁超详细讲解

作者:小马不敲代码

锁是计算机用以协调多个进程间并发访问同一共享资源的一种机制,MySQL中为了保证数据访问的一致性与有效性等功能,实现了锁机制,这篇文章主要介绍了MySQL全局锁、表锁、行锁、间隙锁、临键锁超详细讲解的相关资料,需要的朋友可以参考下

并发事务访问相同记录的情况大致可以划分以下几种:

作为多用户共享的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则。

而锁就是用来实现这些访问规则的重要数据结构。

根据加锁的范围,MySQL 的锁可以分为全局锁、表锁和行锁。

“
对于MyISAM、MEMORY、MERGE这些存储引擎来说,它们只支持表级锁,而且这些引擎并不支持事务,所以使用这些存储引擎的锁一般都是针对当前会话来说的。

全局锁

全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。

当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞。

全局锁的适应场景之一,做全库的逻辑备份。把整个库的表数据都查出来存储为文本。

让整库都只读,在备份期间都不能执行更新,业务基本上就得停摆。这怎么办?

不加锁产生的问题
比如手机卡,购买套餐信息。这里分为两张表 u_acount (用于余额表),u_pricing (资费套餐表)。

可以看到备份的结果是,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、共享锁(Shared Lock, S Lock):也称为“读锁”。

作用:允许多个事务同时读取同一行数据。

互斥性:一个事务持有共享锁后,其他事务可以继续加共享锁来读取数据,但不能加排他锁来修改数据

加锁方式:使用 SELECT … LOCK IN SHARE MODE;或 SELECT … FOR SHARE;

2、排他锁(Exclusive Lock, X Lock):也称为“写锁”。

使用 SELECT … FOR UPDATE;显式加锁。

自动加锁:UPDATE, DELETE, INSERT语句会自动对其操作的记录加排他锁。

作用:允许一个事务读写某行数据。

互斥性:一个事务持有排他锁后,其他事务不能再对该行加任何类型的锁(包括共享锁和其他排他锁),直到该锁被释放。

加锁方式:

它们的兼容关系可以总结为下表:

行锁的底层实现和特性

行锁基于索引实现

这是理解行锁最核心的一点。InnoDB 的行锁是加在索引项上的,而不是直接加在物理数据行上的。这意味着:

两阶段锁协议(Two-Phase Locking, 2PL)

InnoDB 遵循此协议,锁的操作分为两个阶段:

重要提示:由于锁是在事务结束后才释放,为了减少锁冲突和提高并发性,应尽量将最可能引起冲突的写操作(如 SELECT … FOR UPDATE)放在事务的后面执行,以缩短排他锁的持有时间。

意向锁(Intention Locks)

意向锁是表级锁,用于快速判断表内是否有被锁定的行,从而避免为了检查行锁而需要遍历每一行的低效操作。

行锁类型可视化

理解行锁的关键在于区分其三种基本类型。下图通过一个数据索引的例子,清晰展示了三种锁的锁定范围差异,这是理解所有高级锁概念的基础。

图解说明:

以下通过示例和场景进一步解释这三种锁。

记录锁(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 中事务的加锁流程以及死锁如何形成,是构建高并发应用的基石。

下面我们通过一个清晰的流程图来展示一个安全的事务加锁/解锁全过程,然后深入剖析几种典型的死锁场景。

事务加锁与解锁完成流程

首先要明确一个核心概念:两阶段锁协议。它规定锁的操作分为两个阶段:

1、加锁阶段:在事务执行过程中,根据需要逐步获取锁。

2、解锁阶段:直到事务提交(COMMIT)或回滚(ROLLBACK)时,一次性释放所有在该事务中获取的锁。

下面的序列图清晰地展示了一个安全、无冲突的事务加锁与解锁流程。

流程解读:

加锁:事务 A 首先开启事务,然后执行一条 SELECT … FOR UPDATE语句,意图锁定某行数据(例如行 1)进行更新。此时,它会向锁管理器申请该行的排他锁(X Lock)

执行:成功获得锁后,事务 A 可以安全地执行修改操作。在此期间,其他事务(如事务 B)如果尝试获取同一行的排他锁或共享锁,都会被阻塞。

解锁:当事务 A 提交(COMMIT)后,进入解锁阶段,一次性释放它持有的所有锁。

后续:锁被释放后,之前被阻塞的事务 B 会被唤醒,并获得它所需要的锁,继续执行。

这个流程是理想状态下的。但当多个事务并发执行且锁的获取顺序出现环状依赖时,死锁就发生了。

典型死锁场景详解

这个流程是理想状态下的。但当多个事务并发执行且锁的获取顺序出现环状依赖时,死锁就发生了。

场景 1:共享锁升级导致的死锁

这是非常经典的死锁情况,常发生在先读后写的业务逻辑中。

死锁形成:

在 T3 时刻,事务 A 需要事务 B 释放 S 锁才能升级。

在 T4 时刻,事务 B 需要事务 A 释放 S 锁才能升级。

双方互相等待,形成循环等待,死锁产生。

场景 2:顺序交叉访问导致的死锁

当多个事务以不同的顺序访问和锁定资源时,极易发生死锁。

死锁形成:

事务 A 在等待事务 B 释放 id=2 的锁。

事务 B 在等待事务 A 释放 id=1 的锁。

循环等待再次形成,死锁发生。

场景 3:Gap 锁冲突导致的死锁

在可重复读(REPEATABLE READ)隔离级别下,MySQL 会使用间隙锁(Gap Lock)来防止幻读,这也可能引发更复杂的死锁。

假设 accounts表 id 有 1, 5, 10 三个值,存在间隙 (1,5), (5,10)。

死锁形成:

事务 A 的插入在等待事务 B 释放(1,5)间隙上的锁。

事务 B 的插入在等待事务 A 释放(1,5)间隙上的锁。

循环等待形成,死锁发生。

死锁的处理与预防

nnoDB 存储引擎内置了死锁检测机制。当检测到死锁时,它会选择一个回滚代价较小的事务(通常是影响行数较少的事务)进行回滚,并让另一个事务继续执行。

被回滚的事务会收到 ERROR 1213 (40001): Deadlock found错误。

核心预防策略

事务的加锁解锁遵循“两阶段锁协议”,提交或回滚时释放所有锁。死锁的本质是事务间形成了对锁资源的循环等待。

通过理解其原理并采用固定的资源访问顺序、使用乐观锁、减小事务粒度等预防措施,可以显著降低死锁发生概率。

总结

到此这篇关于MySQL全局锁、表锁、行锁、间隙锁、临键锁超详细讲解的文章就介绍到这了,更多相关MySQL全局锁、表锁、行锁、间隙锁、临键锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文