龙空技术网

在分布式系统中生成全局唯一ID的几种方式

JOKERaistlin 225

前言:

而今姐妹们对“java生成唯一的id”大致比较关注,咱们都想要分析一些“java生成唯一的id”的相关知识。那么小编同时在网摘上收集了一些关于“java生成唯一的id””的相关知识,希望看官们能喜欢,同学们一起来了解一下吧!

在分布式系统中,生成全局唯一ID是一个很基础的需求,相信每个项目都会结合自身实际情况,有自己的一套ID生成算法。本文收集了几种常见的ID生成方式,希望可以给大家一些参考。

利用数据库自增ID实现

利用数据库字段本身的auto_increment功能生成自增ID是最常见的做法之一。

这种做法的优势是实现简单,成本低,生成的ID是自增且步长固定的。

当然,在实际游戏中,数据库往往是要分库分表的,在实际应用中的一种常见的做法是使用逻辑服务器ID*偏移量作为自增初始值,例如1服的玩家ID从10000000开始自增,2服的玩家ID从20000000开始自增……这样在分库的同时也保证了ID的唯一性,方便日后服务器的合并。

当然,这种方式的缺陷也很明显,那就是数据库的IO效率往往是比较低的,如果业务过于依赖数据库的话,容易产生瓶颈。因此,这种方式通常不是孤立存在的,而是和各种缓存技术结合使用。

利用单点来统一生成全局ID

分布式系统最大的问题在于程序跑在各个独立的进程上,无法保证不同进程各自生成ID的唯一性。因此,一个顺理成章的思路就是由统一的ID生成模块来专门进行全局的ID生成。而且,这个模块最好是单线程的,这样可以达到最高的执行效率。

满足这个条件的现成组件很容易想到,那就是redis。

Redis的incr命令是原子自增,可以实现该需求。在实际业务中,整个分布式系统可以使用一个公共redis来实现唯一自增ID需求,而且,现在的redis直接支持主从复制,数据的安全性也有一定保证。这种方式的好处就是实现成本低,部署一个redis即可,redis的吞吐量也是经得起考验的。

不过,incr命令的一个问题是不够灵活,只能自增加1,当业务有特殊需求例如手动控制步长时,incr命令无法直接满足需求。如果自己实现全局ID管理模块的话,工作量较大,还需要考虑各种安全性问题,例如单点服务挂掉之后如何处理、脏数据如何处理,如果考虑不周的话很容易出bug或者成为瓶颈,实现起来成本会略高一些。

使用UUID生成全局唯一ID

UUID是Universally Unique ID的缩写。是一种用于分布式系统的全局ID生成思想,最常见的UUID实现方式之一是微软的GUID规则。

UUID的基本思想是由以下几个部分经过算法组合之后生成一个ID:

当前时间时钟序列机器识别号(通常是mac地址)与当前进程相关的一个序列

从组成UUID的四个部分就可以看出,在分布式系统中,哪怕是不同进程部署在同一台机器上,也可以保证每个进程生成的UUID是唯一的。

UUID有不同的实现方式,也有很多现成的API可以使用,成本低,安全性和稳定性都有保证。而且,UUID是直接在本地生成的,生成时无需像前两种方式一样需要远程调用,在速度上是最快的。

不过UUID最大的问题在于,它太长了,甚至超过了64位长整型的上限。如果不经过二次处理的话,它在入库的时候是一个字符串,字符串在进行插入和查询时的效率都远低于数字,而且占用的磁盘空间也更大,不够完美。

使用UUID的变体

对比下上面的几种方式,我们可以看出综合考虑IO效率、稳定性、实现成本这几个方面的话,UUID是最好的思路,如果能解决长度问题,那就不失为一种非常完美的生成算法了。

为了解决这个问题,行业内产生了很多UUID的变体实现。例如Twitter曾经采用的一种方案,将所有信息压缩到一个64位长整形内,他们还给方案起了一个很好听的名字叫做snowflake算法。

Snowflake的思路是使用一个64位long型的数据来生成ID。64位的每一位使用方式如上图所示:

第1位是符号位,固定是0,无法用到(因为这玩意最初是用java写的,java里面没有unsigned数据类型)。

2~42位记录当前时间戳,精确到毫秒级。用41位毫秒级来记录时间戳的话,可以保证69.73年内ID不会重复,这个时长对九成九的产品来说是够用了。实际上, 我觉得在游戏业务中还可以进一步缩减,39位20年就足够绝大多数游戏项目用到公司倒闭了。

接下来10位是节点ID。Twitter是一款互联网产品,只有分布式的不同数据节点,没有服务器概念。在游戏产品中可以把这10位作为逻辑服务器的ID,10位数最多支持1024个服。如果觉得不够,再加3位,8192个服,王者荣耀都没这么多服,管够用了。

最后12位是节点内的ID,在各个进程内部自己循环累加即可。

从上面的分析可以看出,snowflake策略具备UUID的所有优点,实现成本也不高,只要掌握一些位运算基础即可,最多可以支持每毫秒4096个新增,相信还没有哪个游戏能达到这个新增量级的。

不过,对当代手游来说,snowflake还存在两个难以搞定的问题:

首先,需要考虑时钟往回调的问题。当各个节点的时钟是各自向前走的时候,该问题是不会出现的,但是在分布式系统中,为了保证各个硬件的时钟同步,势必会用到时钟校准,在校准时就可能会出现时钟回调,可能因此导致ID重复。处理时钟回调也不难,以游戏的注册量级,一般来说最多重试个两三次即可

第二个问题就比较难搞定了,那就是客户端的数字精度问题。现代手游一般采用脚本语言来做客户端逻辑,而有些脚本语言是不支持64位整数的。例如在JS中,没有int、long、float、double这些类型,所有的数字都统一是number类型,而这个number的上限,是53位,原始的snowflake肯定是没法用的,需要用snowflake的思路进一步将ID压缩到53位以内。

本文介绍了几种分布式系统中常用的方法,对游戏业务来说,这几种方法各有利弊。在现实中,很多时候就是这样,没有完美的方案,只有最适合当前情况的方案。在做项目的过程中,不要盲目迷信,选择最适合自己项目的才是最好的(顺便吐个槽,游戏业盲目迷信spring和protocol buffer的太多了)。

标签: #java生成唯一的id