一、核心概念
唯一性索引(Unique Index) 用于保证列值的唯一性约束。MySQL通过插入前检查 + 行锁机制确保即使在高并发场景下,也不会出现重复值。
核心保障机制:
- 插入前唯一性检查
- Next-Key Lock防止幻读
- Insert Intention Lock优化并发
二、实现原理
1. 插入流程与锁机制
CREATE TABLE user (
id INT PRIMARY KEY,
email VARCHAR(100) UNIQUE KEY
);
-- 并发插入相同email
INSERT INTO user VALUES (1, 'test@example.com');
INSERT INTO user VALUES (2, 'test@example.com'); -- 会被阻塞/失败
完整流程(InnoDB):
阶段1:唯一性检查
1. 事务A执行 INSERT
2. 在唯一索引树上搜索 'test@example.com'
3. 如果找到记录:
- 若记录未删除 → 抛出 Duplicate key error
- 若记录标记删除 → 加 Next-Key Lock
4. 如果未找到 → 继续执行插入
阶段2:加锁协议(RC隔离级别)
事务A: INSERT 'test@example.com'
↓
对唯一索引加 【Insert Intention Lock】(插入意向锁)
↓
检查是否有冲突的锁
↓
插入记录并对新记录加 【Record Lock】
阶段3:锁冲突处理(RR隔离级别)
时刻T1: 事务A插入 'aaa@example.com'
→ 在索引区间 (NULL, 'aaa@example.com'] 加 Next-Key Lock
时刻T2: 事务B插入 'aaa@example.com'
→ 尝试获取 Insert Intention Lock
→ 与事务A的 Next-Key Lock 冲突
→ 进入等待状态
时刻T3: 事务A提交
→ 释放锁
→ 事务B获取锁并检查唯一性
→ 发现重复,抛出错误
2. 锁类型详解
| 锁类型 | 作用 | 加锁时机 |
|---|---|---|
| Record Lock | 锁定具体记录 | 检查到已存在记录时 |
| Gap Lock | 锁定索引间隙 | RR隔离级别,防止幻读 |
| Next-Key Lock | Record Lock + Gap Lock | 唯一索引查询+范围锁定 |
| Insert Intention Lock | 插入意向锁 | 插入新记录前 |
3. 不同隔离级别的行为差异
READ COMMITTED(RC)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 会话1
BEGIN;
INSERT INTO user VALUES (1, 'test@example.com');
-- 会话2(立即执行)
INSERT INTO user VALUES (2, 'test@example.com');
-- 立即返回错误:Duplicate entry 'test@example.com' for key 'email'
特点:只对存在的记录加锁,不加Gap Lock,检查更快。
REPEATABLE READ(RR)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 会话1
BEGIN;
INSERT INTO user VALUES (1, 'test@example.com');
-- 会话2(立即执行)
INSERT INTO user VALUES (2, 'test@example.com');
-- 阻塞等待,直到会话1提交或回滚
特点:加Next-Key Lock防止幻读,会话2必须等待会话1提交。
三、并发场景实战
场景1:高并发插入相同值
// 100个线程同时插入相同email
ExecutorService executor = Executors.newFixedThreadPool(100);
CountDownLatch latch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
final int userId = i;
executor.submit(() -> {
try {
// 所有线程尝试插入相同email
jdbcTemplate.update(
"INSERT INTO user (id, email) VALUES (?, ?)",
userId, "same@example.com"
);
} catch (DuplicateKeyException e) {
// 预期行为:99个失败
System.out.println("重复插入被拒绝");
} finally {
latch.countDown();
}
});
}
latch.await();
// 结果:只有1条记录插入成功
保证机制:
- 第一个事务插入成功,对该记录加Record Lock
- 后续99个事务检查时发现记录已存在,直接抛出异常
- 整个过程无需应用层额外控制
场景2:DELETE后立即INSERT
-- 会话1
BEGIN;
DELETE FROM user WHERE email = 'test@example.com';
-- 此时记录被标记删除,但锁未释放
-- 会话2
INSERT INTO user VALUES (999, 'test@example.com');
-- 会阻塞等待会话1提交
原理:
- 删除的记录依然持有锁(标记删除)
- 插入时会检查到该记录并等待锁释放
- 避免了”幻读”问题
场景3:REPLACE INTO的特殊处理
REPLACE INTO user VALUES (1, 'test@example.com');
-- 等价于:
-- 1. 检查唯一索引
-- 2. 若存在,先DELETE旧记录
-- 3. 再INSERT新记录
// 内部执行流程
public void replaceInto(User user) {
// 1. 尝试插入
try {
insert(user);
} catch (DuplicateKeyException e) {
// 2. 插入失败,执行替换逻辑
delete(user.getEmail()); // 加排他锁
insert(user); // 重新插入
}
}
四、源码级原理(简化)
InnoDB唯一性检查伪代码
// row_ins_scan_sec_index_for_duplicate
dberr_t check_duplicate_for_unique_index(index, entry) {
// 1. 在B+树中搜索目标值
cursor = btr_cur_search_to_nth_level(index, entry);
// 2. 检查是否找到相同值
if (cursor->found_record()) {
rec = cursor->get_record();
// 3. 检查记录状态
if (!rec->is_deleted()) {
// 记录存在且未删除 → 返回重复错误
return DB_DUPLICATE_KEY;
} else {
// 记录已删除 → 加Next-Key Lock防止并发问题
lock_rec_lock(LOCK_X | LOCK_ORDINARY, rec);
}
}
// 4. 未找到重复 → 加Insert Intention Lock
lock_rec_insert_intention_lock(gap_before_insert);
return DB_SUCCESS;
}
关键数据结构
// 索引记录格式
struct rec_t {
trx_id_t trx_id; // 创建该记录的事务ID
roll_ptr_t roll_ptr; // 回滚指针
byte deleted_flag; // 删除标记
byte[] key_values; // 索引键值
byte[] primary_key; // 主键值(二级索引)
};
// 锁信息
struct lock_t {
ulint type; // LOCK_REC / LOCK_TABLE
ulint mode; // LOCK_S / LOCK_X / LOCK_GAP...
hash_node_t hash; // 锁哈希表节点
trx_t* trx; // 持有锁的事务
};
五、性能优化建议
1. 选择合适的隔离级别
-- 高并发写入场景,优先使用RC
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
优势:减少Gap Lock,降低锁冲突,提升并发性能。
2. 使用批量插入减少检查次数
-- 差:逐条插入(每次都检查唯一性)
INSERT INTO user VALUES (1, 'a@example.com');
INSERT INTO user VALUES (2, 'b@example.com');
-- 优:批量插入(MySQL会优化检查)
INSERT INTO user VALUES
(1, 'a@example.com'),
(2, 'b@example.com'),
(3, 'c@example.com');
3. INSERT IGNORE避免异常处理
// 差:捕获异常性能开销大
try {
jdbcTemplate.update("INSERT INTO user VALUES (?, ?)", id, email);
} catch (DuplicateKeyException e) {
// 异常处理
}
// 优:使用INSERT IGNORE
jdbcTemplate.update("INSERT IGNORE INTO user VALUES (?, ?)", id, email);
int affectedRows = ...; // 通过影响行数判断是否插入成功
六、答题总结
面试回答模板:
-
机制概述:
“MySQL通过在B+树索引上执行插入前检查,结合InnoDB的行锁机制保证唯一性” -
核心流程:
“插入时先在唯一索引上搜索,若找到未删除记录直接返回错误;若未找到则加Insert Intention Lock后插入,并对新记录加Record Lock” -
并发保证:
“在RR隔离级别下使用Next-Key Lock防止幻读;在RC级别下仅锁定记录本身,性能更高” -
实战经验:
“生产环境建议使用RC隔离级别减少锁冲突,对于用户注册等场景可用INSERT IGNORE简化异常处理”
关键点:
- 理解锁的类型(Record/Gap/Next-Key/Insert Intention)
- 掌握不同隔离级别的行为差异
- 能说出完整的插入检查流程
- 了解性能优化方向(隔离级别、批量插入)