一、核心结论

不会重复! MySQL通过AUTO_INCREMENT锁机制原子性的ID分配算法,确保即使在极高并发场景下,自增主键也绝对不会产生重复值。

但需要注意:

  • ✅ 自增ID保证唯一性(不重复)
  • ❌ 但不保证连续性(可能存在空洞)
  • ⚠️ 不同锁模式会影响并发性能

二、实现原理

1. 自增锁机制(AUTO-INC Lock)

MySQL使用表级AUTO-INC锁保护自增计数器:

CREATE TABLE orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    amount DECIMAL(10,2)
);

-- 高并发插入
INSERT INTO orders (user_id, amount) VALUES (1001, 99.99);

加锁流程

事务A: INSERT orders
  ↓
  1. 获取 AUTO-INC Lock(表锁)
  2. 读取当前 AUTO_INCREMENT 值:100
  3. 分配给当前插入:id = 100
  4. 更新计数器:AUTO_INCREMENT = 101
  5. 释放 AUTO-INC Lock
  ↓
  6. 继续执行插入操作(行级锁)

关键点

  • 锁作用时间极短(仅计数器操作期间)
  • 锁的粒度是表级,但持有时间短(微秒级)
  • 分配完ID后立即释放锁,不影响后续插入

2. 三种自增锁模式

MySQL提供参数 innodb_autoinc_lock_mode 控制锁策略:

模式0:传统锁模式(Traditional)

SET GLOBAL innodb_autoinc_lock_mode = 0;

特点

  • 所有INSERT都使用表级AUTO-INC Lock
  • 持有锁直到语句执行完成
  • 并发性能最差,但主从复制最安全

适用场景:使用Statement-Based Replication(SBR)


模式1:连续锁模式(Consecutive,默认)

SET GLOBAL innodb_autoinc_lock_mode = 1;

特点

  • Simple INSERT(可预先确定行数):使用轻量级Mutex
    INSERT INTO t VALUES (...);              -- 使用Mutex
    INSERT INTO t VALUES (...), (...), (...); -- 使用Mutex
    
  • Bulk INSERT(无法预先确定行数):使用传统AUTO-INC Lock
    INSERT INTO t SELECT ...;  -- 使用AUTO-INC Lock
    LOAD DATA ...;             -- 使用AUTO-INC Lock
    

性能对比

// 测试代码
@Test
public void testInsertPerformance() {
    long start = System.currentTimeMillis();
    
    // 方式1:Simple INSERT (Mutex)
    for (int i = 0; i < 10000; i++) {
        jdbcTemplate.update("INSERT INTO orders (user_id, amount) VALUES (?, ?)",
            i, 100.0);
    }
    // 耗时:约 2000ms
    
    // 方式2:Batch INSERT (Mutex)
    jdbcTemplate.batchUpdate("INSERT INTO orders (user_id, amount) VALUES (?, ?)",
        batchArgs);
    // 耗时:约 300ms
    
    // 方式3:INSERT SELECT (AUTO-INC Lock)
    jdbcTemplate.update("INSERT INTO orders (user_id, amount) SELECT user_id, 100 FROM users");
    // 耗时:约 5000ms(锁持有时间长)
}

模式2:交错锁模式(Interleaved,最高性能)

SET GLOBAL innodb_autoinc_lock_mode = 2;

特点

  • 所有INSERT都使用轻量级Mutex
  • 并发性能最高
  • 不保证主从复制的ID一致性(Statement模式下)

ID分配示例

线程1: INSERT 3 rows → 分配 ID: 1, 2, 3
线程2: INSERT 2 rows → 分配 ID: 4, 5
线程3: INSERT 5 rows → 分配 ID: 6, 7, 8, 9, 10

-- 实际插入顺序可能是:
实际顺序:线程2(4,5) → 线程1(1,2,3) → 线程3(6,7,8,9,10)

注意:ID是连续分配的,但插入顺序可能交错


3. 锁模式对比表

锁模式 Simple INSERT Bulk INSERT 并发性能 主从一致性 推荐场景
0 (Traditional) AUTO-INC Lock AUTO-INC Lock ✅ 完全保证 SBR复制
1 (Consecutive) Mutex AUTO-INC Lock ⭐⭐⭐ ✅ 完全保证 默认推荐
2 (Interleaved) Mutex Mutex ⭐⭐⭐⭐⭐ ❌ SBR不保证 RBR复制/单机

三、并发场景验证

实验1:1000并发插入验证唯一性

@Test
public void testConcurrentInsert() throws Exception {
    int threadCount = 1000;
    CountDownLatch latch = new CountDownLatch(threadCount);
    ExecutorService executor = Executors.newFixedThreadPool(100);
    
    Set<Long> insertedIds = ConcurrentHashMap.newKeySet();
    
    for (int i = 0; i < threadCount; i++) {
        executor.submit(() -> {
            try {
                // 插入并获取自增ID
                KeyHolder keyHolder = new GeneratedKeyHolder();
                jdbcTemplate.update(connection -> {
                    PreparedStatement ps = connection.prepareStatement(
                        "INSERT INTO orders (user_id, amount) VALUES (?, ?)",
                        Statement.RETURN_GENERATED_KEYS
                    );
                    ps.setInt(1, 1001);
                    ps.setDouble(2, 99.99);
                    return ps;
                }, keyHolder);
                
                Long id = keyHolder.getKey().longValue();
                insertedIds.add(id);
            } finally {
                latch.countDown();
            }
        });
    }
    
    latch.await();
    executor.shutdown();
    
    // 验证
    assertEquals(1000, insertedIds.size()); // ✅ 无重复
    System.out.println("所有ID唯一,无重复!");
}

结果

  • ✅ 1000个并发请求产生1000个唯一ID
  • ✅ 无任何重复
  • ⚠️ ID可能不连续(如:1,2,4,5,7…)

实验2:观察自增计数器更新

-- 查看当前自增值
SELECT AUTO_INCREMENT 
FROM information_schema.TABLES 
WHERE TABLE_SCHEMA = 'test' AND TABLE_NAME = 'orders';
-- 结果:1001

-- 并发插入10000条
-- ...

-- 再次查看
SELECT AUTO_INCREMENT FROM information_schema.TABLES ...;
-- 结果:11001(增加了10000)

四、源码级原理(简化)

InnoDB自增ID分配流程

// ha_innobase::get_auto_increment
void get_auto_increment(ulonglong offset, ulonglong increment,
                        ulonglong nb_desired_values,
                        ulonglong *first_value,
                        ulonglong *nb_reserved_values) {
    
    // 1. 根据锁模式选择锁类型
    if (autoinc_lock_mode == AUTOINC_OLD_STYLE_LOCKING) {
        // 模式0:获取表级AUTO-INC Lock
        lock_table_autoinc(LOCK_AUTO_INC);
    } else {
        // 模式1/2:获取轻量级Mutex
        mutex_enter(&autoinc_mutex);
    }
    
    // 2. 原子性读取并更新计数器
    current_value = dict_table_autoinc_read();
    next_value = current_value + nb_desired_values;
    
    // 3. 检查是否超过最大值
    if (next_value < current_value) {
        // 溢出处理
        return HA_ERR_AUTOINC_ERANGE;
    }
    
    // 4. 更新计数器
    dict_table_autoinc_update_if_greater(next_value);
    
    // 5. 释放锁
    if (autoinc_lock_mode == AUTOINC_OLD_STYLE_LOCKING) {
        lock_table_autoinc_unlock();
    } else {
        mutex_exit(&autoinc_mutex);
    }
    
    // 6. 返回分配的ID范围
    *first_value = current_value;
    *nb_reserved_values = nb_desired_values;
}

关键机制

  • 原子操作:读取和更新在锁保护下完成
  • 预分配:批量插入时一次性分配多个ID
  • 内存缓存:计数器保存在内存中(dict_table_t->autoinc

自增值持久化时机

// 写入到磁盘的时机
1. 表关闭时(FLUSH TABLES
2. 服务器正常关闭时
3. 表定义变更时(ALTER TABLE

// MySQL 8.0新特性:自增值持久化到Redo Log
// 避免重启后自增值回退问题

五、常见问题

Q1:自增ID回滚后会重用吗?

BEGIN;
INSERT INTO orders (user_id, amount) VALUES (1001, 99.99); -- 获得ID=100
ROLLBACK;

-- 下一次插入
INSERT INTO orders (user_id, amount) VALUES (1002, 88.88); -- 获得ID=101(不是100)

答案:不会重用。已分配的ID不会回收,即使事务回滚。

Q2:主从复制会导致ID不一致吗?

Statement模式 + innodb_autoinc_lock_mode=2 的问题

-- 主库执行顺序
INSERT INTO t VALUES (...); -- 分配ID=1
INSERT INTO t VALUES (...); -- 分配ID=2

-- Binlog记录(Statement格式)
INSERT INTO t VALUES (...);
INSERT INTO t VALUES (...);

-- 从库回放时,如果有并发可能导致
-- ID分配顺序变为 2, 1(不一致!)

解决方案

  1. 使用 Row-Based Replication (RBR)(推荐)
  2. 或设置 innodb_autoinc_lock_mode = 1

六、性能优化建议

1. 选择合适的锁模式

-- MySQL 8.0默认使用RBR,可安全开启模式2
SET GLOBAL innodb_autoinc_lock_mode = 2;
SET GLOBAL binlog_format = 'ROW';

2. 批量插入优化

// 差:逐条插入(每次都获取锁)
for (Order order : orders) {
    jdbcTemplate.update("INSERT INTO orders ...", order);
}

// 优:批量插入(一次获取锁,分配多个ID)
jdbcTemplate.batchUpdate("INSERT INTO orders ...", batchArgs);

3. 避免大事务持有自增锁

-- 差:大事务中多次插入
BEGIN;
INSERT INTO orders ...;  -- 获取ID
-- ... 长时间业务逻辑 ...
INSERT INTO orders ...;  -- 再次获取ID
COMMIT;

-- 优:缩小事务范围
INSERT INTO orders ...;  -- 获取ID
-- ... 业务逻辑 ...
BEGIN;
UPDATE orders SET status = 1 WHERE id = ?;
COMMIT;

七、答题总结

面试回答框架

  1. 结论先行
    “高并发下自增主键绝对不会重复,MySQL通过AUTO-INC锁机制保证ID分配的原子性”

  2. 核心机制
    “InnoDB使用表级轻量锁或Mutex保护自增计数器,分配ID的过程是原子操作:读取当前值 → 分配 → 更新计数器,整个过程在锁保护下完成”

  3. 性能优化
    “MySQL 5.7+默认使用innodb_autoinc_lock_mode=1(连续模式),Simple INSERT使用Mutex性能更高;若使用Row-Based复制,可开启模式2获得最佳性能”

  4. 注意事项
    “需要注意ID不保证连续性,事务回滚、批量插入失败都可能产生空洞;另外主从复制场景下需配合合适的binlog格式”

关键点

  • 理解三种自增锁模式的差异
  • 掌握ID分配的原子性保证
  • 知道不同场景下的性能优化方法
  • 能说明主从复制的兼容性问题