先聊一个很现实的场景:优惠券秒杀。活动开始那一瞬间,成千上万的用户同时点进来,系统要做的第一件事不是减库存,而是给每一笔请求生成一个唯一ID。这个ID会贯穿下单、扣券、对账、物流、统计全链路,一旦重复或者生成太慢,后面的所有环节都会跟着遭殃。全局唯一ID听起来是个老话题,但放在秒杀这种高并发、高实时、还要兼顾安全性的场景里,绝对不是一个UUID能糊弄过去的事。
这篇文章就把我在优惠券秒杀项目里做全局唯一ID的完整思路、方案对比、代码实现和踩坑经验都拆开讲一遍。适合正在做电商、营销系统、会员体系,或者准备设计分布式发号器的读者,尤其是那种活动频率高、峰值流量大的业务,看完至少能帮你少走两三个月的弯路。
1. 先别急着写代码,把“ID要长什么样”想清楚
1.1 优惠券秒杀场景的业务背景:为什么普通方案会翻车
我先描述一下典型的优惠券秒杀流程,你就能理解为什么ID会成为一个独立的技术专题。用户在活动页看到一张有限的5元优惠券,点击“立即抢购”,后端要生成一个唯一业务单据号,这个单据号既代表一次抢券行为,也要作为后续所有操作的关联主键。它会被发到MQ里做异步建券,会被写到订单表、流水表、用户券包里,甚至对外返回给前端做展示。
问题在于“瞬时并发”。秒杀的流量不是均匀分布的,而是前面几秒或者几十秒内打进来一个尖峰,可能是平时的百倍甚至千倍。这时候如果能用数据库自增ID,MySQL自增主键在写入压力大的时候会变成明显的瓶颈,每次插入都要争抢锁和序号;如果用UUID,单机生成倒是不冲突,但无序的字符串写进InnoDB的B+树,会导致页分裂、随机IO飙升,整个库的写入吞吐量都跟着崩。所以秒杀场景下的ID方案,不是“能不能唯一”的问题,而是“在极端并发下稳不稳、快不快”的问题。
另外还有一个被很多人忽略的点:安全性。如果ID是连续自增的,竞争对手完全可以拿两个订单号做差值,然后知道你的整体单量,这种信息泄露在营销活动里属于低级事故。优惠券秒杀的单据ID最好不可猜测、不可遍历,要带一点随机性。
1.2 全局唯一ID必须满足的五个硬性条件
我把自己在项目里整理出来的需求清单贴出来,这既是设计目标,也是后面选型时的评判标准。
第一是全局唯一。这个不用多说,它是底线。不管是单机还是多实例部署,任意时刻生成的ID都不能重复,否则订单覆盖、券包覆盖、对账失败都会出现。
第二是趋势递增。数据库索引要友好,尤其当ID被用作主键或聚簇索引时,趋势递增的ID能让插入操作分散或者顺序化,减少页分裂和随机写,这对MySQL、TiDB等存储引擎非常重要。完全递增不是必须,趋势递增基本够用。
第三是高性能、低延迟。你需要它快到什么程度?我自己的经验是,单机QPS至少十万级别,单次生成耗时在1毫秒以内。秒杀场景下,发号器如果成为链路瓶颈,一秒钟也就处理几万请求,后面的系统再快也白搭。
第四是高可用。这是很容易忽视的。发号器如果宕机,整个秒杀入口就等于关闭。所以方案必须有降级路径,不能把单点当成唯一依赖。
第五是不可猜测和可反解。活动类业务需要防止黄牛或爬虫通过遍历订单号来刷接口、拉数据,所以ID里最好混入时间信息、实例标识和随机因子。反过来,运维排查时要能从ID里快速看出是哪台机器、哪个时间窗口生成的,这就是可反解。
1.3 一个反面案例:直接用数据库自增主键为什么扛不住
我确实见过有人这么干:订单表主键直接设置AUTO_INCREMENT,秒杀一开始,数据库连接先被打满,然后主键生成排队。为什么扛不住?因为自增主键的生成依赖数据库下一次写入动作,写入本身要经过连接获取、SQL解析、行锁竞争、事务提交,这个链路在高并发下被成百上千个线程同时压着,自然会排队。
就算你单独建了一张ID序列表,用UPDATE id_table SET value = LAST_INSERT_ID(value + 1)这种方式取号,每取一次也是一次数据库写入,性能天花板就在那里,实测单库也就几千到一万出头每秒,这离秒杀需求差还有数量级差距。而且,Id自增意味着订单号可以被遍历,后台上随便改个参数就能把别人所有的订单都刷出来,这在营销活动里是非常危险的事。
所以从结论上讲,高并发秒杀场景不适合让数据库直接参与实时发号。数据库更合适的角色是“兜底”,要么做号段分配,要么做最终一致性校验,而不是每单都去依赖它。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 主流的全局唯一ID方案,到底该怎么选
2.1 UUID与雪花算法:两种极端思路对比
UUID可能是很多人第一反应想到的方案。它靠算法保证全局唯一性,没有中心节点,生成本地完成,性能也高。但如果你是做电商系统的,我强烈不建议把UUID直接当订单号或主键。一个UUID是36个字符的字符串,包含字母和连字符,存到数据库里占用空间大,索引长度大,而且完全无序。你想象一下,一张千万级订单表,主键是乱序的UUID,InnoDB的聚簇索引在插入时会频繁触发页分裂,数据页的利用率下降,随机IO几乎能把磁盘打穿。所以UUID更多用于本地链路中的分布式唯一标识,或者非核心表的主键,而不是订单号这种热点、对索引敏感的字段。
雪花算法则是另一种思路,Snowflake的核心是用一个64位的long型整数做ID。标准雪花算法把64位拆成1位符号位 + 41位毫秒级时间戳 + 10位机器码 + 12位序列号。机器码保证实例维度不
