讲一下MySQL里有哪些锁?
面试官您好,MySQL中的锁机制非常丰富,它是保证数据一致性和并发安全的核心。我通常会从锁的粒度(加锁范围) 和锁的模式(功能) 这两个维度来理解它们。
第一维度:按锁的粒度划分
按加锁的范围,MySQL的锁可以分为三级:全局锁、表级锁、行级锁。粒度从大到小,加锁开销从低到高,并发性能则从低到高。
-
1. 全局锁 (Global Lock)
- 特点 :对整个数据库实例加锁。
- 具体实现 :通过命令
FLUSH TABLES WITH READ LOCK;
(FTWRL) 来实现。 - 作用 :执行后,整个数据库会处于只读状态 。所有的数据更新语句(
INSERT
,UPDATE
,DELETE
)、数据定义语句(DDL
)和事务提交语句(COMMIT
)都会被阻塞。 - 应用场景 :主要用于全库的逻辑备份。通过加全局锁,可以保证在备份期间,获得一个完全一致的数据快照。
-
2. 表级锁 (Table-level Lock)
- 特点 :对整张数据表加锁。
- 具体实现 :
- 表锁 :通过
LOCK TABLES ... READ/WRITE;
命令可以显式地加表锁。 - 元数据锁 (Meta Data Lock, MDL) :这是MySQL 5.5引入的,由系统自动添加 。当对一个表做增删改查时,会自动加上MDL读锁;当要修改表结构(
DDL
)时,会自动加上MDL写锁。MDL锁主要是为了防止在查询时,有另一个线程来修改表结构,保证数据一致性。 - 意向锁 (Intention Lock) :这是InnoDB引擎特有的、由系统自动管理的表级锁。它不与行锁冲突,它的唯一作用就是"表态"------告诉其他事务,"这张表里有某些行已经被加锁了"。比如,一个事务想加表级写锁,它就不需要去遍历每一行看有没有被锁,只需要检查一下表上有没有意向锁即可,大大提高了效率。
- 表锁 :通过
-
3. 行级锁 (Row-level Lock)
- 特点 :只对某一行或某几行数据 加锁。这是InnoDB存储引擎的巨大优势,也是它能支持高并发的关键。行级锁的粒度最细,锁冲突的概率最低,并发性能最好。
- 具体实现 (InnoDB) :
- 记录锁 (Record Lock) :最简单的行锁,就是精确地锁住一条索引记录。
- 间隙锁 (Gap Lock) :它锁住的是一个 "间隙" ,即两条索引记录之间的开区间。比如,锁住(3, 8)这个区间。它的主要作用是防止幻读,阻止其他事务在这个间隙中插入新的记录。
- 临键锁 (Next-Key Lock) :可以看作是记录锁和间隙锁的结合体 。它既锁住了记录本身,又锁住了该记录之前的那个间隙。这是InnoDB在 "可重复读"隔离级别下,默认的行锁算法。
第二维度:按锁的模式/功能划分
- 共享锁 (Shared Lock, S锁) :也叫读锁。多个事务可以同时持有同一份数据的S锁,大家可以一起读。
- 排他锁 (Exclusive Lock, X锁) :也叫写锁。它是独占的。只要有一个事务持有了X锁,其他任何事务都不能再获取该数据的任何锁(无论是S锁还是X锁)。
补充:乐观锁与悲观锁
- 这是一种思想层面的划分。
- 悲观锁:就是上面提到的所有锁机制,认为冲突总会发生,所以先加锁再操作。
- 乐观锁 :不加锁,而是在更新时通过版本号(version)或CAS机制来检查数据是否被修改过。这在应用层实现,不是MySQL数据库自带的锁机制。
总结一下 ,MySQL通过一个从粗到细(全局->表->行)的锁粒度体系,并结合读写模式(S/X锁) ,以及在InnoDB中精巧的行锁实现(记录锁、间隙锁、临键锁),为我们提供了非常丰富和强大的并发控制能力。在开发中,理解并善用InnoDB的行级锁,是实现高性能并发事务的关键。
数据库的表锁和行锁有什么作用?
面试官您好,表锁和行锁是数据库为了管理并发访问而采用的两种不同粒度 的锁定机制。它们没有绝对的优劣之分,而是分别适用于不同的场景,是在 "加锁开销" 和 "并发性能" 之间做出不同权衡的结果。
1. 表锁 (Table Lock) ------ "简单粗暴,开销小,但并发差"
-
作用与特点:
- 粒度最大 :当一个事务对一张表加锁时,它会锁定整张表。
- 实现简单,加锁开销小:因为只需要一个锁来管理整张表,所以加锁和释放锁的逻辑非常简单,系统开销很低。
- 锁冲突概率最高 :这是它最致命的缺点。只要有一个事务锁住了这张表,其他任何想操作这张表的事务(无论是读还是写,取决于锁的模式)都必须等待。这使得并发性能非常差。
-
适用场景:
- 它非常适合那些大批量的、针对全表 的操作。比如,
ALTER TABLE
修改表结构,或者对整张表进行数据迁移、批量更新等。在这些场景下,锁定整张表反而是最简单高效的方式。 - MyISAM存储引擎主要使用的就是表级锁,这也是为什么MyISAM在写操作频繁的场景下并发性能很差的原因。
- 它非常适合那些大批量的、针对全表 的操作。比如,
2. 行锁 (Row Lock) ------ "精准控制,并发好,但开销大"
-
作用与特点:
- 粒度最细 :只锁定被操作的特定一行或几行数据。
- 并发性能最高 :这是它最大的优势。不同的事务可以同时操作同一张表中的不同行,而互不干扰,极大地提高了系统的并发处理能力。
- 实现复杂,加锁开销大:因为需要为每一行都可能建立锁信息,所以行锁的实现逻辑更复杂,加锁和管理的系统开销也比表锁要大。
- 可能引发死锁 :由于锁的粒度变细,多个事务在操作不同行时,更容易形成复杂的锁等待关系,从而增加了死锁发生的概率。
-
适用场景:
- 非常适合那些高并发、事务操作只涉及少量行的场景。这几乎是所有现代在线交易系统(OLTP)的典型特征。比如,订单系统、用户账户系统等,每次操作都只是针对一个或几个用户的特定数据。
- InnoDB存储引擎的巨大优势,就在于它实现了高效的行级锁,这也是它成为MySQL默认和主流存储引擎的核心原因。
总结与权衡
特性 | 表锁 (Table Lock) | 行锁 (Row Lock) |
---|---|---|
锁定粒度 | 整张表 | 单行/多行数据 |
加锁开销 | 小 | 大 |
并发性能 | 差 | 高 |
锁冲突概率 | 高 | 低 |
死锁概率 | 低 | 高 |
主要使用者 | MyISAM | InnoDB |
一句话总结 :表锁是用"牺牲并发度"来换取"低开销和简单性",而行锁是用"更高的系统开销和更复杂的实现"来换取"极高的并发性能"。在今天的互联网应用中,高并发是常态,因此支持行锁的InnoDB引擎成为了绝对的主流。
MySQL两个线程的UPDATE语句同时处理一条数据,会不会有阻塞?
面试官您好,您提出的这个问题,直击了数据库并发控制的核心。
答案是:会的,后一个UPDATE
语句会被阻塞。
这背后的根本原因,是MySQL InnoDB存储引擎强大的行级锁机制。
1. 详细的执行过程分析
我们来详细地模拟一下这个过程,假设我们有两个独立的事务,事务A和事务B,它们都要执行UPDATE ... WHERE id = 1
。
-
事务A抢先执行:
- 首先,事务A开始执行
UPDATE
语句。 - 为了保证数据的一致性和操作的原子性,InnoDB会在定位到
id = 1
这条记录时,立即为它加上一把排他锁(Exclusive Lock),也就是X锁 。这把锁,具体来说,就是一把记录锁(Record Lock)。 - 加上X锁后,事务A开始对这条记录进行修改。
- 首先,事务A开始执行
-
事务B随后执行:
- 紧接着,事务B也开始执行它自己的
UPDATE
语句,同样尝试去修改id = 1
的记录。 - 当事务B尝试去获取
id = 1
这条记录的锁时,它会发现这行记录已经被事务A持有了X锁。 - 锁冲突发生 :X锁是排他的,意味着只要它存在,其他任何事务都无法再对这条记录获取任何类型的锁(无论是共享的S锁还是排他的X锁)。
- 紧接着,事务B也开始执行它自己的
-
事务B进入阻塞状态:
- 因此,事务B的
UPDATE
语句会立即进入阻塞(Waiting)状态。它会一直在这里等待,直到事务A释放这把锁。
- 因此,事务B的
2. 阻塞之后会发生什么?
-
情况一:事务A提交(COMMIT)
- 当事务A完成所有操作并提交后,它会释放 掉所有持有的锁,包括
id = 1
这条记录上的X锁。 - 锁被释放的瞬间,正在等待的事务B会被唤醒 ,它会立即获取到X锁,然后继续执行自己的
UPDATE
操作。
- 当事务A完成所有操作并提交后,它会释放 掉所有持有的锁,包括
-
情况二:事务A回滚(ROLLBACK)
- 如果事务A因为某种原因回滚了,它同样会释放所有锁。
- 事务B同样会被唤醒,获取锁,然后执行
UPDATE
。
-
情况三:锁等待超时
- 事务B的等待不是无限的。这个等待时间由MySQL的参数
innodb_lock_wait_timeout
控制(默认是50秒)。 - 如果事务A长时间不提交也不回滚,导致事务B的等待时间超过了这个阈值,那么事务B的
UPDATE
语句就会失败 ,并抛出一个 "Lock wait timeout exceeded" 的错误。
- 事务B的等待不是无限的。这个等待时间由MySQL的参数
3. 与"读"操作的对比
-
如果事务B执行的是普通
SELECT
:SELECT * FROM ... WHERE id = 1;
- 在InnoDB的默认隔离级别(可重复读)下,这个读操作会通过MVCC 来执行,它会去读取一个历史快照版本的数据,不会去获取锁 。因此,它和事务A的
UPDATE
不会发生冲突,不会阻塞。
- 在InnoDB的默认隔离级别(可重复读)下,这个读操作会通过MVCC 来执行,它会去读取一个历史快照版本的数据,不会去获取锁 。因此,它和事务A的
-
如果事务B执行的是加锁读
SELECT ... FOR UPDATE
:- 这个操作也需要获取X锁,所以它的行为会和
UPDATE
一样,同样会被阻塞。
- 这个操作也需要获取X锁,所以它的行为会和
总结一下 ,两个线程(事务)同时UPDATE
同一条数据,由于InnoDB行级锁(X锁)的互斥性 ,必然会导致后一个事务被阻塞,直到前一个事务结束。这是数据库保证写-写操作数据一致性的基本手段。
两条UPDATE语句处理一张表的不同的主键范围的记录,一个<10,一个>15,会不会遇到阻塞?底层是为什么的?
面试官您好,您提出的这个问题非常好,它触及了InnoDB在"可重复读"(Repeatable Read)隔离级别下,范围更新 时临键锁(Next-Key Lock) 的工作机制。
直接的答案是:在绝大多数情况下,它们不会相互阻塞。但存在一种特殊的边界情况,可能会导致阻塞。
要理解这一点,我们需要先明确InnoDB的默认行锁算法------临键锁。
- 临键锁 (Next-Key Lock) = 记录锁 (Record Lock) + 间隙锁 (Gap Lock)
- 它不仅会锁住满足条件的记录本身 ,还会锁住这条记录之前的那个 "间隙"。
- 它的主要目的是为了防止幻读。
下面我们来分两种情况讨论:
情况一:两个UPDATE
语句的范围内,都存在实际的数据行(最常见的情况)
假设我们的表中,数据分布是这样的:id
有 1, 5, 8, 16, 20
...
-
事务A执行:
UPDATE ... WHERE id < 10;
- 加锁分析 :InnoDB会扫描
id
小于10的范围。- 它会给
id=1, 5, 8
这三条记录加上X型的记录锁。 - 同时,它会给这些记录之间的间隙,以及
id=8
到id=16
之间的间隙,都加上X型的间隙锁。
- 它会给
- 最终锁定的范围 :大致可以理解为
(-∞, 1]
,(1, 5]
,(5, 8]
,(8, 16)
。 注意,16
这条记录本身没有被锁,但它之前的间隙被锁了。
- 加锁分析 :InnoDB会扫描
-
事务B执行:
UPDATE ... WHERE id > 15;
- 加锁分析 :InnoDB会扫描
id
大于15的范围。- 它会给
id=16, 20
这两条记录加上X型的记录锁。 - 同时,它会给
id=16
到id=20
的间隙,以及id=20
到正无穷的间隙,都加上X型的间隙锁。
- 它会给
- 最终锁定的范围 :大致可以理解为
(8, 16]
,(16, 20]
,(20, +∞)
。注意,这里的(8, 16]
和事务A的(8, 16)
,实际上是对同一个间隙加锁,但由于事务B要锁id=16
这条记录,它会成功,因为事务A没有锁住这条记录。
- 加锁分析 :InnoDB会扫描
-
结论 :在这种情况下,两个事务加锁的记录 和间隙 是完全不重叠 的。因此,它们不会相互阻塞,可以并行执行。
情况二:两个UPDATE
语句的范围内,没有任何数据行(特殊的边界情况)
这是一个容易被忽略的、但能体现理解深度的特例。假设我们的表中,数据是 id = 20, 30
。
-
事务A执行:
UPDATE ... WHERE id < 10;
- 加锁分析 :InnoDB扫描
id < 10
的范围,发现没有任何记录。 - 间隙锁的行为 :为了防止有其他事务插入
id < 10
的数据(防止幻读),它仍然需要加一个间隙锁 。它会找到第一个大于10的记录(即id=20
),然后锁住从负无穷到id=20
之间的这个巨大间隙。 - 最终锁定的范围 :
(-∞, 20)
。注意,是开区间,不包括20这条记录。
- 加锁分析 :InnoDB扫描
-
事务B执行:
UPDATE ... WHERE id > 15;
- 加锁分析 :InnoDB扫描
id > 15
的范围。- 它首先要定位到大于15的第一条记录,也就是
id=20
。 - 它尝试对
id=20
这条记录以及它之前的间隙(..., 20]
加临键锁。
- 它首先要定位到大于15的第一条记录,也就是
- 锁冲突发生 :当它尝试在
id=20
之前的间隙(也就是(..., 20)
这个区间)加锁时,发现这个间隙已经被事务A的间隙锁锁住了。
- 加锁分析 :InnoDB扫描
-
结论 :在这种特殊情况下,虽然两个
UPDATE
的条件范围(<10
和>15
)在逻辑上没有任何交集,但由于间隙锁的存在 ,它们可能会去争抢同一个"间隙"的锁,从而导致事务B被事务A阻塞。
总结
- 在绝大多数 数据分布正常的情况下,两个
UPDATE
操作不同范围的记录,由于行锁和间隙锁的范围不重叠,不会发生阻塞。 - 但在一些边界或数据稀疏的特殊情况下 ,由于间隙锁 可能会锁定一个比查询范围更大的"空隙",导致两个看似无关的
UPDATE
语句,也可能发生锁冲突和阻塞。
能分析到第二种情况,并解释清楚间隙锁在其中的作用,就能充分展示您对InnoDB锁机制的深刻理解。
如果2个范围不是主键或索引?还会阻塞吗?
面试官您好,您提出的这个问题,其答案与上一个问题截然相反。
答案是:会的,后一个UPDATE
语句会被阻塞。
这背后的根本原因,正如您所分析的:当UPDATE
语句的WHERE
条件中,使用的列没有索引时,MySQL无法进行高效的定位,只能退化为全表扫描。而在InnoDB的"可重复读"隔离级别下,全表扫描会给扫描过的每一条记录都加上行锁(临键锁)。
1. 发生了什么?------ 从索引定位到全表扫描
- 有索引时 :当
WHERE
条件是主键或索引列时,InnoDB可以利用B+树,精确地、快速地 定位到需要修改的那几行记录,然后只对这几行以及它们周围的间隙加锁。 - 没有索引时 :当
WHERE
条件是一个普通列时(比如status
列),InnoDB不知道 哪些行的status
满足条件。它唯一的办法,就是从聚簇索引的第一行开始,逐行地向后扫描,直到表的末尾 ,然后对每一行都判断其status
值是否符合条件。这个过程,就是全表扫描。
2. 为什么全表扫描会锁住整张表?
这是InnoDB为了保证事务的隔离性 和数据的一致性而必须采取的措施。
- 加锁过程 :在全表扫描的过程中,InnoDB为了确保它正在检查的行不会被其他事务修改(以避免不可重复读等问题),它会对自己扫描过的每一条记录 ,都加上X型的临键锁(Next-Key Lock)。
- 最终结果 :当这个全表扫描结束后,相当于表中的每一条记录 ,以及记录之间的每一个间隙 ,都被加上了锁。从效果上看,这就等同于锁住了整张表。
3. 场景分析
-
事务A执行:
UPDATE ... WHERE status < 10;
- 由于
status
列没有索引,InnoDB开始全表扫描。 - 它从第一行开始,扫描一行,加一个临键锁;再扫描下一行,再加一个临键锁......
- 当事务A完成扫描并修改了满足条件的行后,它已经持有了整张表的行锁和间隙锁。
- 由于
-
事务B执行:
UPDATE ... WHERE status > 15;
- 事务B开始执行,它也需要进行全表扫描。
- 当它尝试去扫描并锁定第一行记录时,发现这行记录已经被事务A的锁锁住了。
- 锁冲突发生 ,事务B立即进入阻塞状态,等待事务A提交或回滚。
结论与实践建议
- 结论 :当
UPDATE
或DELETE
语句的WHERE
条件列没有索引 时,InnoDB会从行级锁 "升级" 为事实上的 表级锁,导致严重的锁竞争和性能问题。 - 实践建议 :
- 这是我们在SQL开发中必须极力避免的情况。
- 对于所有会出现在
UPDATE
或DELETE
语句的WHERE
子句中的列,都应该建立合适的索引。 - 在上线前,对所有核心的写操作SQL,都应该使用
EXPLAIN
来检查其执行计划,确保type
列不是ALL
(全表扫描),并且key
列显示它用上了正确的索引。
这个例子也从反面印证了索引对于写操作的重要性 ------它不仅提升查询性能,更是缩小写操作锁范围、保证高并发写入的关键。