基于snowflake的无锁uuid生成器(三)

在实际生产过程中,有创建全局唯一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生成器。

发布了3 篇原创文章 · 获赞 2 · 访问量 4225

猜你喜欢

转载自blog.csdn.net/ysir86com/article/details/103949932