不能直接用gorm的autoincrement主键,因分库分表后各物理表自增id独立从1开始,必然冲突;推荐用sony/sonyflake生成全局唯一uint64 id并显式赋值,禁用autoincrement,确保id参与分片路由(如取低12位),避免倾斜与查找失败。

为什么不能直接用 GORM 的 AutoIncrement 主键?
分库分表后,每个物理表的自增 ID 都从 1 开始,user_001 和 user_002 同时插入新记录可能都生成 ID=1,数据库层完全不感知彼此——冲突是必然的。GORM 的 AutoMigrate 和 CREATE TABLE ... AUTO_INCREMENT=1 只作用于单表,无法协调跨库、跨表的序列一致性。
常见错误现象:ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY',尤其在多实例并发写入时高频出现。
- 即使手动给每张表设不同起始值和步长(如
AUTO_INCREMENT=1, STEP=4),扩容时要重设所有表,运维成本爆炸 - GORM 不会自动读取或维护这些配置,全靠 DBA 手动执行
ALTER TABLE - MySQL 8.0+ 的
INVISIBLE COLUMN或GENERATED COLUMN也无法解决跨库唯一性问题
推荐方案:用 sony/sonyflake 生成 ID 并显式赋值
Go 生态里轻量、可靠、无依赖的分布式 ID 生成器,比自己手写 Snowflake 更省心。它返回 uint64,天然适配 GORM 的 int64 或 uint 主键字段,且默认单调递增、毫秒级有序。
关键点不是“怎么生成”,而是“怎么集成进 GORM 流程”:
- 必须在
INSERT前生成 ID,不能依赖数据库回填(GORM 的db.Create()默认会忽略已赋值的主键) - 禁用 GORM 的自动主键生成:模型定义中去掉
primaryKeytag 的 auto-increment 行为,或显式设gorm:"primaryKey;autoIncrement:false" - 生成 ID 后,直接赋值给结构体字段,再调用
db.Create()—— GORM 会把该字段当普通列插入
示例:
type User struct {
ID uint64 `gorm:"primaryKey;autoIncrement:false"`
Name string
CreatedAt time.Time
}
func CreateUser(name string) error {
sf := sonyflake.NewSonyflake(sonyflake.Settings{})
id, err := sf.NextID()
if err != nil {
return err // 注意捕获时钟回拨 panic,需降级处理
}
user := User{ID: id, Name: name}
return db.Create(&user).Error
}
为什么不用 UUID 或数据库自增服务?
UUID 字符串太长(36 字符),作为主键会导致:
- B+ 树索引深度增加,范围查询、JOIN 性能明显下降
- 外键关联时,子表存储和比较开销翻倍
- GORM 默认不支持
uuid.UUID类型主键的自动迁移(需额外注册sql.Scanner)
数据库自增服务(如单独建 id_gen 库)的问题更实际:
- 单点瓶颈:所有服务争抢同一张表的
INSERT ... SELECT LAST_INSERT_ID() - 网络延迟放大:一次写入需至少 2 次 DB 往返(取 ID + 写业务数据)
- GORM 事务无法跨库,若取 ID 成功但业务写入失败,ID 就永久浪费
而 sonyflake 是纯内存计算,吞吐量轻松过万 QPS,且支持自定义机器 ID、时间戳偏移,适配容器化部署。
GORM 分表场景下 ID 必须参与路由计算
生成全局唯一 ID 只是第一步;真正落地时,这个 ID 还得决定数据落到哪个库、哪张表。否则 ID 唯一了,但查询时找不到位置,等于白做。
典型错误:用 userID % 4 算分库,但生成的 ID 是 sonyflake 输出的 64 位整数——高位时间戳占满,低位序列随机,取模结果严重倾斜,导致数据分布不均。
- 正确做法:提取 ID 中稳定、可预测的部分用于分片,比如取低 12 位(
id & 0xfff)或截取时间戳后 6 位数字 - 避免用
abs(int64(id)) % N:Go 中uint64转int64可能溢出,直接用位运算更安全 - 分表名计算和分库选择必须在
db.Table()和获取*gorm.DB实例前完成,顺序错一点就查不到数据
最容易被忽略的一点:ID 生成逻辑和分片路由逻辑必须耦合在同一个上下文里。不能 A 服务生成 ID,B 服务根据 ID 查库——中间任何环节出错(如日志丢失、网络分区),都会导致“写了找不到”。











