自增ID在分布式环境下必然冲突,因依赖单点序列;推荐UUID(存为BINARY(16)或UUID类型)或号段法(批量预分配),避免数据库内实现雪花算法。

为什么自增 ID 在分布式环境下会冲突
自增 id 依赖单点数据库的序列或 AUTO_INCREMENT,一旦分库分表或引入多个写入节点(比如读写分离的从库误开写权限、多活架构、应用层双写),INSERT 就可能生成重复 id。这不是“概率低”,而是“只要并发写入不同实例,就必然发生”。雪花算法本意是用时间戳 + 机器 ID + 序列号拼出全局唯一 long 型 ID,但直接在 SQL 层模拟它既难维护又慢——数据库不擅长位运算和系统时钟协调。
用数据库原生 UUID 函数最省事但要注意字段类型
PostgreSQL 有 gen_random_uuid()(需 pgcrypto 扩展),MySQL 8.0+ 支持 UUID_TO_BIN(UUID(), TRUE) 存为 BINARY(16) 提升索引效率。别用字符串存 UUID() 默认格式(36 字符),否则索引体积膨胀、范围查询失效。
- 必须把主键设为 BINARY(16) 或 UUID 类型(PG)
- 插入时显式调用函数:INSERT INTO orders (id, ...) VALUES (UUID_TO_BIN(UUID(), TRUE), ...)
- 不要依赖触发器或默认值自动填充,某些 ORM(如 Django)对二进制默认值支持不稳定
如果必须用数字 ID,用数据库序列 + 分段预分配更可控
放弃“每个 ID 都实时计算”,改用“批量领号”:建一张 id_generator 表,每行代表一个业务类型(如 'order')的号段起止。应用启动时或号段用尽时,用原子更新抢一个新区间:
UPDATE id_generator SET min_id = min_id + 1000, max_id = max_id + 1000 WHERE biz_type = 'order' RETURNING min_id, max_id;
- 这个操作必须加 FOR UPDATE 或靠唯一约束兜底
- 应用缓存当前号段,本地递增分配,避免每次插入都查 DB
- 号段大小按写入 QPS 估算(比如 1000 条/秒 → 段长 10000),太小导致频繁抢锁,太大导致 ID 空洞明显
时间戳前缀 + 自增不是真分布式,但能缓解单点压力
在单库内,可用 CONCAT(YEAR(NOW()), LPAD(MONTH(NOW()),2,'0'), LPAD(DAY(NOW()),2,'0'), LPAD(id,6,'0')) 拼出类似 20240520000123 的 ID。它没解决跨库冲突,但带来两个实际好处:
- 写入天然按天聚簇,历史数据归档方便
- id 字段可退化为无意义的局部自增,降低对主键连续性的心理依赖
- 注意:MySQL 中这种拼接结果是字符串,得用 CHAR(12) 或 VARCHAR(12),且无法用 INT 索引加速范围扫描
gen_random_uuid()(需 pgcrypto 扩展),MySQL 8.0+ 支持 UUID_TO_BIN(UUID(), TRUE) 存为 BINARY(16) 提升索引效率。别用字符串存 UUID() 默认格式(36 字符),否则索引体积膨胀、范围查询失效。
- 必须把主键设为 BINARY(16) 或 UUID 类型(PG)
- 插入时显式调用函数:INSERT INTO orders (id, ...) VALUES (UUID_TO_BIN(UUID(), TRUE), ...)
- 不要依赖触发器或默认值自动填充,某些 ORM(如 Django)对二进制默认值支持不稳定
如果必须用数字 ID,用数据库序列 + 分段预分配更可控
放弃“每个 ID 都实时计算”,改用“批量领号”:建一张 id_generator 表,每行代表一个业务类型(如 'order')的号段起止。应用启动时或号段用尽时,用原子更新抢一个新区间:
UPDATE id_generator SET min_id = min_id + 1000, max_id = max_id + 1000 WHERE biz_type = 'order' RETURNING min_id, max_id;
- 这个操作必须加 FOR UPDATE 或靠唯一约束兜底
- 应用缓存当前号段,本地递增分配,避免每次插入都查 DB
- 号段大小按写入 QPS 估算(比如 1000 条/秒 → 段长 10000),太小导致频繁抢锁,太大导致 ID 空洞明显
时间戳前缀 + 自增不是真分布式,但能缓解单点压力
在单库内,可用 CONCAT(YEAR(NOW()), LPAD(MONTH(NOW()),2,'0'), LPAD(DAY(NOW()),2,'0'), LPAD(id,6,'0')) 拼出类似 20240520000123 的 ID。它没解决跨库冲突,但带来两个实际好处:
- 写入天然按天聚簇,历史数据归档方便
- id 字段可退化为无意义的局部自增,降低对主键连续性的心理依赖
- 注意:MySQL 中这种拼接结果是字符串,得用 CHAR(12) 或 VARCHAR(12),且无法用 INT 索引加速范围扫描
CONCAT(YEAR(NOW()), LPAD(MONTH(NOW()),2,'0'), LPAD(DAY(NOW()),2,'0'), LPAD(id,6,'0')) 拼出类似 20240520000123 的 ID。它没解决跨库冲突,但带来两个实际好处:
- 写入天然按天聚簇,历史数据归档方便
- id 字段可退化为无意义的局部自增,降低对主键连续性的心理依赖
- 注意:MySQL 中这种拼接结果是字符串,得用 CHAR(12) 或 VARCHAR(12),且无法用 INT 索引加速范围扫描
真正需要跨库全局唯一时,UUID 或号段法是目前 SQL 层最稳妥的选择;硬要在数据库里实现雪花逻辑,会把时钟偏移、机器 ID 管理、序列重置这些运维问题拖进 SQL 脚本里,得不偿失。










