在实际生产过程中,有创建全局唯一id的情况(游戏开发中尤为明显)。这样做的目的和好处很多。
一般情况下,我们可以通过数据库或特殊算法来达到一致,这里主要讲如何通过snowflake的方式创建golang的uuid。
符号位| 32 时间戳 | 8 区域 | 13 节点 | 10自增ID
0 | 00000000 00000000 00000000 00000000 | 00000000 | 00000000 00000 | 00000000 00
上图简单的描述了该算法的核心思路,即提前约定不同位代表不同的意义,如上代码中(具体所占位的分配,可根据实际业务情况而定),
32位->时间戳代表系统当前时间,(从标准时间开始32位肯定不够用,所以一般需要减去某个<当前时间的值)
8 区域->用来表示平台商户号(运营商代号)
13 节点->表示物理机编号
10 自增ID->单物理机中的自增号(此号码达到最大值后归0)
按照此思路即可做到,任一时刻任意一台物理机节点中,生成的id在全网络都是唯一的。(同样原理配合中英文可生成更多唯一的uuid,不过一般不会有这么大用户体量)
下面是基于snowflake的变种golang实现,为何是变种,在文章结尾中说明。
package uuid
import (
"strconv"
"sync/atomic"
"time"
)
const (
//http://www.matools.com/timestamp
twepoch string = "2019-11-21 13:19:00" //默认开始时间
districtIdBits uint = 8 //区域 最多支持255
nodeIdBits uint = 13 //节点 最多支持8181
sequenceBits uint = 10 //自增ID 1023
/*
* 1 符号位 | 32 时间戳 | 8 区域 | 13 节点 | 10自增ID
* 0 | 00000000 00000000 00000000 00000000 | 00000000 | 00000000 00000 | 00000000 00
*/
maxNodeId = -1 ^ (-1 << nodeIdBits) //节点 ID 最大范围
maxDistrictId = -1 ^ (-1 << districtIdBits) //最大区域范围
nodeIdShift = sequenceBits //节点左移位数
districtIdShift = sequenceBits + nodeIdBits //区域左移位数
timestampLeftShift = sequenceBits + nodeIdBits + districtIdBits //时间戳左移位数
sequenceMask = -1 ^ (-1 << sequenceBits) //自增ID最大值
)
/*
NodeId 节点8181
districtId 区域255
*/
//@return nil,nil
func NewIdWorker(NodeId int64, districtId int64) (*IdWorker, error) {
worker := &IdWorker{}
if NodeId > maxNodeId || NodeId < 0 {
return nil, ERR_NodeId
}
if districtId > maxDistrictId || districtId < 0 {
return nil, ERR_DistrictId
}
worker.nodeId = NodeId
worker.districtId = districtId
worker.twepoch = time.Now().Unix() - GetTimeByFormat1(twepoch).Unix()
return worker, nil
}
type IdWorker struct {
sequence int64 //序号
nodeId int64 //节点ID
twepoch int64 //默认开始时间
districtId int64 //区域号
}
//@return ""
func (this *IdWorker) NextString() string {
return strconv.FormatInt(this.NextInt64(), 10)
}
//@return 0
func (this *IdWorker) NextInt64() int64 {
seqId := atomic.AddInt64(&this.sequence, 1)
timestamp := this.twepoch + seqId/sequenceMask
seqId = seqId % sequenceMask
return (timestamp << timestampLeftShift) | (this.districtId << districtIdShift) | (this.nodeId << nodeIdShift) | seqId
}
//@return []int64{}
func (this *IdWorker) GetInt64s(num int64) []int64 {
seqId := atomic.AddInt64(&this.sequence, num)
ids := make([]int64, 0, num)
for num >= 0 {
num--
id := seqId - num
timestamp := this.twepoch + id/sequenceMask
id = id % sequenceMask
ids = append(ids, (timestamp<<timestampLeftShift)|(this.districtId<<districtIdShift)|(this.nodeId<<nodeIdShift)|id)
}
return ids
}
//@return []string{}
func (this *IdWorker) GetStrings(num int64) []string {
seqId := atomic.AddInt64(&this.sequence, num)
ids := make([]string, 0, num)
for num > 0 {
num--
id := seqId - num
timestamp := this.twepoch + id/sequenceMask
id = id % sequenceMask
ids = append(ids, strconv.FormatInt((timestamp<<timestampLeftShift)|(this.districtId<<districtIdShift)|(this.nodeId<<nodeIdShift)|id, 10))
}
return ids
}
//"2006-01-02 15:04:05"格式获取time对象
//@return time.Now()
func GetTimeByFormat1(str string) time.Time {
tm, _ := time.ParseInLocation(F1, str, time.Local)
return tm
}
仔细阅读代码会发现, 我们并没有在每次Get的时候立即获取当前系统时间这是为何呢?
按照上面的位数分配,1秒钟1台物理机只能分配出1000个uuid,如果按照秒来进行驱动,uuid不足则必须在Get时增加当前时间位值(弱会发生这种情况,则可以变相认为时间位实际值肯定会>=当前真实时间)。如果存在上述情况,那么就会出现时间溢出的情况(即提前消费了后面时间中的uuid)。
有了上面的推论,可以得到时间与真实时间无关化,最终得到的uuid依然是正确的。也就是说,时间位值我们可以让他<>=当前真实时间,这都不会影响上面的推论。
最终变种为:
1、每当节点启动后,我们取当前时间-约定时间(永久固定的)为我们的基础时间。
2、自增数从0递增,每次获取uuid+1。直到最大值1023回归到0并将time+1秒。
3、这样就既满足了uuid唯一性,也不再关心1秒1023个uuid的限制(服务器空闲n秒,即节约下n*1023个uuid)
在实际游戏开发中,uuid的产生量是绝对足够的,因内存部分(如怪物,道具等)或玩家私有部分不一定要用到uuid,因为他们有上下文关系,总会快速关联到某个已存在的uuid。所以在这种变种情况下,只有一种情况会出现意想不到的结果,即:服务器启动后,立即产生了大量的uuid(假设1秒内产生了1w个)。此时服务器无论什么原因重新启动,而重新启动的时间<10秒。当然这是相当极端的情况了,至少在以往的开发中,还没有遇到过这样的情况。
最终,我们实现了只需要一个原子操作,即可实现的uuid生成器。