549 字
1 分钟
为什么 MySQL 自增主键不单调也不连续
MySQL 的自增主键(AUTO_INCREMENT)看起来应该生成单调递增、连续的唯一 ID,但实际观察发现:
SELECT * FROM users;+----+-------+| id | name |+----+-------+| 1 | Alice || 2 | Bob || 4 | Carol | -- 跳过了 3| 5 | Dave |+----+-------+
INSERT INTO users (name) VALUES ('Eve'); -- 新增SELECT * FROM users;+----+-------+| id | name |+----+-------+| 1 | Alice || 2 | Bob || 4 | Carol || 5 | Dave || 3 | Eve | -- Eve 的 ID 比 Dave 小!+----+-------+这是为什么?
一、自增主键的工作原理
1.1 InnoDB 自增锁机制
flowchart LR
subgraph 事务 A
T1[开始事务]
T1 --> I1[INSERT user]
end
subgraph 事务 B
T2[开始事务]
T2 --> I2[INSERT user]
end
subgraph InnoDB
L[自增锁]
end
I1 -->|申请| L
I2 -->|等待| L
1.2 自增锁的类型
| 模式 | 说明 |
|---|---|
| 传统模式 | 表级锁,完成 INSERT 后释放 |
| 连续模式 | 轻量级锁,语句结束后释放 |
| 交叉模式 | 混合使用,性能和一致性平衡 |
-- 查看当前自增锁模式SHOW VARIABLES LIKE 'innodb_autoinc_lock_mode';-- value: 2 (交叉模式)二、自增主键不连续的原因
2.1 事务回滚
-- 事务 ABEGIN;INSERT INTO users (name) VALUES ('Alice'); -- id = 1ROLLBACK; -- 回滚,id=1 被释放
-- 事务 B(独立事务)INSERT INTO users (name) VALUES ('Bob'); -- id = 2SELECT * FROM users;+----+-------+| id | name |+----+-------+| 2 | Bob | -- id=1 被回滚,id 不连续+----+-------+原因:自增 ID 在 INSERT 时就分配了,回滚后不会回收。
2.2 批量插入
-- 批量插入多条INSERT INTO users (name) VALUES ('A'), ('B'), ('C'), ('D'), ('E');
SELECT * FROM users;+----+-------+| id | name |+----+-------+| 1 | A || 2 | B || 3 | C || 4 | D || 5 | E |+----+-------+批量插入时,ID 预分配给每行,可能因为某些行失败而跳跃。
2.3 插入失败
-- 部分插入失败INSERT IGNORE INTO users (id, name) VALUES (1, 'A'), -- 重复 id,失败 (6, 'B'); -- 只有这条成功
SELECT * FROM users;+----+-------+| id | name |+----+-------+| 1 | A || 6 | B | -- id=2,3,4,5 被跳过+----+-------+三、自增主键不单调的原因
3.1 并发插入
-- 事务 ABEGIN;INSERT INTO users (name) VALUES ('Alice'); -- 分配 id=1-- 事务 A 还没有提交
-- 事务 B(在另一个连接)BEGIN;INSERT INTO users (name) VALUES ('Bob'); -- 分配 id=2COMMIT;
-- 事务 ACOMMIT; -- 提交SELECT * FROM users;+----+-------+| id | name |+----+-------+| 2 | Bob | -- Bob 先提交,id=2 先可见| 1 | Alice | -- Alice 后提交,id=1 后可见+----+-------+现象:后提交的事务先可见,导致 id 看起来不单调。
3.2 自增计数器的刷出时机
-- innodb_autoinc_lock_mode = 2 (交叉模式)-- 自增值在语句开始前就确定了# 伪代码:自增 ID 分配def allocate_ids(count): # 先获取当前计数器值 current = innodb_counter.get() # 增加到 current + count innodb_counter.set(current + count) # 返回当前值到 current + count - 1 return range(current, current + count)四、为什么这样设计?
4.1 性能优先
| 设计选择 | 原因 |
|---|---|
| 预分配 ID | 避免每次插入都更新计数器 |
| 语句级分配 | 并发插入可以重叠 |
| 不回收 ID | 简化实现,避免锁竞争 |
4.2 唯一性保证
flowchart LR
T1[事务 1] -->|INSERT| I1[id=1]
T2[事务 2] -->|INSERT| I2[id=2]
I1 -->|COMMIT| U[唯一 ID]
I2 -->|COMMIT| U
style U fill:#9f9
核心:保证唯一性,不保证连续性或单调性。
五、如何获取连续 ID?
5.1 使用 Sequence 表
-- 创建序列表CREATE TABLE sequence ( name VARCHAR(50) PRIMARY KEY, current_value BIGINT NOT NULL);
-- 获取下一个值(事务安全)CREATE FUNCTION next_val(seq_name VARCHAR(50))RETURNS BIGINTBEGIN UPDATE sequence SET current_value = current_value + 1 WHERE name = seq_name; RETURN (SELECT current_value FROM sequence WHERE name = seq_name);END;5.2 使用分布式 ID 生成器
# 雪花算法实现class Snowflake: def __init__(self, worker_id): self.worker_id = worker_id self.sequence = 0 self.last_timestamp = -1
def generate(self): # 保证单调递增 timestamp = int(time.time() * 1000) if timestamp == self.last_timestamp: self.sequence = (self.sequence + 1) & 4095 else: self.sequence = 0 self.last_timestamp = timestamp return ((timestamp - 1288834974657) << 22) | \ (self.worker_id << 12) | self.sequence六、总结
6.1 自增主键不单调/不连续的原因
| 原因 | 说明 |
|---|---|
| 事务回滚 | ID 分配后回滚,ID 不会回收 |
| 并发插入 | 不同事务提交顺序不同 |
| 插入失败 | 部分插入失败导致 ID 跳跃 |
| 批量插入 | ID 预分配,不受单行失败影响 |
6.2 设计权衡
| 权衡 | 选择 |
|---|---|
| 性能 vs 连续性 | 选择性能,不保证连续 |
| 简单性 vs 精确 | 简化实现,依赖应用层处理 |
| 并发 vs 单调 | 允许并发,不保证提交顺序 |
核心结论:自增主键的唯一性是可靠的,但连续性和单调性不是设计目标。
参考资料
- MySQL AUTO_INCREMENT — 官方文档
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
为什么 MySQL 自增主键不单调也不连续
https://blog.souloss.com/posts/why-the-design/why-mysql-auto-increment-is-not-monotonic/ 部分信息可能已经过时
相关文章 智能推荐
1
为什么数据库不应该使用外键
技术科普 深入解析为什么现代互联网应用中不建议使用外键,以及如何替代外键实现数据一致性。
2
为什么 MySQL 使用 B+ 树
技术科普 深入解析 MySQL 为什么选择 B+ 树作为索引结构,对比 B 树、哈希表等其他数据结构,理解数据库索引设计。
3
为什么 OLAP 需要列式存储
技术科普 深入解析为什么 OLAP 数据库使用列式存储,以及它相比行式存储的优势。
4
为什么 PostgreSQL 使用 MVCC
技术科普 深入解析多版本并发控制的设计原理,理解 PostgreSQL 如何实现高并发事务处理。
5
为什么数据库会丢失数据
技术科普 深入解析数据库丢失数据的场景与原因,WAL、fsync、缓冲池等机制与数据安全。






