游戏后台生成唯一ID
游戏后台生成唯一ID
MMO游戏后台通常需要由大量服务器来共同承载海量玩家,虽然玩家可能分布在不同的游戏大区,但是他们可能会通过跨服等等方式进行各种交互。游戏中的角色,装备,物品等需要生成一个全局唯一ID标识,便于辨别不同玩家,不同装备,也方便定位外网问题。
常见的分布式全局唯一ID生成方式包括使用数据库自增,使用Redis的原子操作INCR和INCRBY,使用UUID,SnowFlake算法等等。前面两种方式均需要产生一次异步调用,在MMO中,海量玩家会集中在一个场景中进行PK,做任务,打怪等,场景内业务逻辑复杂,为了降低编码复杂度,减少BUG几率,通常会选择使用本地算法来生成全局唯一ID。
UUID方式生成的ID比较长,通常需用字符串表示,作为内存数据主键或者数据库主键它的查找效率比不上直接使用整数类型生成的ID做主键。同时它对业务来说是一串无规则的字符串,不能根据相关业务规则进行调整。
SnowFlake算法是开源的分布式生成算法,它是一个本地生成算法,它可以生成一个位的整数,具体生成的位结构如下图:
SnowFlake算法12位***支持一个节点同一毫秒内产生4096个ID,一秒内可以产生400多万个UID,其41位的时间戳可以使用69年。
下面讲述一种MMO中的分布式ID生成方式,它会生成一个64位的整数ID,核心思想与SnowFlake类似。同时会根据游戏的特性对64位ID中的位段进行相应的调整。在游戏部署上,我们会根据进程所在不同大区,不同功能,不同机器给线上部署的进程分配一个唯一的进程业务ID,这个进程业务ID的格式如下:WorldID.ZoneID.FuncID.InstID。
根据游戏进程部署的特点,产生了下面这种64位ID的通用结构:
具体字段含义如下:
大区号: 游戏中的分区
虚拟机器号: 一个小区内的机器虚拟编号
功能号: 不同类型的进程的功能编号,比如排行榜进程和组队进程功能号不一样
实例ID: 同一类型的进程的不同实例编号
校验序号: 这个序号在每次进程重启时就自增1,数据会写入本地文件中
***: 同一时间内生成ID会自增***
自适应时间: 进程启动时初始化为当前时间的时间戳(秒级别的)
大区号,虚拟机器号,功能号,实例ID的位数通常根据不同类型的游戏特点进行调整。比如分区分服的游戏,大区很多,那么大区号位数会比较长,而一个区内机器数比较少,那么虚拟机器号分配的位数比较少。而对于全区全服的游戏,甚至可以把大区号与虚拟机器号合并成一个段。
下面以校验序号为2位,***位12位,自适应时间为29位来说明一下这个UID的生成方式。
大区号,虚拟机器号,功能号和实例ID部署时就已经固定好了。校验序号在进程每次启动时自增1并写入本地文件中,自适应时间在每次进程启动时,初始化为当前时间的时间戳(秒级别的)。
在单个UID生成过程中,***,自适应时间变化规则伪代码如下:
uint64_t CreateUID(){
... // 省略部分代码## 标题
dwLoopSeq++;
if(dwLoopSeq == 0) {
dwAdapativeTime++;
}
... // 省略部分代码
}
问题一:由于***只有12位,一秒内一个进程最多产生4096个UID,如何解决一段时间内UID生成突增的情况?
如果在一些突发情况下,一秒内产生的UID超过4096个,那么自适应时间会自增1,相当于提前消费下一秒可产生的UID,通过这种提前消费的方式可以满足某些时间点UID突增的情况。
另外,如果一些时间点产生的UID比较少,那么自适应时间慢于当前时间,也会累计一些逝去的时间用于未来某个时间点UID突增的情况。
问题二:如果自适应时间已经超前当前时间,进程又重启了,自适应时间又倒退到当前时间,会不会导致UID重复?
在每次进程重启时,校验号都会自增1,因此两次启动中,校验序号是不一样的。另外这种方式生成的UID通常用于角色ID,物品ID等,单秒内生成量是可以预估的,从而事先调整UID的各个段的位数来满足业务需求。
在实际运行中,通常也会对自适应时间超前进行监控,当遇到这种情况时,避免进程的频繁重启,随着时间的流逝,自适应时间会慢慢靠近当前时间。