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

在分布式系统中,生成全局惟一ID,有不少种方案,可是在这多种方案中,每种方案都有有缺点,下面咱们之针对经过经常使用数据库来生成分布式ID的方案,其它方法会在其它文中讨论:mysql

1,RDBMS生成ID:

这里咱们讨论mysql生成ID。由于MySQL自己能够auto_increment和auto_increment_offset来保证ID自增,很天然地,咱们会想到借助这个特性来实现这个功能。算法

全局ID生成方案里采用了MySQL自增加ID的机制(auto_increment + replace into + MyISAM)。一个生成64位ID方案具体实现是这样的: 
先建立单独的数据库(eg:ticket),而后建立一个表:sql

CREATE TABLE Tickets64 (
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 Tickets64,查询结果就是这样的:mongodb

每当咱们的应用须要ID的时候就会作以下操做,调用以下存储过程:数据库

begin;
REPLACE INTO Tickets64 (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年撰文介绍的一种主键生成策略(Ticket Servers: Distributed Unique Primary Keys on the Cheap )。以下所示,为了实现上述方案分别设置两台机器对应的参数,TicketServer1从1开始发号,TicketServer2从2开始发号,两台机器每次发号以后都递增2。

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

TicketServer2:
auto-increment-increment = 2
auto-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的一种算法,这种方案把64-bit分别划分红多段,分开来标示机器、时间等,好比在snowflake中的64-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为例:

MongoDB官方文档 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,21

B:2,7,12,17,22

C:3,8,13,18,23

D:4,9,14,19,24

E:5,10,15,20,25

这个,随便负载到哪一个机肯定好,将来很难作修改。可是3-5台服务器基本可以知足器上,均可以得到不一样的ID。可是步长和初始值必定须要事先须要了。使用Redis集群也能够方式单点故障的问题。

另外,比较适合使用Redis来生成天天从0开始的流水号。好比订单号=日期+当日自增加号。能够天天在Redis中生成一个Key,使用INCR进行累加。

 

优势:

1)不依赖于数据库,灵活方便,且性能优于数据库。

2)数字ID自然排序,对分页或者须要排序的结果颇有帮助。

缺点:

1)若是系统中没有Redis,还须要引入新的组件,增长系统复杂度。

2)须要编码和配置的工做量比较大。

相关文章
相关标签/搜索