分布式全局惟一ID的实现

分布式全局惟一ID的实现

前言

上周末考完试,这周正好把工做整理整理,而后也把以前的一些素材,整理一番,也当本身再学习一番。
一方面正好最近看到几篇这方面的文章,另外一方面也是正好工做上有所涉及,因此决定写一篇这样的文章。
先是简单介绍概念和现有解决方案,而后是我对这些方案的总结,最后是我本身项目的解决思路。node

概念

在复杂分布式系统中,每每须要对大量的数据和消息进行惟一标识。redis

如在金融、电商、支付、等产品的系统中,数据日渐增加,对数据分库分表后须要有一个惟一ID来标识一条数据或消息,数据库的自增ID显然不能知足需求,此时一个可以生成全局惟一ID的系统是很是必要的。算法

特色:

  • 全局惟一性(核心):做为惟一标识,不能够出现重复ID
  • 趋势递增:在MySQL InnoDB引擎中使用的是汇集索引,因为多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面咱们应该尽可能使用有序的主键保证写入性能。
  • 单调递增:保证下一个ID必定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
  • 信息安全:若是ID是连续的,恶意用户的扒取工做就很是容易作了,直接按照顺序下载指定URL便可;若是是订单号就更危险了,竞对能够直接知道咱们一天的单量。因此在一些应用场景下,会须要ID无规则、不规则。
    同时除了对ID号码自身的要求,业务还对ID号生成系统的可用性要求极高,想象一下,若是ID生成系统瘫痪,这就会带来一场灾难。

运用场景:

分布式全局惟一ID(数据库的分库分表后须要有一个惟一ID来标识一条数据或消息;特别一点的如订单、骑手、优惠券也都须要有惟一ID作标识;MQ中消息的高可用性(确认消息是否发送成功,是否已发送等)等)
其实分布式全局ID是一个比较复杂,重要的分布式问题(什么问题涉及真正的分布式,高并发后都会比较复杂)。常看法决方案有UUID,Snowflake,Flicker,Redis,Zookeeper,Leaf等。sql

实现方案:

UUID(此处用的Version1:共五个版本,Version1是基于时间的)

生成一个32位16进制字符串(16字节的128位数据,一般以32位长度的字符串表示)(结合机器识别码(全局惟一的IEEE机器识别号,若是有网卡,从网卡MAC地址得到,没有网卡以其余方式得到),当前时间,一个随机数)。数据库

优势:

  • 性能好;
  • 扩展性高;
  • 本地生成;
  • 无网络消耗;
  • 不须要考虑性能瓶颈;
  • 不须要提早商定,各自为政,但绝对不会冲突

缺点:

  • 没法保证趋势递增(因为数据库MySQL的InnoDB采用聚簇索引,有序的ID能够保证写入速度);
  • UUID过长(消耗内存,带宽等。更重要的是若是存储在数据库中,做为主键创建索引效率低)

适用场景:

不须要考虑空间占用,不须要生成有递增趋势的ID,且不在MySQL中存储。安全

Snowflake

Twitter开源,生成一个64bit(0和1)字符串(1bit不用,41bit表示存储时间戳,10bit表示工做机器id(5位数据标示位,5位机器标识位),12bit序列号)服务器

结构:

  • 首位符号位:由于ID通常为正数,该值为0.
  • 41位时间戳(毫秒级):时间戳并非当前时间戳,而是存储时间戳的差值(当前时间戳-起始时间戳(起始时间戳须要程序指定),理论能够适用(1<<41)/(1000x60x60x24x365),69年。
  • 10位数据机器位(说白了就是逻辑分片ID,具体实现和机器自己无关系):包括5位数据标识位和5位机器标识位(好比5位机房ID,5位机器ID),理论最多能够部署节点位:1<<10=1024。
  • 12位毫秒内的序列:同一节点,同一时刻(同一毫秒内)最多生成ID数1<<12=4096。

最后生成64位Long型数值(这里指,通常Long数据就是64位bit的)。网络

优势:

  • 趋势递增,且按照时间有序;
  • 性能高,稳定性高,不依赖数据库等第三方系统;
  • 能够按照自身业务特性灵活分配bit位(好比机器位改成15bit,序列位改成7bit)。

缺点:

  • 依赖机器时钟(虽然UUID也根据当前时间,但其非时间部分波动太大了(从新组织措辞)),时钟回拨会形成暂不可用或重复发号(分布式系统中,每台机器上的时钟不可能彻底同步。在同步各个服务器的时间时,有必定概率发生时钟回拨(时间超了,往回拨))

适用场景:

要求高性能,能够不连续,数据类型为long型。数据结构

Flicker

主要思路是涉及单独的库表,利用数据库的自增ID+replace_into,来生成全局ID。架构

前置补充:

replace into跟insert功能相似,不一样点在于:replace into首先尝试插入数据列表中,若是发现表中已经有此行数据(根据主键或惟一索引判断)则先删除,再插入。不然直接插入新数据。

建表:
 create table t_global_id(
            id bigint(20) unsigned not null auto_increment,
            stub char(1) not null default '',
            primary key (id),
            unique key stub (stub)
    ) engine=MyISAM;

 

 (stub:票根,对应须要生成ID的业务方编码,能够是项目名,表名,甚至是服务器IP地址。 MyISAM(MYSQL5.5.8前默认数据库存储引擎,5.5.8及以后默认存储引擎为InnoDB):(此处应当有MyISAM与InnoDB引擎的区别,乃至其余引擎)基于ISAM类型。不是事务安全(没有事务隔离??),不支持外键,没有行级锁。若是执行大量的select,建议MyISAM。 获取数据: # 每次业务可使用如下SQL读写MySQL获得ID号
 replace into t_golbal_id(stub) values('a');
    select last_insert_id();

 

扩展:为解决单点问题,启用多台服务器,如MySQL,利用给字段设置auto_increment_increment和auto_increment_offset来保证ID自增(如经过设置起始值与步长,生成奇偶数ID)

优势:

  • 很是简单,充分利用了数据库系统的功能实现,成本小,有DBA专业维护;
  • ID号单调自增,能够实现一些对ID有特殊要求的业务。

缺点:

  • 强依赖DB,当DB异常时,整个系统不可用,属于致命问题(配置主从复制能够尽量地增长可用性,可是数据一致性在特殊状况下难以保证。主从切换时的不一致可能致使重复发号);
  • 水平扩展困难(定义好了起始值,步长和机器台数以后,若是要添加机器就比较麻烦(为何我想到了REDIS的哈希一致原理));
  • ID发号性能瓶颈限制在单台MySQL的读写性能。

适用场景:

数据量不大,并发量不大。

Redis

因为Redis的全部命令是单线程的,因此能够利用Redis的原子操做INCR和INCRBY,来生成全局惟一的ID。

扩展:

能够经过集群来提高吞吐量(能够经过为不一样Redis节点设置不一样的初始值并赞成步长,从而利用Redis生成惟一且趋势递增的ID)(其实这个方法和Flicker一致,只是利用到了Redis的一些特性,如原子操做,内存数据库读写快等)(Incrby:将key中储存的数字加上指定的增量值。这是一个“INCR AND GET”的原子操做,业务方能够定义一个本身的key值,经过INCR命令来获取对应的ID)

优势:

不依赖数据库,灵活方便,且性能优于基于数据库的Flicker方案。

缺点:

  • 扩展性低,Redis集群须要设置号初始值与步长(与Flicker方案同样);
  • Redis宕机可能生成重复的ID;若是系统中没有Redis,还须要引入新的组件,增长系统复杂度;
  • 须要编码和配置的工做量比较大。

适用场景:

Redis集群高可用,并发量高。

举例:

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

zookeeper

经过其znode数据版原本生成序列号,能够生成32位和64位的数据版本号,客户端可使用这个版本号来做为惟一的序列号。

小结:不多会使用zookeeper来生成惟一ID。主要是因为须要依赖zookeeper,而且是多步调用API,若是在竞争较大的状况下,须要考虑使用分布式锁。所以,性能在高并发的分布式环境下,也不甚理想。

Leaf

美团的Leaf分布式ID生成系统,在Flicker策略与Snowflake算法的基础上作了两套优化的方案:Leaf-segment数据库方案(相比Flicker方案每次都要读取数据库,该方案改用proxy server批量获取,且作了双buffer的优化)与Leaf-snowflake方案(主要针对时钟回拨问题作了特殊处理。若发生时钟回拨则拒绝发号,并进行告警)。

MongDB objectID

ObjectID能够算做和snowflake相似方法,经过”时间+机器码+pid+inc”共12个字节,经过4+3+2+3的方式,最终标识一个24长度的十六进制字符。

理论总结

其实除了上述方案外,还有ins等的方案,但总的来看,方案主要分为两种:第一有中心(如数据库,包括MYSQL,REDIS等),其中能够会利用事先的预定来实现集群(起始步长)。第二种就是无中心,经过生成足够散落的数据,来确保无冲突(如UUID等)。站在这两个方向上,来看上述方案的利弊就方便多了。

中心化方案:

优势:

  • 数据长度相对小一些;
  • 数据能够实现自增趋势等。

缺点:

  • 并发瓶颈处理;
  • 集群须要实现约定;横向扩展困难(固然有的方案看起来后二者没有那么问题,是由于,这些方案利用其技术特性,早就必定程度上解决了这些问题,如Redis的横向扩展等)。

非中心化方案:

优势:

  • 实现简单(由于不须要与其余节点存在这方面的约定,耦合);
  • 不会出现中心节点带来的性能瓶颈;
  • 扩展性较高(扩展的局限每每集中于数据的离散问题)。

缺点:

  • 数据长度较长(毕竟就是经过这一特性来实现无冲突的);
  • 没法实现数据的自增加(毕竟是随机的);
  • 依赖数据生成方案的优劣(数据生成方案的优劣会全盘接收,但能够推成出新)。

体悟:

技术是无穷无尽的,咱们不只须要看到其中体现的思想与原则,在学习新技术或方案时,须要明确其中一些特性,优缺点的来源,从而进行有效的总结概括。

应用角度来讲:(一方面想要标示符短,便于处理与存储,另外一方面想要足够大,而不会产生冲突。呵呵)。最理想就是追求从0开始,每一个标示符都被使用,且不重复,并且不用担忧并发。呵呵。彻底应该根据当前业务场景来选择,毕竟业务场景在当前是肯定的。若是业务变更较大(好比发展初期,业务增加很快),那就须要考虑扩展性,便于往后进行该模块的更新与技术方案的替换实现(避免一个系统开发一年,用不到一年,那就尴尬了))。

我的经验

我曾经作过一个“工业物联网”系统,该系统系统是分为三个子系统:终端服务器(用于收集终端传感器数据);企业中控服务器(接收来自多个终端服务器的数据,进行综合查看与控制);云平台服务器(提供上云)。其中就涉及多个终端服务器的传感器数据辨识问题,这里以倾斜传感器数据为例。简述不一样终端服务器的倾斜数据的如何实现全局惟一标识。

以企业中控服务器的数据库做为统一的数据标识来源

简单说,就是终端服务器发送一个数据到企业中控室,企业中控服务器就将该数据保存到数据库中,那么每一个数据在企业中控服务器数据库中都有惟一的ID,而且保持了自增。

优势是实现简单,只须要作好数据收发,与数据的插入工做便可。惟一须要注意的是数据库插入时注意资源互斥,防止出现数据插入异常问题(Springframework生成的Bean默认时单例的)。

缺点是须要实时收发数据,防止数据丢失,数据积压,数据的create_time异常等问题。

以UUID等方式生成数据的全局惟一标识

简单说,就是终端服务器要发送的数据赋予UUID这样的ID,来确保全局惟一。这样终端服务器就能够和中控服务器保持一样且不冲突的ID了。数据的生成是实如今终端服务器的,而中控服务器只是做为数据的保存与调用(经过统一ID调用)。

优势是不须要数据的实时收发,避免系统在弱网络状况下出现各种异常。

缺点是数据的ID过长,而且没法保持自增。而且在某种程度上带来了数据复杂度,从而提升了系统复杂度。

落地方案

因为实际业务的需求,如弱网络,数据交互频率跨度大等状况。最终个人实现是先由终端服务器在启动之初,在企业中控服务器注册TerminalId,做为不一样终端服务器的标识。不一样终端服务器接收与保存数据时,都会在每条数据中插入TerminalId,便于企业中控服务器的识别。固然,具体实现当中还有一些细节。如终端服务器在注册时因为网络等状况注册失败,会先创建一个相似UUID的TerminalId来先保存监测数据。当注册成功时(系统会根据TerminalId的长度等特性来判断是否注册失败,是否须要从新注册),会从新修改全部数据的TerminalId,再容许数据上传。

优势是确保了数据在弱网络状况下的正确性,而且实现了自动注册等通用模块的实现。

缺点是最终数据插入企业中控服务器数据库时,并无严格实现数据符合实际时间的增加(如某终端服务器因为网络等状况无法发送数据,等待一段时间后发送了这段时间堆积的数据),但保持了整体增加的趋势。

总结

IT没有银弹,咱们要作的是多去了解现有的技术方案,再产生符合本身需求的技术方案。由于不一样的技术方案都由于其使用场景有着各自的特色,而咱们须要了解各类特色的技术来源(是什么技术造就了这一特色,或者说是什么架构造就了这一特色等),从而构建出最符合本身需求的技术方案。

没有最好,只有最适合。

相关文章
相关标签/搜索