基于Java代码实现游戏服务器生成全局唯一ID的方法汇总
在服务器系统开发时,为了适应数据大并发的请求,我们往往需要对数据进行异步存储,特别是在做分布式系统时,这个时候就不能等待插入数据库返回了取自动id了,而是需要在插入数据库之前生成一个全局的唯一id,使用全局的唯一id,在游戏服务器中,全局唯一的id可以用于将来合服方便,不会出现键冲突。也可以将来在业务增长的情况下,实现分库分表,比如某一个用户的物品要放在同一个分片内,而这个分片段可能是根据用户id的范围值来确定的,比如用户id大于1000小于100000的用户在一个分片内。目前常用的有以下几种:
1,Java自带的UUID.
UUID.randomUUID().toString(),可以通过服务程序本地产生,ID的生成不依赖数据库的实现。
优势:
本地生成ID,不需要进行远程调用。
全局唯一不重复。
水平扩展能力非常好。
劣势:
ID有128bits,占用的空间较大,需要存成字符串类型,索引效率极低。
生成的ID中没有带Timestamp,无法保证趋势递增,数据库分库分表时不好依赖
2,基于Redis的incr方法
Redis本身是单线程操作的,而incr更保证了一种原子递增的操作。而且支持设置递增步长。
优势:
部署方便,使用简单,只需要调用一个redis的api即可。
可以多个服务器共享一个redis服务,减少共享数据的开发时间。
Redis可以群集部署,解决单点故障的问题。
劣势:
如果系统太庞大的话,n多个服务同时向redis请求,会造成性能瓶颈。
3,来自Flicker的解决方案
这个解决方法是基于数据库自增id的,它使用一个单独的数据库专门用于生成id。详细的大家可以网上找找,个人觉得使用挺麻烦的,不建议使用。
4,TwitterSnowflake
snowflake是twitter开源的分布式ID生成算法,其核心思想是:产生一个long型的ID,使用其中41bit作为毫秒数,10bit作为机器编号,12bit作为毫秒内序列号。这个算法单机每秒内理论上最多可以生成1000*(2^12)个,也就是大约400W的ID,完全能满足业务的需求。
根据snowflake算法的思想,我们可以根据自己的业务场景,产生自己的全局唯一ID。因为Java中long类型的长度是64bits,所以我们设计的ID需要控制在64bits。
优点:高性能,低延迟;独立的应用;按时间有序。
缺点:需要独立的开发和部署。
比如我们设计的ID包含以下信息:
|41bits:Timestamp|3bits:区域|10bits:机器编号|10bits:序列号|
产生唯一ID的Java代码:
/** *自定义ID生成器 *ID生成规则:ID长达64bits * *|41bits:Timestamp(毫秒)|3bits:区域(机房)|10bits:机器编号|10bits:序列号| */ publicclassGameUUID{ //基准时间 privatelongtwepoch=1288834974657L;//Thu,04Nov201001:42:54GMT //区域标志位数 privatefinalstaticlongregionIdBits=3L; //机器标识位数 privatefinalstaticlongworkerIdBits=10L; //序列号识位数 privatefinalstaticlongsequenceBits=10L; //区域标志ID最大值 privatefinalstaticlongmaxRegionId=-1L^(-1L<<regionIdBits); //机器ID最大值 privatefinalstaticlongmaxWorkerId=-1L^(-1L<<workerIdBits); //序列号ID最大值 privatefinalstaticlongsequenceMask=-1L^(-1L<<sequenceBits); //机器ID偏左移10位 privatefinalstaticlongworkerIdShift=sequenceBits; //业务ID偏左移20位 privatefinalstaticlongregionIdShift=sequenceBits+workerIdBits; //时间毫秒左移23位 privatefinalstaticlongtimestampLeftShift=sequenceBits+workerIdBits+regionIdBits; privatestaticlonglastTimestamp=-1L; privatelongsequence=0L; privatefinallongworkerId; privatefinallongregionId; publicGameUUID(longworkerId,longregionId){ //如果超出范围就抛出异常 if(workerId>maxWorkerId||workerId<0){ thrownewIllegalArgumentException("workerIdcan'tbegreaterthan%dorlessthan0"); } if(regionId>maxRegionId||regionId<0){ thrownewIllegalArgumentException("datacenterIdcan'tbegreaterthan%dorlessthan0"); } this.workerId=workerId; this.regionId=regionId; } publicGameUUID(longworkerId){ //如果超出范围就抛出异常 if(workerId>maxWorkerId||workerId<0){ thrownewIllegalArgumentException("workerIdcan'tbegreaterthan%dorlessthan0"); } this.workerId=workerId; this.regionId=0; } publiclonggenerate(){ returnthis.nextId(false,0); } /** *实际产生代码的 * *@paramisPadding *@parambusId *@return */ privatesynchronizedlongnextId(booleanisPadding,longbusId){ longtimestamp=timeGen(); longpaddingnum=regionId; if(isPadding){ paddingnum=busId; } if(timestamp<lastTimestamp){ try{ thrownewException("Clockmovedbackwards.Refusingtogenerateidfor"+(lastTimestamp-timestamp)+"milliseconds"); }catch(Exceptione){ e.printStackTrace(); } } //如果上次生成时间和当前时间相同,在同一毫秒内 if(lastTimestamp==timestamp){ //sequence自增,因为sequence只有10bit,所以和sequenceMask相与一下,去掉高位 sequence=(sequence+1)&sequenceMask; //判断是否溢出,也就是每毫秒内超过1024,当为1024时,与sequenceMask相与,sequence就等于0 if(sequence==0){ //自旋等待到下一毫秒 timestamp=tailNextMillis(lastTimestamp); } }else{ //如果和上次生成时间不同,重置sequence,就是下一毫秒开始,sequence计数重新从0开始累加, //为了保证尾数随机性更大一些,最后一位设置一个随机数 sequence=newSecureRandom().nextInt(10); } lastTimestamp=timestamp; return((timestamp-twepoch)<<timestampLeftShift)|(paddingnum<<regionIdShift)|(workerId<<workerIdShift)|sequence; } //防止产生的时间比之前的时间还要小(由于NTP回拨等问题),保持增量的趋势. privatelongtailNextMillis(finallonglastTimestamp){ longtimestamp=this.timeGen(); while(timestamp<=lastTimestamp){ timestamp=this.timeGen(); } returntimestamp; } //获取当前的时间戳 protectedlongtimeGen(){ returnSystem.currentTimeMillis(); } }
使用自定义的这种方法需要注意的几点:
为了保持增长的趋势,要避免有些服务器的时间早,有些服务器的时间晚,需要控制好所有服务器的时间,而且要避免NTP时间服务器回拨服务器的时间;在跨毫秒时,序列号总是归0,会使得序列号为0的ID比较多,导致生成的ID取模后不均匀,所以序列号不是每次都归0,而是归一个0到9的随机数。
上面说的这几种方式我们可以根据自己的需要去选择。在游戏服务器开发中,根据自己的游戏类型选择,比如手机游戏,可以使用简单的redis方式,简单不容易出错,由于这种游戏单服并发新建id量并不太大,完全可以满足需要。而对于大型的世界游戏服务器,它本身就是以分布式为主的,所以可以使用snowflake的方式,上面的snowflake代码只是一个例子,需要自己根据自己的需求去定制,所以有额外的开发量,而且要注意上述所说的注意事项。
以上所述是小编给大家介绍的基于Java代码实现游戏服务器生成全局唯一ID的方法汇总,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!