分布式系统技术——分布式ID方案的思考

摘要

分布式ID是一种用于在分布式系统中生成全局唯一标识符的技术,解决了多节点、多服务间ID冲突和重复的问题。常见的分布式ID方案包括数据库自增ID、号段模式、Twitter的Snowflake算法等。Snowflake算法使用64位ID,由时间戳、机器ID、序列号组成,既能保证唯一性,又具备时间有序性,适合高并发场景。但是有关于分布式ID方案有很多问题值得我们思考,而不是的想到ID重复就是上分布式ID。要多想想分布式解决了什么问题?在什么场景中使用,使用有什么好处和坏处?那哪些好用方案,没有替代方案呢?这些都是的值的我们在方案设计的时候思考的。希望大家在设计系统的时候多思考一点为什么。

1. 分布式ID方案的思考

1.1. 为什么需要使用分布式ID,它解决了什么问题?

分布式ID是一种用于在分布式系统中生成全局唯一标识符(ID)的方法。它主要解决以下几个问题:

1.1.1. 唯一性问题(解决核心问题就是分布式系统中ID可能重复问题)

在分布式系统中,各个服务节点可能同时生成ID。为了避免不同节点生成相同的ID,需要一个全局唯一的ID生成机制。分布式ID确保在不同机器、不同时间生成的ID是唯一的。

1.1.2. 高可用性与性能(ID服务存在单点故障)

分布式系统的节点通常是水平扩展的,单一的数据库自增ID或UUID生成器可能成为性能瓶颈。分布式ID通过在多个节点上并行生成ID,减少了单点故障,提高了系统的可用性,并且通常能够以较高的速度生成大量ID。

1.1.3. 无中心化(不依赖于其他组件或者服务)

在分布式系统中,依赖一个集中式的ID生成器(如数据库自增主键)会带来延迟和瓶颈。分布式ID的生成通常不依赖中心化组件,比如通过雪花算法(Snowflake)可以让每个节点独立生成ID,降低了中心化系统的负载。

1.1.4. 顺序性(呈现递增的特点)

在某些场景下,ID生成的顺序性很重要,比如订单系统,ID越大表示时间越晚。分布式ID可以根据某些算法保证生成的ID具有一定的顺序性,便于后续的业务逻辑处理。、

1.2. ID可能重复的场景?什么场景需要使用的分布式ID?

使用数据库主键作为ID生成方式时,在某些特定场景下可能会出现ID重复问题,尤其是在分布式架构或特殊操作情况下。以下是一些可能导致ID重复的常见场景:

1.2.1. 布式环境下的主键冲突

  • 在分布式系统中,多个数据库实例同时写入数据。如果每个实例都依赖各自的数据库自增主键机制(如MySQL的AUTO_INCREMENT),那么不同实例之间生成的ID可能会冲突。因为每个实例的自增ID序列是独立的,无法保证全局唯一性。有可能多个数据ID是一样例如:id=1000.在的整个分布式系统中存在多条数据。
  • 解决方法:可以使用分布式ID生成机制(如雪花算法)或将ID生成交给一个集中式的服务来处理。

1.2.2. 数据库复制(Replication)中的冲突

  • 在主从数据库复制场景下,如果主从库同时进行写操作,并且都使用自增主键生成ID,可能导致主从库生成相同的ID。数据库复制通常设计为主库写入,从库只负责读操作,但如果主从库写入没有被严格限制,冲突可能发生。
  • 解决方法:限制写操作仅发生在主库,或对数据库进行合理的分片与协调。

1.2.3. 主键值的手动插入

  • 在某些场景中,开发人员可能手动插入主键ID值,特别是在初始化数据或数据迁移时。如果手动插入的ID值与数据库自增生成的ID值重复,插入操作将会失败,甚至导致ID冲突。
  • 解决方法:在手动插入ID时,确保与数据库的自增序列不冲突,可以通过查询当前自增序列的最大值,确保新插入的数据不使用重复的ID。

1.2.4. ID自增列在重新设置自增值时冲突

  • 当使用数据库自增主键时,如果通过SQL语句重置自增序列(例如ALTER TABLE设置自增的起始值),可能导致新插入的数据ID与已有数据ID冲突。如果手动调整的自增值小于或等于当前最大ID值,则新生成的ID可能会与已存在的ID重复。
  • 解决方法:重置自增值时需要确保设置的值大于现有表中最大的ID值。

1.2.5. 分库分表场景下的冲突

  • 在分库分表的情况下,不同数据库分片可能各自生成ID。如果不进行全局ID协调,不同数据库分片之间可能生成相同的ID。
  • 解决方法:在分库分表场景下,可以通过分布式ID生成器(如美团的Leaf、Twitter的Snowflake)来保证ID的全局唯一性。

1.2.6. 数据库重启或复制错误

  • 在某些情况下,数据库重启或复制过程中可能会导致自增序列的错乱。例如,如果主从库同步出错,导致主从库的自增ID不同步,那么从库在切换为主库时可能会生成重复的ID。
  • 解决方法:应确保数据库复制机制的稳定性,并在主从切换时严格控制自增ID的生成与同步。

1.2.7. 批量数据导入

  • 在批量导入数据的过程中,特别是当批量导入的数据已经带有主键时,如果新导入的数据主键值与数据库现有数据冲突,可能会导致重复ID。
  • 解决方法:在批量导入数据时,确保导入数据的主键不与现有数据冲突,或者在导入时采用新的分布式ID方案。

在使用数据库自增主键时,虽然在单个实例中自增ID通常不会冲突,但在分布式环境、主从复制、手动操作或分库分表场景下,可能会出现ID重复问题。为了解决这些问题,分布式系统中通常不依赖数据库自增ID,而是采用更复杂的分布式ID生成算法,如Snowflake、UUID等,以确保ID的全局唯一性。

1.3. 现有方案没有能够替带雪花算法吗?

分布式雪花算法(Snowflake)是非常流行的分布式ID生成方案,但并不是唯一的选择。在实际应用中,可以根据具体的业务场景、技术要求、以及系统复杂性设计自定义的ID生成方式。以下是几种常见的替代方案及其优缺点:

1.3.1. UUID(Universally Unique Identifier)

UUID是通用的唯一标识符生成算法,能保证全球范围内生成的ID都是唯一的。

优点:

  • 生成过程不依赖中心化的服务,每个节点都可以独立生成UUID。
  • 保证全球唯一性,简单可靠。

缺点:

  • UUID生成的ID较长(通常是128位),不适合做数据库主键,影响存储和检索效率。
  • UUID生成的ID没有顺序性,无法用于需要顺序的场景。

应用场景:

  • 不要求顺序性,且需要跨系统保证唯一性的场景(如分布式日志追踪、跨平台数据交互)

实现方式:

  • 使用Java等语言的标准库直接生成UUID,或使用第三方库。

1.3.2. 数据库生成ID(号段模式)

数据库号段模式通过数据库统一管理ID段,每次批量获取一组ID区间,用于分布式节点。它是一种比较简单的方案,适用于分布式场景。

优点:

  • 简单易实现,依赖现有的数据库系统。
  • ID自带有序性,可以按时间顺序生成。

缺点:

  • 数据库可能成为性能瓶颈,特别是在高并发场景下。
  • 需要依赖数据库,存在一定的中心化问题,数据库可用性影响ID生成。

应用场景:

  • 并发量较小、对性能要求不高的系统。

实现方式:

  • 通过数据库表存储当前的最大ID,每次批量获取一定区间的ID(如一次获取1000个),然后分配给各个节点使用。

1.3.3. 基于时间戳 + 机器ID + 序列号的自定义方案

自定义的ID生成方案可以根据系统的需求,结合时间戳、机器ID、序列号等信息生成唯一ID。这种方案类似于雪花算法,但可以根据不同的场景进行定制化。

优点:

  • 灵活,可以根据业务需求调整ID的组成部分。
  • ID通常较短(相比UUID),可以控制长度。
  • 能够保证一定的有序性,特别是基于时间戳的设计。

缺点:

  • 需要设计和维护机器ID分配方案,以确保不同机器或节点生成的ID不冲突。
  • 实现复杂度较高,可能需要额外的协调机制。

应用场景:

  • 需要自定义ID格式的场景,比如订单号、用户ID等。

实现方式:

  • ID的组成部分通常包括时间戳、机器ID和序列号:
    • 时间戳:确保不同时间生成的ID具有顺序性。
    • 机器ID:标识生成ID的节点,确保不同节点的ID不会冲突。
    • 序列号:用于解决同一时间同一节点生成多个ID的情况。

1.3.4. Leaf(美团开源的分布式ID生成服务)

Leaf是美团点评开源的分布式ID生成服务,提供两种ID生成模式:号段模式(基于数据库)和Snowflake模式(基于时间戳)。

优点:

  • 经过大规模应用验证,可靠性高。
  • 两种模式灵活适配不同的业务场景。

缺点:

  • 依赖额外的服务部署和维护,需要一定的运维成本。

应用场景:

  • 需要高度可靠和成熟的分布式ID生成方案,且能够承担一定的服务运维成本的业务。

实现方式:

  • Leaf提供了一整套服务端API和客户端SDK,系统通过调用API生成ID。

替代雪花算法的分布式ID生成方案有很多,每种方案都有其优势和适用场景。选择具体方案时需要根据业务需求、系统架构、性能要求、实现难度等因素综合考虑。对于大部分场景,基于时间戳、机器ID、序列号的自定义方案或者已有的分布式ID生成服务(如Leaf)是雪花算法之外的常用选择。

1.4. 各种分布式ID算法弊端和优势?

如下分布式ID生成方案将详细介绍其中有缺点设计。

1.5. 分布式ID生成是作为一个服务还是作为一个工具类?怎么选择呢?

分布式ID生成可以设计成独立的服务或工具类,选择哪种方案取决于系统的架构、业务需求、性能要求和维护成本等因素。以下是两种方案的对比及选择依据:

1.5.1. 分布式ID生成作为独立服务思考

这种方案是将ID生成逻辑抽象为一个独立的服务,所有需要ID的系统通过API或RPC调用该服务生成ID。常见的方案有类似美团开源的Leaf、Twitter的Snowflake等独立服务。

1.5.1.1. 优点:
  • 全局唯一性:ID生成逻辑集中管理,可以确保在所有节点和服务之间生成的ID都唯一。
  • 可扩展性强:通过水平扩展ID生成服务,可以应对大规模并发场景。
  • 一致性保障:集中管理的ID生成服务便于在分布式环境中维护一致性,比如在多数据中心、跨地域部署的场景下,确保ID不会重复。
  • 灵活性高:独立服务可以根据业务需求调整ID生成规则,支持复杂的场景(如多种ID格式、支持不同业务线等)。
1.5.1.2. 缺点:
  • 维护成本高:需要单独维护一个服务,包含服务的部署、监控、日志等,增加了运维负担。
  • 网络延迟:每次生成ID都需要通过网络请求,可能增加响应时间,尤其在高并发场景下。
  • 服务可靠性要求高:ID生成服务的可用性至关重要,一旦服务不可用,整个系统可能无法生成新的ID,导致部分功能中断。
1.5.1.3. 适用场景:
  • 大规模分布式系统:需要多个服务、多个节点生成全局唯一ID。
  • 高并发系统:系统具有较高的并发性,需要强大的分布式ID生成能力。
  • 跨服务、跨业务线:需要在不同服务或业务线之间生成统一格式的ID,集中管理方便维护。
1.5.1.4. 实现方式:
  • 搭建一个分布式ID生成服务,所有业务通过HTTP、RPC或其他协议调用这个服务生成ID。ID生成服务可以根据业务量水平扩展。
public class IdGeneratorService {
    public long generateId() {
        // 调用分布式ID生成服务
        return httpClient.call("http://id-gen-service/generateId");
    }
}

1.5.2. 分布式ID生成作为工具类思考

这种方案是将ID生成的逻辑嵌入到应用程序中,作为一个工具类在各个服务内独立运行。每个服务节点都可以独立生成ID,而不依赖外部服务。

1.5.2.1. 优点:
  • 简单易用:无需单独维护一个服务,直接嵌入到业务代码中,开发和运维成本较低。
  • 无网络延迟:ID生成完全在本地进行,避免了网络请求带来的延迟,性能较好。
  • 高可用性:工具类没有外部依赖,不存在服务宕机导致不可用的问题。
1.5.2.2. 缺点:
  • ID冲突风险:在分布式系统中,每个节点独立生成ID,如何保证全局唯一性是一个挑战。通常需要依赖某种全局机制(如机器ID)来保证不同节点生成的ID不会冲突。
  • 扩展性受限:如果系统需要大规模横向扩展,工具类的扩展能力较弱,特别是在多数据中心、跨地域场景下,可能需要额外的协调机制。
1.5.2.3. 适用场景:
  • 小规模系统:系统规模较小,分布式节点数量有限,不需要特别复杂的ID生成机制。
  • 单体服务或微服务架构:在单体应用或微服务应用中,每个服务节点独立生成ID,且不要求跨服务的ID唯一性。
1.5.2.4. 实现方式:
  • 在业务代码中创建一个工具类,利用类似Snowflake算法等方式独立生成ID。
public class IdGeneratorUtil {
    private static final long workerId = 1L; // 机器ID
    private static final long datacenterId = 1L; // 数据中心ID

    public static long generateId() {
        // 使用Snowflake算法生成唯一ID
        return SnowflakeIdGenerator.getInstance(workerId, datacenterId).nextId();
    }
}

1.5.3. 作为一个服务还是作为一个工具类如何选择?

业务复杂性

  • 如果业务相对简单,不涉及大规模的分布式系统,使用工具类嵌入在业务系统中可能更合适。如果业务复杂,特别是多服务、多业务线的场景,独立服务方案能更好地满足全局唯一性要求,并支持横向扩展。

性能需求

  • 如果系统对性能要求极高,工具类生成ID没有网络开销,能显著提升性能。
  • 如果系统有较高的并发需求,但对ID生成的延迟敏感度较低,独立服务方案也可以通过水平扩展来满足需求。

可扩展性要求

  • 如果业务可能会持续扩展,并且未来需要跨多个服务、数据中心生成全局唯一的ID,独立服务方案更易扩展。
  • 如果系统规模有限,工具类方案足以满足需求,且易于实现。

运维成本

  • 独立服务方案需要额外的部署、监控、维护成本,适用于有完善运维能力的公司。
  • 工具类的实现更为轻量,开发和运维成本低,适用于中小型企业或简单场景。

1.5.4. 总结

  • 分布式ID生成服务:适用于大规模分布式系统、需要全局唯一性和高并发的场景。
  • 分布式ID生成工具类:适用于中小规模的系统,业务复杂度低且不需要跨服务唯一性的场景。

两种方式各有优劣,最终的选择应结合具体的系统需求、业务场景以及未来的扩展性考虑。

2. 分布式ID生成方案

在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。如在美团点评的金融、支付、餐饮、酒店、猫眼电影等产品的系统中,数据日渐增长, 对数据分库分表后需要有一个唯一ID来标识一条数据或消息,数据库的自增ID显然不能满足需求;特别一点的如订单、骑手、优惠券也都需要有唯一ID做标识。 此时一个能够生成全局唯一ID的系统是非常必要的。因此需要设计具有一下特点额ID:

  • 全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求。
  • 趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
  • 单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
  • 信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则。

2.1. UUID算法ID

UUID算法的定义:标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符, 通常使用UUID的格式为:时间戳(当前日期+时间)+时钟序列+机器识别号(MAC、其它)。到目前为止业界一共有5种方式生成UUID算法:

  1. 时间:(注意有非时钟回拨的问题)
  2. DCE算法
  3. MD5算法
  4. 随机数算法
  5. SHA1算法

UUID算法优点:

  • 性能非常高:本地生成,没有网络消耗。

UUID算法缺点:

  • 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
  • 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用:MySQL官方有明确的建议主键要尽量越短越好, 36个字符长度的UUID不符合要求。对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。

2.2. 数据库自增

以MySQL举例,利用给字段设置auto-increment-incrementauto_increment_offset来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号。

数据库自增ID的优点:

  • 非常简单,利用现有数据库系统的功能实现,成本小,有DBA专业维护。
  • ID号单调自增,可以实现一些对ID有特殊要求的业务。

数据库自增ID的缺点:

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

2.3. 基于数据库+段号模式自增

对于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 = 2
auto-increment-offset = 1

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

2.4. Leaf-segment算法

Leaf-segment方案可以生成趋势递增的ID,同时ID号是可计算的,不适用于订单ID生成场景,比如竞对在两天中午12点分别下单,通过订单id号相减就能大致计算出公司一天的订单量,这个是不能忍受的。面对这一问题,我们可以采用Leaf-snowflake方案。Leaf-snowflake方案完全沿用snowflake方案的bit位设计,即是“1+41+10+12”的方式组装ID号。对于workerID的分配,当服务集群数量较小的情况下,完全可以手动配置。Leaf服务规模较大,动手配置成本太高。所以使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID。Leaf-snowflake是按照下面几个步骤启动的:

弱依赖ZooKeeper:除了每次会去ZK拿数据以外,也会在本机文件系统上缓存一个workerID文件。当ZooKeeper出现问题,恰好机器出现问题需要重启时,能保证服务能够正常启动。这样做到了对三方组件的弱依赖。一定程度上提高了SLA。

解决时钟问题:因为这种方案依赖时间,如果机器的时钟发生了回拨,那么就会有可能生成重复的ID号,需要解决时钟回退的问题。

参见上图整个启动流程图,服务启动时首先检查自己是否写过ZooKeeper leaf_forever节点:

  1. 若写过,则用自身系统时间与leaf_forever/${self}节点记录时间做比较,若小于leaf_forever/${self}时间则认为机器时间发生了大步长回拨,服务启动失败并报警。
  2. 若未写过,证明是新服务节点,直接创建持久节点leaf_forever/${self}并写入自身系统时间,接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IP:Port,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize。
  3. 若abs( 系统时间-sum(time)/nodeSize ) < 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary/${self} 维持租约。
  4. 否则认为本机系统时间发生大步长偏移,启动失败并报警。
  5. 每隔一段时间(3s)上报自身系统时间写入leaf_forever/${self}。

由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警,在美团在2017年闰秒出现那一次出现过部分机器回拨,由于Leaf-snowflake的策略保证,成功避免了对业务造成的影响。

//发生了回拨,此刻时间小于上次发号时间
 if (timestamp < lastTimestamp) {
  			  
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                try {
                	//时间偏差大小小于5ms,则等待两倍时间
                    wait(offset << 1);//wait
                    timestamp = timeGen();
                    if (timestamp < lastTimestamp) {
                       //还是小于,抛异常并上报
                        throwClockBackwardsEx(timestamp);
                      }    
                } catch (InterruptedException e) {  
                   throw  e;
                }
            } else {
                //throw
                throwClockBackwardsEx(timestamp);
            }
        }
 //分配ID

2.4.1. 数据库ID的优化方案

  • 原方案每次获取ID都得读写一次数据库,造成数据库压力大。改为利用proxy server批量获取, 每次获取一个segment(step决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。
  • 各个业务不同的发号需求用biz_tag字段来区分,每个biz-tag的ID获取相互隔离, 互不影响。如果以后有性能需求需要对数据库扩容,不需要上述描述的复杂的扩容操作,只需要对biz_tag分库分表就行。

重要字段说明:

  • biz_tag用来区分业务,
  • max_id表示该biz_tag目前所被分配的ID号段的最大值,
  • step表示每次分配的号段长度。原来获取ID每次都需要写数据库,现在只需要把step设置得足够大,比如1000。 那么只有当1000个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从1减小到了1/step。

Leaf-segment算法优点:

  • Leaf服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景。
  • ID号码是趋势递增的8byte的64位数字,满足上述数据库存储的主键要求。
  • 容灾性高:Leaf服务内部有号段缓存,即使DB宕机,短时间内Leaf仍能正常对外提供服务。
  • 可以自定义max_id的大小,非常方便业务从原有的ID方式上迁移过来。

Leaf-segment算法缺点

  • ID号码不够随机,能够泄露发号数量的信息,不太安全。
  • TP999数据波动大,当号段使用完之后还是会hang在更新数据库的I/O上,tg999数据会出现偶尔的尖刺。(主要是在获取新的号段的时候,可能有请求导致的数据尖刺。)
  • DB宕机会造成整个系统不可用。

2.4.2. 双buffer优化方案

采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新, 则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。

  • 每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。
  • 每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新。

2.5. Leaf-segment高可用容灾方案

对于第三点“DB可用性”问题,我们目前采用一主两从的方式,同时分机房部署,Master和Slave之间采用半同步方式同步数据。 同时使用公司Atlas数据库中间件(已开源,改名为DBProxy)做主从切换。当然这种方案在一些情况会退化成异步模式, 甚至在非常极端情况下仍然会造成数据不一致的情况,但是出现的概率非常小。如果你的系统要保证100%的数据强一致, 可以选择使用“类Paxos算法”实现的强一致MySQL方案。

同时Leaf服务分IDC部署,内部的服务化框架是“MTthrift RPC”。服务调用的时候,根据负载均衡算法会优先调用同机房的Leaf服务。在该IDC内Leaf服务不可用的时候才会选择其他机房的Leaf服务。同时服务治理平台OCTO还提供了针对服务的过载保护、一键截流、动态流量分配等对服务的保护措施。

2.6. Snowflake雪花算法

这种方案大致来说是一种以划分命名空间(UUID也算,由于比较常见,所以单独分析)来生成ID的一种算法, Mongdb objectID算作是和snowflake类似方法,通过“时间+机器码+pid+inc”共12个字节,通过4+3+2+3的方式最终标识成一个24长度的十六进制字符。 这种方案把64-bit分别划分成多段,分开来标示机器、时间等,比如在snowflake中的64-bit分别表示如下图所示:

  • 41-bit的时间可以表示(1L<<41)/(1000L360024*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都是不同的。

Snowflake优点:

  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
  • 可以根据自身业务特性分配bit位,非常灵活。

雪花算法(Snowflake Algorithm)生成的ID通常是一个64位的整数。因此,在数据库中存储这些ID时,通常使用整数类型,而不是字符串类型。选择整数类型的好处在于它占用的存储空间更小,查询速度更快,索引效率更高。

Snowflake缺点:

  • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

生成超过的最大(4096个)?

Snowflake一毫秒的能够产生的最大的个数是4096个。如果是的超过的4096那就等到下一秒的来生成的。Snowflake算法1s生成的ID是300W+的ID。

2.6.1. 时钟回拨问题解决方案

防止时钟回拨 因为机器的原因会发生时间回拨,我们的雪花算法是强依赖我们的时间的,如果时间发生回拨,有可能会生成重复的ID,在我们上面的nextId中我们用当前时间和上一次的时间进行判断, 如果当前时间小于上一次的时间那么肯定是发生了回拨,普通的算法会直接抛出异常,这里我们可以对其进行优化,一般分为两个情况:

  1. 如果时间回拨时间较短,比如配置5ms以内,那么可以直接等待一定的时间,让机器的时间追上来。
  2. 如果时间的回拨时间较长,我们不能接受这么长的阻塞等待,那么又有两个策略:
    1. 直接拒绝,抛出异常,打日志,通知运维时钟回滚。
    2. 利用扩展位,上面我们讨论过不同业务场景位数可能用不到那么多,那么我们可以把扩展位数利用起来了, 比如当这个时间回拨比较长的时候,我们可以不需要等待,直接在扩展位加1。位的扩展位允许我们有3次大的时钟回拨,一般来说就够了,如果其超过三次我们还是选择抛出异常,打日志。

2.7. 基于Redis模式单线程模式

Redis也同样可以实现,原理就是利用redis的 incr命令实现ID的原子性自增。

127.0.0.1:6379> set seq_id 1     // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id      // 增加1,并返回递增后的数值
(integer) 2

用redis实现需要注意一点,要考虑到redis持久化的问题。redis有两种持久化方式RDB和AOF。

  • RDB会定时打一个快照进行持久化,假如连续自增但redis没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况。
    • AOF会对每条写命令进行持久化,即使Redis挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis重启恢复的数据时间过长。

2.8. 基于ZK数据唯一性质模式

在过去的单库单表型系统中,通常可以使用数据库字段自带的auto_increment 属性来自动为每条记录生成一个唯一的ID。但是分库分表后,就无法在依靠数据库的 auto_increment属性来唯一标识一条记录了。此时我们就可以用zookeeper在分布式环 境下生成全局唯一ID。

实现方式有两种,一种通过节点,一种通过节点的版本号

  • 节点的特性 ,持久顺序节点(PERSISTENT_SEQUENTIAL)
    他的基本特性和持久节点是一致的,额外的特性表现在顺序性上。在ZooKeeper中,每个父节点都会为他的第一级子节点维护一份顺序,用于记录下每个子节点创建的先后顺序。基于这个顺序特性,在创建子节点的时候,可以设置这个标记,那么在创建节点过程中,ZooKeeper会自动为给定节点加上一个数字后缀,作为一个新的、完整的节点名。另外需要注意的是,这个数字后缀的上限是整型的最大值。
  • 版本-保证分布式数据原子性操作
    ZooKeeper中为数据节点引入了版本的概念,每个数据节点都具有三种类型的版本信息,对数据节点的任何更新操作都会引起版本号的变化。

2.9. Tinyid算法

Tinyid是基于号段模式原理实现的与Leaf如出一辙,每个服务获取一个号段(1000,2000]、(2000,3000]、(3000,4000]

创建数据表:

CREATE TABLE `tiny_id_info` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `biz_type` varchar(63) NOT NULL DEFAULT '' COMMENT '业务类型,唯一',
  `begin_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '开始id,仅记录初始值,无其他含义。初始化时begin_id和max_id应相同',
  `max_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '当前最大id',
  `step` int(11) DEFAULT '0' COMMENT '步长',
  `delta` int(11) NOT NULL DEFAULT '1' COMMENT '每次id增量',
  `remainder` int(11) NOT NULL DEFAULT '0' COMMENT '余数',
  `create_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '更新时间',
  `version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_biz_type` (`biz_type`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT 'id信息表';

CREATE TABLE `tiny_id_token` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id',
  `token` varchar(255) NOT NULL DEFAULT '' COMMENT 'token',
  `biz_type` varchar(63) NOT NULL DEFAULT '' COMMENT '此token可访问的业务类型标识',
  `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
  `create_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT 'token信息表';

INSERT INTO `tiny_id_info` (`id`, `biz_type`, `begin_id`, `max_id`, `step`, `delta`, `remainder`, `create_time`, `update_time`, `version`)
VALUES
    (1, 'test', 1, 1, 100000, 1, 0, '2018-07-21 23:52:58', '2018-07-22 23:19:27', 1);

INSERT INTO `tiny_id_info` (`id`, `biz_type`, `begin_id`, `max_id`, `step`, `delta`, `remainder`, `create_time`, `update_time`, `version`)
VALUES
    (2, 'test_odd', 1, 1, 100000, 2, 1, '2018-07-21 23:52:58', '2018-07-23 00:39:24', 3);


INSERT INTO `tiny_id_token` (`id`, `token`, `biz_type`, `remark`, `create_time`, `update_time`)
VALUES
    (1, '0f673adf80504e2eaa552f5d791b644c', 'test', '1', '2017-12-14 16:36:46', '2017-12-14 16:36:48');

INSERT INTO `tiny_id_token` (`id`, `token`, `biz_type`, `remark`, `create_time`, `update_time`)
VALUES
    (2, '0f673adf80504e2eaa552f5d791b644c', 'test_odd', '1', '2017-12-14 16:36:46', '2017-12-14 16:36:48');

配置数据库:

datasource.tinyid.names=primary
datasource.tinyid.primary.driver-class-name=com.mysql.jdbc.Driver
datasource.tinyid.primary.url=jdbc:mysql://192.168.25.136:3306/tinyidtest?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
datasource.tinyid.primary.username=root
datasource.tinyid.primary.password=root

启动tinyid-server后测试

获取分布式自增ID: http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&token=0f673adf80504e2eaa552f5d791b644c'
返回结果: 3

批量获取分布式自增ID: http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&token=0f673adf80504e2eaa552f5d791b644c&batchSize=10'
返回结果:  4,5,6,7,8,9,10,11,12,13

Java客户端方式接入

<dependency>
    <groupId>com.xiaoju.uemc.tinyid</groupId>
    <artifactId>tinyid-client</artifactId>
    <version>${tinyid.version}</version>
</dependency>
# 配置文件
tinyid.server =localhost:9999
tinyid.token =0f673adf80504e2eaa552f5d791b644c

test 、tinyid.token是在数据库表中预先插入的数据,test 是具体业务类型,tinyid.token表示可访问的业务类型

// 获取单个分布式自增ID
Long id =  TinyId.nextId("test");

// 按需批量分布式自增ID
List< Long > ids =  TinyId.nextId("test" , 10);

3. 博文参考

uid-generator https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md

Leaf github地址:https://github.com/Meituan-Dianping/Leaf

Tinyid Github地址:https://github.com/didi/tinyid

Leaf——美团点评分布式ID生成系统 - 美团技术团队

snowflake/IdWorker.scala at snowflake-2010 · twitter-archive/snowflake · GitHub

https://segmentfault.com/a/1190000022717820

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

庄小焱

我将坚持分享更多知识

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值