mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
549 字
1 分钟
为什么 MySQL 自增主键不单调也不连续
2023-07-26

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 事务回滚#

-- 事务 A
BEGIN;
INSERT INTO users (name) VALUES ('Alice'); -- id = 1
ROLLBACK; -- 回滚,id=1 被释放
-- 事务 B(独立事务)
INSERT INTO users (name) VALUES ('Bob'); -- id = 2
SELECT * 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 并发插入#

-- 事务 A
BEGIN;
INSERT INTO users (name) VALUES ('Alice'); -- 分配 id=1
-- 事务 A 还没有提交
-- 事务 B(在另一个连接)
BEGIN;
INSERT INTO users (name) VALUES ('Bob'); -- 分配 id=2
COMMIT;
-- 事务 A
COMMIT; -- 提交
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 BIGINT
BEGIN
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 自增主键不单调也不连续
https://blog.souloss.com/posts/why-the-design/why-mysql-auto-increment-is-not-monotonic/
作者
Souloss
发布于
2023-07-26
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时