zoukankan      html  css  js  c++  java
  • 分布式全局唯一ID的实现

    分布式全局唯一ID的实现

    前言

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

    概念

    在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。

    如在金融、电商、支付、等产品的系统中,数据日渐增长,对数据分库分表后需要有一个唯一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等。

    实现方案:

    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没有银弹,我们要做的是多去了解现有的技术方案,再产生符合自己需求的技术方案。因为不同的技术方案都因为其使用场景有着各自的特点,而我们需要了解各种特点的技术来源(是什么技术造就了这一特点,或者说是什么架构造就了这一特点等),从而构建出最符合自己需求的技术方案。

    没有最好,只有最适合。

  • 相关阅读:
    MQTT TLS 加密传输
    python多进程并发redis
    各种消息队列的特点
    mqtt异步publish方法
    Numpy API Analysis
    Karma install steps for unit test of Angular JS app
    reinstall bower command
    Simulate getter in JavaScript by valueOf and toString method
    How to: Raise and Consume Events
    获取对象的类型信息 (JavaScript)
  • 原文地址:https://www.cnblogs.com/JavaHxm/p/11636530.html
Copyright © 2011-2022 走看看