规则:
业务前缀 + 订单类型 + 年月日 + 秒级时间戳的后5位 + 微秒时间戳微秒部分前5位 + 随机值M
业务前缀 + 2(买家单类型) + 241018 + 83239 + 39048 + 7
秒级控制:
一天的秒数是86400 ,所以取5位数,能满足秒级别的不重复
00000-86400
00001 -86401
13599 - 99999
13600 - 1 00000
微秒级别控制
微秒时间戳 前5位 + 随机值 能一定程度上避免微级别的冲突,
但是不能百分之百,所以肯定要用锁来去重一下
锁控制
jiuwu_genUniOn_[order_number] 做锁,incr操作如果是返回不为1 ,sleep一毫秒,递归调用自己;否则设置缓存过期时间,直接返回
代码实现示例:
func GetOrderNum(ctx context.Context, orderType string) string {
prefix := "start"
num := 5 //因为一天的秒数是86400(5位) 如果只取4位会有重复的风险 比如 同一天获取秒级时间戳可能会有 XXX00000 和 XXX10000 如果只取4位 得到的值是一样的都是0000,取5位就不会
// 获取当前时间
now := time.Now()
// 转换时间为字符串
ymd := now.Format("060102")
// 将时间戳转换为字符串
timestampStr := strconv.FormatInt(now.Unix(), 10)
// 获取微秒
microTs := strconv.FormatInt(now.UnixNano()/1e3, 10)
// 生成随机数
randomNum := strconv.Itoa(rand.Intn(9) + 1)
//秒级后5位 毫秒级前5位 95 + 订单类型 + 年月日 + 秒级时间戳的后N位 + 微秒时间戳微秒部分前5位 + 随机值M
orderNumber := prefix + orderType + ymd + timestampStr[len(timestampStr)-num:] + microTs[10:][:5] + randomNum
key := fmt.Sprintf("jiuwu_genUniOn_%s", orderNumber)
cnt, err := gd.Redis.Incr(ctx, key).Result()
//出错或者碰撞都要重新生成
if err != nil || cnt != 1 {
time.Sleep(1 * time.Microsecond)//因为上面是控制在微秒级别的,所以这里休息一微秒
msg := fmt.Sprintf("生成订单号出错或者出现碰撞 类型 (%d) err(%+v) 是否碰撞(%t)", orderType, err, cnt != 1)
go SendFeiShuMsg(msg)
return GetOrderNumV2(ctx, orderType)
}
//设置过期时间1天 1天后不可能有碰撞
gd.Redis.Expire(ctx, key, 86400)
return orderNumber
}
思考:
1.如果出现异常,一直递归怎么办
每次获取当前时间方法是底层的一般不会出现问题,递归前sleep了一微秒,避免了无用的循环,所以第二次递归的时候肯定不在同一微秒了,不会出现多次递归的情况;如果出现了有飞书告警,可以人工介入
2.生成订单 的QPS是多少
提交订单接口的响应99线耗时200ms以内,QPS 个位数(好吧...)
吐槽
1.微妙是 16位,前面10位是秒级别,后面6位是微妙部分,没必要只取5位再加一个随机数,完全可以取6位,控制微妙级别不重复,+锁 避免重复
- 1 秒 = 1000 毫秒
- 1 毫秒 = 1000 微秒
- 1 微秒 = 1000 纳秒