[面试题] MySQL 是如何保证唯一性索引的唯一性的?
📖 版块 A:知识讲解版 (温故知新)
- 底层数据结构支撑:MySQL 的 InnoDB 存储引擎使用 B+Tree 作为索引结构。当定义了唯一索引(Unique Index)时,B+Tree 的叶子节点不仅存储数据,还维护了键值的唯一约束。
- 插入前的检查机制:在执行插入操作(INSERT)之前,InnoDB 必须先检查该记录是否存在。它会先通过 B+Tree 查找到对应的位置,如果发现该位置已经存在相同的值,则报错
Duplicate entry,中止插入。 - Change Buffer 的特殊处理:这是唯一索引与普通索引最大的区别点。普通索引在插入时,如果数据页不在内存中,可以先记录在 Change Buffer 中,异步合并。但唯一索引必须立刻进行唯一性检查,因此它必须将数据页读入内存进行校验,无法利用 Change Buffer。
- 并发控制(锁机制):在多个事务并发插入时,InnoDB 使用 Next-Key Locks(记录锁+间隙锁) 来确保唯一性。如果一个事务正在插入某个值,其他事务在插入相同值或相邻范围时会被阻塞,从而避免幻读导致重复数据的写入。
🎤 版块 B:面试实战版 (高手逐字稿)
Q:MySQL 是如何保证唯一性索引的唯一性的?
💬 高手逐字稿:
关于 MySQL 保证唯一性索引的机制,我想从底层存储流程和并发一致性两个维度来拆解。
首先,从底层操作流程来看,唯一索引的校验是“同步且强制”的。在 InnoDB 引擎中,每当我们要插入一条带唯一索引的记录时,引擎必须先在 B+Tree 中定位这个值。如果定位到该值已存在,直接返回错误。这里有一个非常关键的细节:普通索引在更新时,如果数据页不在内存里,可以先写在 Change Buffer 中以减少磁盘 IO;但唯一索引不行,因为它必须把数据页从磁盘加载到内存里,亲眼确认这个值真的不存在,才能执行插入。所以从性能上讲,唯一索引的写操作开销通常比普通索引大。
其次,从并发场景来看,为了防止两个事务同时看到“没有这个值”而同时插入,MySQL 引入了锁机制。当一个事务执行插入前,它会对目标位置加一个 Next-Key Lock(即记录锁和间隙锁的组合)。如果两个事务并发插入相同的值,第二个事务在尝试获取锁时会被阻塞,直到第一个事务提交。如果是通过 INSERT INTO ... ON DUPLICATE KEY UPDATE 这种语句,InnoDB 甚至会升级锁级别来确保安全性。
这里其实涉及到一个容易被忽略的细节:即便我们在业务层做了查询检查,也不能替代数据库的唯一索引。
因为在高并发下,业务层的“先查后写”是非原子性的,存在 Time-of-check to time-of-use(检查与使用的间隙)风险。而 MySQL 将唯一性检查放在引擎层,配合 B+Tree 的有序查找和行级锁,在原子性层面彻底封死了重复数据的可能。
所以在架构设计时,我通常建议对于具备业务唯一属性的字段,必须在数据库层面建立唯一索引作为最后一道防线。虽然这会牺牲掉 Change Buffer 带来的写性能收益,但它是保证数据一致性的硬性要求。