math/rand 默认不适合当订单号,因 rand.intn() 无时间/机器信息且纳秒级重复 seed 导致序列重复;推荐用时间戳+原子计数器+随机后缀组合,或 xid 库生成全局唯一 id,并强制 db 唯一约束兜底。

为什么 math/rand 默认生成的数字不适合直接当订单号
因为 rand.Intn() 本身不带时间、机器或上下文信息,单机高并发下极易重复;更关键的是,它默认用 rand.NewSource(time.Now().UnixNano()) 初始化时,若在纳秒级内多次调用(比如 for 循环快速创建),time.Now().UnixNano() 可能返回相同值,导致多个 *rand.Rand 实例使用同一 seed,输出完全相同的随机序列。
实操建议:
- 绝不要在热路径里反复调用
rand.New(rand.NewSource(time.Now().UnixNano())) - 全局复用一个
*rand.Rand实例(用sync.Pool或包级变量),并确保初始化时 seed 足够离散 - 如果必须每请求新建,改用
crypto/rand(虽慢但不可预测),不过订单号其实不需要密码学安全
用 time.Now() + atomic + 随机后缀组合最稳妥
这是生产环境最常用、平衡唯一性、可读性与性能的做法:时间戳保证大致有序和跨进程不重,原子计数器解决同毫秒冲突,随机后缀防猜测和进一步打散。
示例结构:"ORD20240521152345-000123-7f9a"(日期时间 + 6位自增 + 4位小写十六进制)
实操建议:
- 时间部分用
time.Now().Format("20060102150405"),避免分隔符和时区问题 - 自增部分用
atomic.AddUint32(&counter, 1) % 1000000,防止溢出后归零撞号 - 随机后缀推荐
fmt.Sprintf("%x", rand.Uint32()%0x10000),比字符串拼接快,且长度可控 - 注意:
atomic计数器是单机维度,多实例部署时需额外加机器标识(如 hostname 哈希前两位)
用 xid 库生成无状态、全局唯一 ID
xid 是轻量级库,生成的 ID 是 12 字节 base32 编码(如 "9m4e2mr0ui3e8a215n4g"),含时间戳、机器标识、进程 ID 和随机数,天然去中心化、无需协调、单调递增(按字典序近似)。
实操建议:
- 安装:
go get github.com/rs/xid - 直接调用
xid.New().String(),线程安全,内部已处理种子和并发 - 不建议截断或修改格式——破坏其唯一性和排序性
- 如果业务要求固定前缀(如
"ORD"),拼接即可:"ORD" + xid.New().String(),但注意总长变长 - 注意:
xid不含校验位,无法检测录入错误;若需防误输,得额外加 checksum(如 mod 37)
别忽略数据库唯一约束这个最终防线
无论应用层怎么生成,只要没落到 DB,就不算真正唯一。网络超时、重试、服务重启都可能导致重复插入。
实操建议:
- 订单表必须对订单号字段加
UNIQUE索引 - 插入失败捕获
ErrDuplicateEntry(MySQL)或unique_violation(PostgreSQL),触发重试逻辑(带指数退避) - 重试时不能原样再生成一次——要重新走完整号段逻辑(时间+计数器+随机),否则可能死循环
- 应用层生成 + DB 唯一约束,才是成本低、落地稳的组合
真正麻烦的不是生成逻辑,而是“同一订单被前端重复提交、后端没做幂等、又恰好两次生成了相同号段”的情况——这时靠 DB 约束兜底,比靠算法穷举更可靠。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











