尔游网
您的当前位置:首页基于数据库构建分布式的ID生成方案

基于数据库构建分布式的ID生成方案

来源:尔游网
基于数据库构建分布式的ID⽣成⽅案

在分布式系统中,⽣成全局唯⼀ID,有很多种⽅案,但是在这多种⽅案中,每种⽅案都有有缺点,下⾯我们之针对通过常⽤数据库来⽣成分布式ID的⽅案,其它⽅法会在其它⽂中讨论:

1,RDBMS⽣成ID:

这⾥我们讨论mysql⽣成ID。因为MySQL本⾝可以auto_increment和auto_increment_offset来保证ID⾃增,很⾃然地,我们会想到借助这个特性来实现这个功能。

全局ID⽣成⽅案⾥采⽤了MySQL⾃增长ID的机制(auto_increment + replace into + MyISAM)。⼀个⽣成位ID⽅案具体实现是这样的: 先创建单独的数据库(eg:ticket),然后创建⼀个表:

CREATE TABLE Tickets (

id bigint(20) unsigned NOT NULL auto_increment,stub char(1) NOT NULL default '',PRIMARY KEY (id),

UNIQUE KEY stub (stub)) ENGINE=MyISAM

表创建之后我们要设置⼀个初始值,⽐如100000,执⾏SELECT * from Tickets,查询结果就是这样的:

每当我们的应⽤需要ID的时候就会做如下操作,调⽤如下存储过程:

begin;

REPLACE INTO Tickets (stub) VALUES ('a');SELECT LAST_INSERT_ID();commit;

架构如图:

这样我们就能拿到不断增长且不重复的ID了。

这种⽅案的优缺点如下:优点:

⾮常简单,利⽤现有数据库系统的功能实现,成本⼩,有DBA专业维护。

ID号单调⾃增,可以实现⼀些对ID有特殊要求的业务。缺点:

强依赖DB,当DB异常时整个系统不可⽤,属于致命问题。配置主从复制可以尽可能的增加可⽤性,但是数据⼀致性在特殊情况下难以保证。主从切换时的不⼀致可能会导致重复发号。ID发号性能瓶颈在单台MySQL的读写性能。

对于MySQL性能问题,可⽤如下⽅案解决:在分布式系统中我们可以多部署⼏台机器,每台机器设置不同的初始值,且步长和机器数相等。⽐如有两台机器。设置步长step为2,TicketServer1的初始值为1(1,3,5,7,9,11...)、TicketServer2的初始值为2(2,4,6,8,10...)。这是Flickr团队在2010年撰⽂介绍的⼀种主键⽣成策略()。如下所⽰,为了实现上述⽅案分别设置两台机器对应的参数,TicketServer1从1开始发号,TicketServer2从2开始发号,两台机器每次发号之后都递增2。

TicketServer1:

auto-increment-increment = 2auto-increment-offset = 1TicketServer2:

auto-increment-increment = 2auto-increment-offset = 2

假设我们要部署N台机器,步长需设置为N,每台的初始值依次为0,1,2...N-1那么整个架构就变成了如下图所⽰:

这种架构貌似能够满⾜性能的需求,但有以下⼏个缺点:

系统⽔平扩展⽐较困难,⽐如定义好了步长和机器台数之后,如果要添加机器该怎么做?假设现在只有⼀台机器发号是1,2,3,4,5(步长是1),这个时候需要扩容机器⼀台。可以这样做:把第⼆台机器的初始值设置得⽐第⼀台超过很多,⽐如14(假设在扩容时间之内第⼀台不可能发到14),同时设置步长为2,那么这台机器下发的号码都是14以后的偶数。然后摘掉第⼀台,把ID值保留为奇数,⽐如7,然后修改第⼀台的步长为2。让它符合我们定义的号段标准,对于这个例⼦来说就是让第⼀台以后只能产⽣奇数。扩容⽅案看起来复杂吗?貌似还好,现在想象⼀下如果我们线上有100台机器,这个时候要扩容该怎么做?简直是噩梦。所以系统⽔平扩展⽅案复杂难以实现。ID没有了单调递增的特性,只能趋势递增,这个缺点对于⼀般业务需求不是很重要,可以容忍。数据库压⼒还是很⼤,每次获取ID都得读写⼀次数据库,只能靠堆机器来提⾼性能。

2,类snowflake⽅案

这种⽅案⼤致来说是⼀种以划分命名空间(UUID也算,由于⽐较常见,所以单独分析)来⽣成ID的⼀种算法,这种⽅案把-bit分别划分成多段,分开来标⽰机器、时间等,⽐如在snowflake中的-bit分别表⽰如下图(图⽚来⾃⽹络)所⽰:

41-bit的时间可以表⽰(1L<<41)/(1000L*3600*24*365)=69年的时间,10-bit机器可以分别表⽰1024台机器。如果我们对IDC划分有需求,还可

以将10-bit分5-bit给IDC,分5-bit给⼯作机器。这样就可以表⽰32个IDC,每个IDC下可以有32台机器,可以根据⾃⾝需求定义。12个⾃增序列号可以表⽰2^12个ID,理论上snowflake⽅案的QPS约为409.6w/s,这种分配⽅式可以保证在任何⼀个IDC的任何⼀台机器在任意毫秒内⽣成的ID都是不同的。

这种⽅式的优缺点是:优点:

毫秒数在⾼位,⾃增序列在低位,整个ID都是趋势递增的。

不依赖数据库等第三⽅系统,以服务的⽅式部署,稳定性更⾼,⽣成ID的性能也是⾮常⾼的。可以根据⾃⾝业务特性分配bit位,⾮常灵活。缺点:

强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可⽤状态。以Mongdb objectID为例:可以算作是和snowflake类似⽅法:

为了考虑分布式,“_id”要求不同的机器都能⽤全局唯⼀的同种⽅法⽅便的⽣成它。因此不能使⽤⾃增主键(需要多台服务器进⾏同步,既费时⼜费⼒),

因此选⽤了⽣成ObjectId对象的⽅法。

ObjectId使⽤12字节的存储空间,其⽣成⽅式如下:|0|1|2|3|4|5|6 |7|8|9|10|11||时间戳 |机器ID|PID|计数器 |

前四个字节时间戳是从标准纪元开始的时间戳,单位为秒,有如下特性:

1 时间戳与后边5个字节⼀块,保证秒级别的唯⼀性; 2 保证插⼊顺序⼤致按时间排序; 3 隐含了⽂档创建时间;

4 时间戳的实际值并不重要,不需要对服务器之间的时间进⾏同步(因为加上机器ID和进程ID已保证此值唯⼀,唯⼀性是ObjectId的最终诉求)。机器ID是服务器主机标识,通常是机器主机名的散列值。

同⼀台机器上可以运⾏多个mongod实例,因此也需要加⼊进程标识符PID。

前9个字节保证了同⼀秒钟不同机器不同进程产⽣的ObjectId的唯⼀性。后三个字节是⼀个⾃动增加的计数器(⼀个mongod进程需要⼀个全局的计数器),保证同⼀秒的ObjectId是唯⼀的。同⼀秒钟最多允许每个进程拥有(256^3 = 16777216)个不同的ObjectId。

总结⼀下:时间戳保证秒级唯⼀,机器ID保证设计时考虑分布式,避免时钟同步,PID保证同⼀台服务器运⾏多个mongod实例时的唯⼀性,最后的计数器保证同⼀秒内的唯⼀性(选⽤⼏个字节既要考虑存储的经济性,也要考虑并发性能的上限)。\"_id\"既可以在服务器端⽣成也可以在客户端⽣成,在客户端⽣成可以降低服务器端的压⼒。

3,使⽤Redis来⽣成ID

当使⽤数据库来⽣成ID性能不够要求的时候,我们可以尝试使⽤Redis来⽣成ID。这主要依赖于Redis是单线程的,所以也可以⽤⽣成全局唯⼀的ID。可以⽤Redis的原⼦操作 INCR和INCRBY来实现。

可以使⽤Redis集群来获取更⾼的吞吐量。假如⼀个集群中有5台Redis。可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是5。各个Redis⽣成的ID为:A:1,6,11,16,21B:2,7,12,17,22C:3,8,13,18,23D:4,9,14,19,24E:5,10,15,20,25

这个,随便负载到哪个机确定好,未来很难做修改。但是3-5台服务器基本能够满⾜器上,都可以获得不同的ID。但是步长和初始值⼀定需要事先需要了。使⽤Redis集群也可以⽅式单点故障的问题。

另外,⽐较适合使⽤Redis来⽣成每天从0开始的流⽔号。⽐如订单号=⽇期+当⽇⾃增长号。可以每天在Redis中⽣成⼀个Key,使⽤INCR进⾏累加。 优点:

1)不依赖于数据库,灵活⽅便,且性能优于数据库。2)数字ID天然排序,对分页或者需要排序的结果很有帮助。缺点:

1)如果系统中没有Redis,还需要引⼊新的组件,增加系统复杂度。2)需要编码和配置的⼯作量⽐较⼤。 参考:https://tech.meituan.com/MT_Leaf.html

因篇幅问题不能全部显示,请点此查看更多更全内容