结论先行:你描述的「跳号」在 InnoDB 自增机制下大概率是正常行为,不是 bug;ON DUPLICATE KEY UPDATE 会消耗自增值,这是根因之一。下面分点说。
1) ON DUPLICATE KEY UPDATE 命中主键时是否消耗自增值?
会消耗。 这是关键点。InnoDB 在语句执行前就会为 INSERT 部分分配自增值,逻辑上「先申请号、再判断是否冲突」。具体到 lock_mode=1(consecutive):
- 单行 INSERT:InnoDB 为该语句申请 1 个自增值(即使最终走了 UPDATE 分支,号已经分配出去了)。
- 多行 INSERT:InnoDB 一次性预分配 N 个号(N=行数),即使部分行命中主键走 UPDATE,预分配的号也不会回收。
- 命中主键后,被「浪费」的自增值就表现为跳号。
所以你看到的 3 万次跳号,与「20% 流量走 INSERT ... ON DUPLICATE KEY UPDATE」是高度自洽的——这部分流量即使最终没产生新行,也消耗了自增值。
2) 怎么直接观测自增分配?
MySQL 原生没有「每次自增分配」级别的钩子,但有几条路可以走:
- performance_schema.events_statements_summary_by_digest:按 SQL 指纹聚合,能看到
INSERT ... ON DUPLICATE KEY UPDATE的执行次数与影响行数对比。如果「执行次数 > 影响行数」,差值就是被 ODKU 浪费的自增值。 - general log(谨慎,生产慎用):能逐条看到 SQL,但 8000 万行表的 QPS 下 general log 会拖垮磁盘 IO,建议只在短窗口内开。
- 审计插件(如 MySQL Enterprise Audit、Percona Audit Plugin):能记录每条 SQL 的上下文,包括 ODKU 的命中情况。这是最干净的方案,但需要装插件。
- binlog 反查(你已经做了):binlog 只记录「实际写入」的行,不记录「被 ODKU 浪费」的自增值,所以 binlog 反查看不到跳号——这是预期行为,不是你漏看了。
3) 跳号是否正常?
正常。 InnoDB 自增主键的设计契约就是「单调递增、不保证连续」,官方文档明确说了这一点。跳号的合法来源包括:
- 事务回滚(已分配未提交的自增值不回收);
- INSERT ... ON DUPLICATE KEY UPDATE 命中主键(号已分配但行未插入);
- 多行 INSERT 部分行失败(预分配的号不回收);
- 批量插入预分配(lock_mode=0/1 都会预分配);
- 显式指定主键值插入(跳过的号不回收)。
只有当业务把「id 连续」当作硬约束(比如用 id 差值做增量同步)时,跳号才需要当作 bug 处理。 否则,这是 InnoDB 的正常行为,不需要修。
可执行建议(按优先级):
- 先确认业务是否依赖 id 连续。如果依赖,建议改用
updated_at+ 主键做增量同步,而不是 id 差值——这是行业通用做法,能彻底解耦。 - 量化 ODKU 浪费。跑一条
SELECT COUNT(*) FROM performance_schema.events_statements_summary_by_digest WHERE DIGEST_TEXT LIKE '%INSERT%ON DUPLICATE KEY UPDATE%',对比SUM(COUNT_STAR)与SUM(SUM_ROWS_AFFECTED),差值就是被浪费的自增值。如果差值 ≈ 3 万,根因锁定。 - 如果业务不依赖 id 连续,不用修。把「自增主键可能跳号」写进团队的 DBA 文档,避免下次再被当成故障。
- 如果跳号量级异常大(比如远超 ODKU 流量),再考虑查:是否有隐藏的回滚(比如应用层 try-catch 吞了异常)、是否有批量 INSERT 部分失败、是否有触发器(虽然你说没有,但建议
SHOW TRIGGERS再确认一次)。
一句话:先排除业务依赖,再量化 ODKU 浪费,最后才考虑是否修。90% 的概率这是正常行为,不需要动。
参考:
- MySQL 官方:AUTO_INCREMENT columns
- InnoDB 自增计数器机制:InnoDB auto-increment counters
- INSERT ... ON DUPLICATE KEY UPDATE:INSERT syntax