
先说一个我印象很深的事。当时我还在某家电商平台的后端团队某天凌晨快下班的时候DBA同事在群里炸了一句“线上合并分表用户ID撞了。”短短十个字整个值班群瞬间安静了几秒。群里没人敢接话因为大家都清楚撞ID意味着什么——两个不同用户的订单、地址、优惠券会互相串数据一旦写脏想恢复简直比登天还难。而罪魁祸首恰恰是当时被整个团队奉为“最省心默认选项”的自增主键。这大概就是互联网公司后端团队内部一直流传的一条“潜规则”凡是上了规模、面向用户的线上核心库表严禁使用自增主键。很多刚入行的人不理解觉得自增主键明明又短又有序性能也够好为什么公司非要立这种规矩我的看法是这条规矩不是拍脑袋定的是用生产事故和亿级别数据量换来的教训。这篇内容我就把背后的逻辑拆开讲透从分库分表、数据迁移、安全泄露到并发热点说清楚自增主键到底“死”在哪以及不用它之后主流方案又该怎么选、怎么落地。1. 自增主键为什么会被拉黑先看它到底惹了什么事1.1 从一张订单表说起自增主键最直接的三宗罪假设你负责的是一个电商系统的订单表建表时图省事主键直接写id BIGINT AUTO_INCREMENT。系统刚上线时确实爽插入不用管ID数据库自己往上加查询走主键索引也快谁看了不说一句“经典配置”。但业务跑到一定规模后问题就来了。第一宗罪数据合并直接撞车。如果你做过平台型业务就知道一个主体下往往有多个子业务线比如一个公司做商城、做外卖、做本地生活各自先跑自己的订单表。等到公司要做“会员中台”“订单中台”把多个业务线的订单拉到一个大池子里统一管理时自增ID立刻变成地雷。A线订单ID到了 100 万B线订单ID也到了 80 万两个库的数据一合并ID从 1 到 80 万全是冲突。你要么改源数据ID要么引入映射关系这个工程量和风险系数经历过一次的人绝对不想再经历第二次。第二宗罪业务信息被白嫖。自增主键天然自带“业务透视镜”属性。你今天的注册用户ID是 10001明天是 10050竞争对手只需要隔几天注册一个新账号就能算出你平台每天的增量用户数。更狠的抓你订单号今天下单的ID区间和昨天对比就能推算出日订单量。这个数据在投融资、市场对标、商家谈判中都值大钱而自增ID等于你亲手把经营数据做成明牌递给对手。第三宗罪ID可预测带来的越权风险。很多系统早期做水平权限校验不严格接口里传一个订单ID就把详情返回了。用自增ID攻击者从 1 开始往上遍历能把你所有历史订单都扒一遍。虽然这是接口鉴权的锅但自增ID等于给攻击者提供了完美的遍历序列降低了批量拉数据的成本。安全评审的时候自增ID往往会被直接标记为高风险项。1.2 一个真实的生产事故数据合并时ID撞车后的连锁反应我之前参与过一个社区产品的用户数据整合项目两个子产品合并用户体系两边都用的自增主键。开发初期大家觉得这事很简单无非就是把B产品的用户搬过来ID加一个偏移量就行。结果真做的时候发现偏移量根本定不下来。B产品线上还在持续产生新用户你今天定偏移量 100 万明天它真实用户数涨到 110 万偏移量就得重算。而且A产品自己也删过一批用户存在ID空洞偏移量加上空洞对账怎么都对不平。更麻烦的是用户ID在业务系统里不是只有一张表的事。帖子表里有user_id评论表里有user_id私信、关注关系、消息通知、操作日志全都有用户ID的外键关联。你改了用户主表的主键就得同步改几十张关联表里的user_id字段。那段时间我们跑数据订正任务跑了两天两夜跑了三个批次才把关联表的数据修正完中间还因为漏了一张日志表导致一部分用户的行为轨迹串到了另一个人身上后面又花了一周排查和修复。那次之后团队里定了一条铁律凡是核心业务表的新建表一律禁用自增主键存量表也列了改造计划逐步替换为分布式ID。所以如果你所在团队还在大规模用自增主键我建议你认真评估一下未来是否有合并、拆分、迁移的可能——只要有一条占上自增主键就是定时炸弹。2. 分库分表语境下的自增主键看似能用实则寸步难行2.1 分片算法迁移ID冲突带来的连锁反应互联网业务增长到单库单表扛不住的时候分库分表基本是必经之路。常见的分片方式有 range 分片和 hash 分片两种不管哪种你都需要一个全局唯一的ID作为业务主键。如果你一直用自增主键分库分表那一刻就傻眼了。每个分片的自增ID都从 1 开始A分片和B分片会生成相同的ID你拿什么区分这条数据属于哪个分片有人会说那我给每个分片设置不同的初始值比如分片1从 1 开始、分片2从 1 亿开始步长设为分片数这不就解决了吗理论上听着可行实际落地全是坑。分片数不是一成不变的今天 4 个分片双十一前要扩到 8 个分片扩的时候步长就得从 4 改成 8。但已经是 4 步长生成出来的ID分布是 1、5、9、13 这种间隔改成 8 步长后新ID直接撞上已有ID。要平滑扩容你必须重建整张表的数据分布。更糟的是自增ID只保证在单实例内递增扩容时跨实例迁移数据ID顺序全乱曾经依赖ID排序展示的业务查询逻辑也会跟着出幺蛾子。2.2 并发插入热点自增锁如何拖垮写性能自增主键在高并发写入场景下有一个常被忽略的瓶颈——自增锁竞争。早期的MySQL InnoDB引擎对自增列使用表级锁也就是AUTO-INC Lock插入的时候整个表只能有一个事务在申请自增ID其他插入全部排队等待。后来MySQL加入了innodb_autoinc_lock_mode参数可以调整为交错模式把表级锁降级成轻量级互斥锁性能好了一些。但问题在于如果你的复制模式还是基于语句的复制statement-based replication为了保持主从数据一致批量插入还是需要退回到表级锁。就算你用了行复制高并发下自增ID的生成依然是一个全局互斥点——所有写入线程都要抢同一把“发号锁”。对比一下如果主键是业务生成的分布式ID比如雪花ID每个应用节点各自独立生成ID数据库侧根本不存在锁竞争。这在写入密集型的场景里差别非常大尤其是秒杀、抢购、GMV对账这种瞬间写入量极大的业务自增锁的等待时间会直接影响接口的TP99。2.3 主从复制与ID空洞运维侧的隐藏坑自增ID在主从架构里还会暴露另一个问题ID空洞和主从不一致。最常见的空洞来源是事务回滚。一个事务插入了一行拿到了自增ID但事务最终回滚了这个ID就废掉了下次插入不会复用。这是正常现象但如果业务里对ID有“连续编号”的预期比如发票号、流水号就会对不上账。更隐蔽的是某些数据库运维操作比如手动插入指定ID、DELETE后重启服务、批量导入数据也会导致自增计数器被重置或跳跃。主从不一致就更麻烦。MySQL在binlog_formatstatement的情况下自增ID的生成在主库和从库上可能不一致因为从库重放的是语句而非实际ID值。一旦从库的数据和主库产生偏差读从库做报表、做数据分析、做异步搜索索引时拿到的数据就不对。排查这种问题往往要翻 binlog 对比ID序列极其费劲。所以在分库分表、主从复制、高并发写入三重压力下自增主键已经不只是“不好用”而是从根上就不适配互联网核心链路的架构模型。3. 换个角度想问题自增主键对业务和安全的杀伤力3.1 订单号和用户ID泄露被爬虫白嫖的商业机密自增ID的泄露问题不是“可能泄露”而是“必然泄露”。只要你的业务对外开放哪怕是登录后才能看到的接口爬虫用批量注册的账号就能拿到连续ID的访问方式。举一个当时在电商公司遇到的真实例子。竞品公司用一批自动注册的账号每隔一小时下一单“测试订单”然后记录订单ID。连续记录几天就能画出一条订单ID增长曲线。再结合我们平台公布的GMV增速他们基本能反推我们各时段的真实订单量。这种信息在外界看来只是“写爬虫的人真厉害”但对公司战略部门来说等于是把经营底牌交了出去。订单ID还会泄露时间信息。自增ID单调递增ID本身就是时间序列的索引。知道一个订单ID是 50000另一个是 50005虽然没有精确到秒但结合ID生成的速率模型能猜出大概的时间窗口。如果业务上还用了ID做URL参数比如https://xxx.com/order/detail/{id}搜索引擎和历史快照可能还会把旧订单页缓存下来进一步扩大了泄露面。3.2 数据合并与中台化整改历史包袱从何而来我见过很多传统企业转互联网的业务系统早期全部是自增主键。到公司决心做数据中台、做精细化管理时第一个要推翻的就是主键设计。为什么因为中台化意味着多业务线的数据要进入统一的模型。用户统一、订单统一、商品统一这是中台的最基本要求。而各业务线独立跑了好几年各自的自增主键早就形成了自己的生态。A业务的用户ID是 1 到 500 万B业务的用户ID也是 1 到 300 万C业务的用户ID还是从 1 开始。把这三个体系合并没有一个是能直接用的。处理这种历史包袱行业里通常的做法是引入一个全局唯一的“新ID”然后把旧ID作为普通业务字段保留通过映射表关联。但映射表本身也有数据量大的问题而且映射关系维护是持续的每新增一个老系统的用户就得同步新增一条映射记录。这套链路如果没设计好就成了后续所有查询的额外 JOIN 开销。可以说早期图省事选的自增主键最终都会在系统整合的时候变成加倍偿还的技术债。3.3 前端性能优化视角为什么稀疏ID让缓存失效自增ID还有一个和前端、缓存强相关的隐藏问题——ID稀疏导致的数据分布不均匀。比如一个内容平台的帖子表主键自增。用户收藏的帖子ID、浏览历史的帖子ID在缓存层如果用ID做 key高维度的稀疏ID会让缓存的内存效率变差。更重要的是对数据库进行按ID的 range 扫描时如果中间有大量空洞InnoDB 会把空洞对应的索引页也扫进来同样扫描 10000 条ID记录含空洞的表可能要多读好几倍的索引页。落实到业务上最常见的坑是分页。用WHERE id lastId ORDER BY id LIMIT 20的方式翻页如果ID空洞过多每一页实际要跳过的行数会很不稳定用户体验表现为翻页时出现内容跳跃或重复。改成分布式ID雪花ID或类雪花ID后虽然不能完全消除空洞问题但你可以通过一些ID含时间信息的方案让分页条件里加上时间维度规避空洞对翻页体验的伤害。4. 不用自增主键那用什么主流分布式ID方案全对比讲完了自增主键的问题自然要回答“替代方案”。目前行业里用得最成熟的是这么几类雪花算法、数据库号段发号器、Redis发号器、UUID/类UUID。下面逐一展开。4.1 雪花算法高并发场景的主流选择雪花算法Snowflake是分布式ID方案中的“默认答案”Twitter 提出的思路在行业里被广泛改造和应用。它的ID是一个64位的Long型通常拆成四个部分1位符号位 41位毫秒时间戳 10位工作机器ID 12位序列号。41位时间戳可以支撑约69年的时间在业务内绰绰有余。10位机器ID可以支持1024个节点独立工作。12位序列号表示同一毫秒内最多可以生成4096个ID。三者合在一起保证了“全局唯一、趋势有序、高吞吐”。我对雪花算法的评价是它是互联网核心链路最省心的选择但有几个细节必须处理。第一机器ID的分配要稳定。服务重启、扩容缩容时机器ID不能变否则可能出现重复ID。第二时钟回拨问题。如果服务器发生NTP时间回拨雪花算法可能生成比之前更小的ID甚至重复ID。解决思路是记录上次生成ID的毫秒时间如果当前时间小于上次时间就短暂拒绝请求或等待时间追上。雪花算法生成的ID是“趋势递增”的不是严格递增。做过订单分库分表的人都知道这个特性已经足够支撑分页和排序了别纠结“严格连续”。4.2 基于数据库号段的发号器控制权最稳的方案如果你对“全局唯一”的强一致要求特别高比如金融交易流水、账务明细雪花算法那种“靠机器时钟和随机序列”的方案会让你心里不踏实。这时候可以选数据库号段方案行业内俗称 Leaf、Segment或者叫“号段发号器”。这个方案的核心思想很简单用一个中心数据库表维护发号区间比如表里存biz_type, max_id, step。应用节点启动时向数据库申请一个号段比如[1001, 2000]拿到之后在本地内存里直接自增生成ID用完再向数据库申请下一个号段。优点很明显ID严格递增、全局唯一、不依赖机器时钟不会出现回拨问题。而且号段方案天然支持批量取号每次去数据库只申请一次网络开销固定应用侧生成ID的性能非常高。但它的代价是需要额外维护一套发号服务并且发号表是单点写入。为了保证高可用通常要部署双主或者多副本用分布式锁控制并发取号。这套基础设施的运维成本小团队不一定扛得住。我的建议是业务规模到了需要分布式ID但团队还没能力自研雪花算法改造的时候号段发号器反而是风险最低的方案因为它的逻辑简单、故障定位容易生产环境最容易兜底。4.3 UUID与类UUID方案什么时候可以选什么时候千万别用UUID方案是最常见的“看起来省事”的方案。UUID.randomUUID()一行代码生成一个128位的随机字符串比自增ID还省心而且绝对全局唯一、无法猜测。但如果你在MySQL这样的关系型数据库里用UUID做主键我劝你先想清楚两个代价。第一UUID是128位的十六进制字符串占用的存储空间比BIGINT大得多主键索引聚簇索引里的每一行都要带这个主键值二级索引更是会把主键作为“回表指针”嵌入每一条记录。数据量大以后索引体积膨胀非常恐怖查询性能会明显下降。第二UUID的随机性导致插入的数据在索引B树上的位置完全分散频繁插入会造成严重的页分裂和随机写InnoDB的缓冲池命中率也会受影响。如果你确实要用UUID类方案行业里有两个改进做法一是用UUID_TO_BIN函数把字符串转成二进制存储MySQL 8.0 支持节省空间二是使用有序UUID比如UUID v7它按时间戳排序能减少页分裂。但老实说既然已经要在MySQL里存有序ID了直接用雪花算法不是更省事吗UUID真正的适用场景是那些“不需要数据库参与生成、完全离线生成”的场景比如客户端生成的设备ID、消息轨迹ID、日志追踪ID这些地方主键性能不再关键UUID方便且唯一用起来很合适。4.4 各方案对比表格与选型原则为了更直观我做了一张对比表方便你在方案评审会上直接引用。方案全局唯一性趋势有序性性能实现复杂度典型场景自增主键单库内唯一严格递增高但并发有锁竞争极低内部工具表、日志流水雪花算法全局唯一趋势递增高应用侧生成并发无锁中核心业务主键、订单ID、用户ID数据库号段全局唯一严格递增高批量取号后本地生成高金融流水、强一致性账务UUID二进制全局唯一基本无序中索引膨胀、页分裂低客户端设备ID、日志追踪ID选型原则我个人总结为三条第一核心业务表的用户可见ID禁用自增、禁用纯UUID优先选雪花类方案因为既要防猜测又要存得住、查得快。第二强一致且对序要求极高的场景选号段方案用运维复杂度换确定性。第三非核心的内部系统、日志表、临时表自增主键可以用没人拦你别一刀切把简单问题复杂化。5. 一套可以落地的主键改造方案从存量系统到增量设计5.1 存量表改造如何平滑迁移已有自增主键如果你团队已经有大量表用了自增主键也别慌改造是可行的但要分步走千万别一口气在线换主键。第一步引入新的全局ID字段。在业务表里增加一个biz_id字段类型为 BIGINT索引单独建。这期间线上继续用旧自增ID做主键新写入的数据同时生成biz_id并落库。第二步双写与对账。所有写操作在更新旧主键数据的同时也保证biz_id被正确赋值。对存量数据写一个数据订正任务为每一条旧记录补齐biz_id。这个阶段每天要做对账确认读写两侧数据一致。第三步切读切写。业务代码把所有基于主键的查询和写入条件都改成基于biz_id数据库的主键可以不变因为InnoDB主键索引改动风险大但业务上已经完全不再依赖旧自增ID了。这一步相当于逻辑迁移。第四步可选物理重构。如果旧自增主键占用的索引空间影响了性能或者分库分表计划要求必须换物理主键可以在低峰期做一次表重建把biz_id设为主键旧自增ID退化为普通字段或直接删除。这套流程里最容易踩的坑是双写阶段的漏写和错写。我的实践经验是改造期间可以加一个监控项每次插入或更新记录时如果检测到biz_id为空立刻报警并阻塞该操作。宁可牺牲一点写入性能也不能让脏数据流下去。5.2 新建表的推荐DDL写法与业务规范很多新项目从零开始建表规范可以直接定好省得未来填坑。我推荐的核心表 DDL 大概是这样的CREATE TABLE order_info ( id BIGINT NOT NULL COMMENT 业务订单ID由分布式ID生成器产生, user_id BIGINT NOT NULL COMMENT 用户ID同样使用分布式ID生成器, order_no VARCHAR(64) NOT NULL COMMENT 业务订单号, amount DECIMAL(12,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;从这个DDL可以看到几个设计要点主键id不再加AUTO_INCREMENT由应用层负责生成并传入字符串型业务流水号走唯一索引而不是主键对高频查询条件建独立索引而不是依赖主键排序。团队内部还可以约定分布式ID生成器只允许统一封装在基础库的ID生成组件里业务代码严禁自己 new 雪花算法实现避免每个团队一套ID规则、排查问题的时候鸡同鸭讲。5.3 配套的代码规范与监控告警建议光改数据库不够代码层和应用基础设施也得跟上。ID生成器必须做降级演练。如果发号服务挂了应用是阻塞等待还是降级为“临时生成UUID并标记”我的建议是核心链路必须有降级预案比如设置发号超时时间超时后使用备用发号通道或者本地内存缓存号段确保写入链路不中断。监控方面需要重点关注三个指标ID生成延迟、ID重复率、时钟偏差。ID生成延迟直接反映发号服务健康度ID重复率理论上必须是0一旦出现非0立刻进P0故障处理时钟偏差监控针对雪花算法服务器每次NTP校时偏差超过阈值要告警。另外一个容易被忽视的点是测试环境。很多公司测试环境的表也沿用了生产的主键策略导致联调时经常出现ID冲突。建议测试环境统一使用一个独立的发号服务实例或者干脆用时间戳随机数拼接的测试专用ID别和生产共用同一套ID规则否则测试数据污染生产数据的问题防不胜防。6. 常见问题与我的实操体验6.1 常见争议自增主键在某些场景下真的完全不能碰吗每次聊到这个话题总有人问那我公司体量小、表数据量只有几十万用自增主键也不行吗我的回答是技术上能用管理上要谨慎。“严禁使用自增主键”主要是针对互联网公司面向用户的在线交易、在线互动这类核心链路因为这些系统的数据天然会有合并、迁移、拆分、防爬取等需求。如果你的表是内部系统的操作日志、后台管理端的字典表、一次性数据导入的暂存表用自增主键完全没问题毕竟自增主键的写入效率和索引紧凑度确实是实打实的优势。但有一个例外请你注意只要这张表未来存在“对外暴露ID”的可能或者业务上要和其他系统的数据做关联整合建议直接上分布式ID别赌。6.2 踩坑记录ID设计不当引发的三个故障我职业生涯里踩过三个和主键设计直接相关的大坑分享出来供你参考。第一个是数据合并的ID撞车前面讲过了那次事故让团队加了两周班。第二个是分库分表后使用“步长自增”扩容扩分片时ID冲突最后靠临时停机维护重建了数据分布。第三个是测试环境和生产环境用了相同的雪花算法机器ID导致从测试库导入生产库的部分数据ID重复引发了用户端数据展示错乱。后面两个问题都是典型的“基础设计缺规范”导致的如果从一开始就统一发号服务和环境隔离完全可以避免。6.3 关于主键设计我最后的经验心得做后端这些年我越来越觉得主键设计不是单纯的数据库性能问题而是数据生命周期管理问题。一张表的数据未来会怎么增长、怎么流转、怎么被外部访问决定了你今天该选什么主键策略。单纯因为“用起来顺手”就默认自增主键等于把未来所有麻烦都提前欠下了。如果你现在正在设计新系统的库表结构或者正准备对老系统做主键改造我觉得最值得先做的事是把你自己系统未来一年内可能发生的合并、迁移、分片、扩容、对外开放数据接入这些事列一个清单再对照这篇内容里的方案选型基本不会走偏。分布式ID的上手成本没有想象中高从雪花算法的改造做起配合号段方案兜底你的核心链路就再也不用半夜被ID撞车的电话叫醒了。