欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 养生 > Java八股文——MySQL「锁篇」

Java八股文——MySQL「锁篇」

2025/6/18 11:31:49 来源:https://blog.csdn.net/qq_54452916/article/details/148699113  浏览:    关键词:Java八股文——MySQL「锁篇」

讲一下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)
锁定粒度整张表单行/多行数据
加锁开销
并发性能
锁冲突概率
死锁概率
主要使用者MyISAMInnoDB

一句话总结表锁是用“牺牲并发度”来换取“低开销和简单性”,而行锁是用“更高的系统开销和更复杂的实现”来换取“极高的并发性能”。在今天的互联网应用中,高并发是常态,因此支持行锁的InnoDB引擎成为了绝对的主流。


MySQL两个线程的UPDATE语句同时处理一条数据,会不会有阻塞?

面试官您好,您提出的这个问题,直击了数据库并发控制的核心。

答案是:会的,后一个UPDATE语句会被阻塞。

这背后的根本原因,是MySQL InnoDB存储引擎强大的行级锁机制

1. 详细的执行过程分析

我们来详细地模拟一下这个过程,假设我们有两个独立的事务,事务A和事务B,它们都要执行UPDATE ... WHERE id = 1

  1. 事务A抢先执行

    • 首先,事务A开始执行UPDATE语句。
    • 为了保证数据的一致性和操作的原子性,InnoDB会在定位到id = 1这条记录时,立即为它加上一把排他锁(Exclusive Lock),也就是X锁。这把锁,具体来说,就是一把记录锁(Record Lock)
    • 加上X锁后,事务A开始对这条记录进行修改。
  2. 事务B随后执行

    • 紧接着,事务B也开始执行它自己的UPDATE语句,同样尝试去修改id = 1的记录。
    • 当事务B尝试去获取id = 1这条记录的锁时,它会发现这行记录已经被事务A持有了X锁
    • 锁冲突发生:X锁是排他的,意味着只要它存在,其他任何事务都无法再对这条记录获取任何类型的锁(无论是共享的S锁还是排他的X锁)。
  3. 事务B进入阻塞状态

    • 因此,事务B的UPDATE语句会立即进入阻塞(Waiting)状态。它会一直在这里等待,直到事务A释放这把锁。
2. 阻塞之后会发生什么?
  • 情况一:事务A提交(COMMIT)

    • 当事务A完成所有操作并提交后,它会释放掉所有持有的锁,包括id = 1这条记录上的X锁。
    • 锁被释放的瞬间,正在等待的事务B会被唤醒,它会立即获取到X锁,然后继续执行自己的UPDATE操作。
  • 情况二:事务A回滚(ROLLBACK)

    • 如果事务A因为某种原因回滚了,它同样会释放所有锁。
    • 事务B同样会被唤醒,获取锁,然后执行UPDATE
  • 情况三:锁等待超时

    • 事务B的等待不是无限的。这个等待时间由MySQL的参数innodb_lock_wait_timeout控制(默认是50秒)。
    • 如果事务A长时间不提交也不回滚,导致事务B的等待时间超过了这个阈值,那么事务B的UPDATE语句就会失败,并抛出一个 “Lock wait timeout exceeded” 的错误。
3. 与“读”操作的对比
  • 如果事务B执行的是普通SELECTSELECT * FROM ... WHERE id = 1;

    • 在InnoDB的默认隔离级别(可重复读)下,这个读操作会通过MVCC来执行,它会去读取一个历史快照版本的数据,不会去获取锁。因此,它和事务A的UPDATE不会发生冲突,不会阻塞
  • 如果事务B执行的是加锁读SELECT ... FOR UPDATE

    • 这个操作也需要获取X锁,所以它的行为会和UPDATE一样,同样会被阻塞

总结一下,两个线程(事务)同时UPDATE同一条数据,由于InnoDB行级锁(X锁)的互斥性,必然会导致后一个事务被阻塞,直到前一个事务结束。这是数据库保证写-写操作数据一致性的基本手段。


两条UPDATE语句处理一张表的不同的主键范围的记录,一个<10,一个>15,会不会遇到阻塞?底层是为什么的?

面试官您好,您提出的这个问题非常好,它触及了InnoDB在“可重复读”(Repeatable Read)隔离级别下,范围更新临键锁(Next-Key Lock) 的工作机制。

直接的答案是:在绝大多数情况下,它们不会相互阻塞。但存在一种特殊的边界情况,可能会导致阻塞。

要理解这一点,我们需要先明确InnoDB的默认行锁算法——临键锁。

  • 临键锁 (Next-Key Lock) = 记录锁 (Record Lock) + 间隙锁 (Gap Lock)
    • 它不仅会锁住满足条件的记录本身,还会锁住这条记录之前的那个 “间隙”
    • 它的主要目的是为了防止幻读

下面我们来分两种情况讨论:

情况一:两个UPDATE语句的范围内,都存在实际的数据行(最常见的情况)

假设我们的表中,数据分布是这样的:id1, 5, 8, 16, 20

  • 事务A执行: UPDATE ... WHERE id < 10;

    • 加锁分析:InnoDB会扫描id小于10的范围。
      1. 它会给id=1, 5, 8这三条记录加上X型的记录锁
      2. 同时,它会给这些记录之间的间隙,以及id=8id=16之间的间隙,都加上X型的间隙锁
    • 最终锁定的范围:大致可以理解为 (-∞, 1], (1, 5], (5, 8], (8, 16)。 注意,16这条记录本身没有被锁,但它之前的间隙被锁了。
  • 事务B执行: UPDATE ... WHERE id > 15;

    • 加锁分析:InnoDB会扫描id大于15的范围。
      1. 它会给id=16, 20这两条记录加上X型的记录锁
      2. 同时,它会给id=16id=20的间隙,以及id=20到正无穷的间隙,都加上X型的间隙锁
    • 最终锁定的范围:大致可以理解为 (8, 16], (16, 20], (20, +∞)。注意,这里的(8, 16]和事务A的(8, 16),实际上是对同一个间隙加锁,但由于事务B要锁id=16这条记录,它会成功,因为事务A没有锁住这条记录。
  • 结论:在这种情况下,两个事务加锁的记录间隙完全不重叠的。因此,它们不会相互阻塞,可以并行执行。

情况二:两个UPDATE语句的范围内,没有任何数据行(特殊的边界情况)

这是一个容易被忽略的、但能体现理解深度的特例。假设我们的表中,数据是 id = 20, 30

  • 事务A执行: UPDATE ... WHERE id < 10;

    • 加锁分析:InnoDB扫描id < 10的范围,发现没有任何记录
    • 间隙锁的行为:为了防止有其他事务插入id < 10的数据(防止幻读),它仍然需要加一个间隙锁。它会找到第一个大于10的记录(即id=20),然后锁住从负无穷到id=20之间的这个巨大间隙。
    • 最终锁定的范围(-∞, 20)。注意,是开区间,不包括20这条记录。
  • 事务B执行: UPDATE ... WHERE id > 15;

    • 加锁分析:InnoDB扫描id > 15的范围。
      1. 它首先要定位到大于15的第一条记录,也就是id=20
      2. 它尝试对id=20这条记录以及它之前的间隙 (..., 20] 加临键锁。
    • 锁冲突发生:当它尝试在id=20之前的间隙(也就是(..., 20)这个区间)加锁时,发现这个间隙已经被事务A的间隙锁锁住了
  • 结论:在这种特殊情况下,虽然两个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;

    1. 由于status列没有索引,InnoDB开始全表扫描
    2. 它从第一行开始,扫描一行,加一个临键锁;再扫描下一行,再加一个临键锁……
    3. 当事务A完成扫描并修改了满足条件的行后,它已经持有了整张表的行锁和间隙锁
  • 事务B执行:UPDATE ... WHERE status > 15;

    1. 事务B开始执行,它也需要进行全表扫描。
    2. 当它尝试去扫描并锁定第一行记录时,发现这行记录已经被事务A的锁锁住了
    3. 锁冲突发生,事务B立即进入阻塞状态,等待事务A提交或回滚。
结论与实践建议
  • 结论:当UPDATEDELETE语句的WHERE条件列没有索引时,InnoDB会从行级锁 “升级” 为事实上的 表级锁,导致严重的锁竞争和性能问题。
  • 实践建议
    • 这是我们在SQL开发中必须极力避免的情况。
    • 对于所有会出现在UPDATEDELETE语句的WHERE子句中的列,都应该建立合适的索引
    • 在上线前,对所有核心的写操作SQL,都应该使用EXPLAIN来检查其执行计划,确保type列不是ALL(全表扫描),并且key列显示它用上了正确的索引。

这个例子也从反面印证了索引对于写操作的重要性——它不仅提升查询性能,更是缩小写操作锁范围、保证高并发写入的关键。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词