Skip to content

MySQL 事务与锁面试通关指南 ​


❓ 面试官:能说说你对事务 ACID 特性的理解吗? ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论): 事务的 ACID 分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。InnoDB 引擎通过 Redo Log 保证持久性,Undo Log 保证原子性,MVCC 和锁保证隔离性,最终通过这三个特性来保证一致性。

📝 详细原理解析:

  1. A(原子性):一个事务中的所有操作,要么全部成功,要么全部失败。通过 Undo Log(回滚日志) 实现。如果事务执行失败或调用了 rollback,InnoDB 会根据 Undo Log 中的反向操作将数据恢复到事务开始前的状态。
  2. I(隔离性):多个并发事务之间相互不影响。主要通过 锁机制(排他锁、共享锁等) 和 MVCC(多版本并发控制) 来实现。
  3. D(持久性):事务一旦提交,对数据的改变就是永久的,即使宕机也不丢失。通过 Redo Log(重做日志) 实现。修改数据时先写日志到磁盘(WAL技术),宕机重启后可通过 Redo Log 恢复数据。
  4. C(一致性):事务执行前后,数据必须满足业务规则(如转账前后总金额不变)。这是事务的最终目的,由 A、I、D 以及业务代码共同保证。

🌟 面试加分项(实战经验): 很多候选人分不清 Redo log 和 Binlog 的区别。Redo log 是 InnoDB 引擎特有的,属于物理日志,记录的是"在某个数据页上做了什么修改",用于崩溃恢复(Crash-safe);而 Binlog 是 MySQL Server 层特有的,属于逻辑日志,用于主从复制和数据备份。我们在生产环境排查误删数据时,主要就是靠解析 Binlog 来恢复数据的。


❓ 面试官:并发事务会带来哪些问题?MySQL 的隔离级别有哪些? ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论): 并发事务会带来脏读、不可重复读和幻读的问题。MySQL 提供了四种隔离级别:读未提交、读已提交、可重复读(MySQL 默认)和串行化。

📝 详细原理解析:三大并发问题:

  1. 脏读:事务 A 读到了事务 B 已经修改但还没提交的数据。如果 B 回滚了,A 读到的就是无效的脏数据。
  2. 不可重复读:事务 A 在两次读取同一条记录之间,事务 B 修改并提交了该记录,导致 A 两次读到的数据不一致。
  3. 幻读:事务 A 在两次范围查询之间,事务 B 插入或删除了满足条件的记录,导致 A 发现多出(或少)了一些"幻影"行。

四种隔离级别(级别由低到高,性能由高到低):

隔离级别解决脏读解决不可重复读解决幻读默认引擎
Read Uncommitted(读未提交)❌❌❌无
Read Committed(读已提交/RC)✅❌❌Oracle
Repeatable Read(可重复读/RR)✅✅❌(MySQL通过Gap锁解决大半)MySQL
Serializable(串行化)✅✅✅无

🌟 面试加分项(实战经验): 虽然 MySQL 的默认隔离级别是 RR(可重复读),但在很多大厂(比如阿里、字节)的生产环境中,会把隔离级别改成 RC(读已提交)。原因是:

  1. RC 级别下没有间隙锁(Gap Lock),能大大降低发生死锁的概率,并发写入性能更高。
  2. RC 级别下,条件列如果没有索引,在全表扫描时如果发现不满足条件,会提前释放锁,而 RR 级别会锁住所有扫描过的行直到事务结束。

❓ 面试官:什么是 MVCC?它的底层实现原理是什么? ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论): MVCC(Multi-Version Concurrency Control)即多版本并发控制。它主要是为了在 RC 和 RR 隔离级别下,解决读写冲突问题,做到"读不加锁,读写不冲突"。它的底层是由 隐藏字段、Undo Log 版本链、Read View(读视图) 共同实现的。

📝 详细原理解析:

  1. 三个隐藏字段:
    • DB_TRX_ID:记录最后一次插入或更新该行的事务 ID。
    • DB_ROLL_PTR:回滚指针,指向 Undo Log 中的上一个版本。
    • DB_ROW_ID:隐藏的自增主键(如果没有显式主键的话)。
  2. Undo Log 版本链:每次更新数据,并不会直接覆盖,而是将旧值写入 Undo Log 中,并通过 DB_ROLL_PTR 将多个历史版本串成一个链表。
  3. Read View(读视图):事务在执行查询时生成的快照。它包含当前系统中活跃(未提交)的事务 ID 列表。
    • 如果当前行的事务 ID 小于活跃列表中的最小值:说明行在查询前就提交了,可见。
    • 如果当前行的事务 ID 大于活跃列表中的最大值:说明行是在查询生成快照后才生成的,不可见。
    • 如果在活跃列表中:说明是被其他未提交事务修改的,不可见。 (不可见时,就顺着 Undo Log 版本链往下找,直到找到可见的版本)。

🌟 面试加分项(实战经验): RC 和 RR 隔离级别都能通过 MVCC 解决读写冲突,它们的根本区别在于生成 Read View 的时机不同。

  • RC 级别:每次执行 SELECT 都会生成一个新的 Read View。所以能读到其他事务最新提交的数据(导致不可重复读)。
  • RR 级别:只有在事务的第一次 SELECT 时生成 Read View,之后一直复用这个快照。所以整个事务期间看到的数据都是一致的(解决了不可重复读)。

❓ 面试官:说一下 MySQL 的锁机制吧,什么是行锁、表锁、间隙锁? ​

频率:🔥🔥🔥🔥

💡 一句话总结(先抛结论): MySQL 锁按粒度分为表锁、行锁和全局锁。InnoDB 默认使用行锁。行锁又分为记录锁(Record Lock)、间隙锁(Gap Lock)和临键锁(Next-Key Lock)。

📝 详细原理解析:

  1. 表级锁:
    • 开销小,加锁快,不会死锁;粒度大,并发冲突概率高。
    • 常用的有:表锁(lock tables)、元数据锁(MDL,做 DDL 时自动加)。
  2. 行级锁(InnoDB 特有):
    • 开销大,加锁慢,会出现死锁;粒度小,并发性能高。
    • 记录锁(Record Lock):仅仅锁住某一行记录(基于索引加锁)。
    • 间隙锁(Gap Lock):锁住两个索引之间的间隙,防止其他事务在这个间隙插入数据(主要为了解决 RR 级别的幻读问题)。
    • 临键锁(Next-Key Lock):记录锁 + 间隙锁的组合,锁住一段左开右闭的区间。

🌟 面试加分项(实战经验): 在面试中如果你能点出 "InnoDB 的行锁是加在索引上的,而不是加在数据行上的",面试官会对你刮目相看。 如果 UPDATE 语句的 WHERE 条件没有走索引,InnoDB 就会退化成全表扫描,把所有记录和间隙都锁住(相当于表锁),这在线上极其危险,很容易引发大面积的锁等待甚至雪崩。我们在执行批量 Update 或 Delete 时,一定会用 EXPLAIN 检查是否走了索引。


❓ 面试官:产生死锁的原因是什么?如何排查和解决? ​

频率:🔥🔥🔥🔥

💡 一句话总结(先抛结论): 死锁是指两个或多个事务互相持有对方需要的锁,并且都在等待对方释放,导致死循环。解决死锁主要靠应用层优化加锁顺序,以及 MySQL 自身的死锁检测机制。

📝 详细原理解析:产生死锁的典型场景:

  • 事务 A 锁住了记录 1,去请求记录 2。
  • 同时,事务 B 锁住了记录 2,去请求记录 1。

如何排查:

  1. 使用 SHOW ENGINE INNODB STATUS 查看最近一次死锁的详细信息(LATEST DETECTED DEADLOCK)。
  2. 查询 information_schema.innodb_locks 和 innodb_lock_waits(MySQL 8.0 之后被 data_locks 替代)查看当前持有的锁。

如何解决/预防:

  1. 固定加锁顺序:不同业务在操作同一批表或行时,尽量约定以相同的顺序进行加锁。
  2. 大事务拆小:事务越长,占用锁的时间越长,冲突概率越大。尽量在业务层将非核心写操作移出事务。
  3. 降低隔离级别:在允许幻读的业务中,把 RR 降级为 RC,因为 RC 级别没有间隙锁,能大幅减少死锁。
  4. 添加合理索引:避免更新操作因没有索引而退化为表锁。

🌟 面试加分项(实战经验): 我曾经处理过一个电商订单支付的死锁 Bug:用户下单时扣减库存(锁库存表),再创建支付流水(锁流水表);而退款时先更新流水(锁流水表),再恢复库存(锁库存表)。这就因为操作顺序不一致导致了高并发下的死锁。最终我们把退款的逻辑顺序调整为先恢复库存再更新流水,彻底解决了这个问题。