在进行分布式服务开发时,绕不开的一个问题就是分布式唯一ID的生成,现在apache开源了这个框架:分布式唯一ID生成框架cosID,开源地址:https://gitee.com/AhooWang/CosId
在这里插入图片描述
这里只说两种通用的算法:雪花算法(SnowflakeId)与号段链模式(SegmentChainId)

雪花算法SnowflakeId

在这里插入图片描述
SnowflakeId使用Long(64-bit)位分区来生成ID的一种分布式ID算法。 通用的位分配方案为:timestamp(41-bit)+machineId(10-bit)+sequence(12-bit)=63-bit。

  1. 41-bittimestamp=(1L<<41)/(1000/3600/24/365),约可以存储69年的时间戳,即可以使用的绝对时间为EPOCH+69年,一般我们需要自定义EPOCH为产品开发时间,另外还可以通过压缩其他区域的分配位数,来增加时间戳位数来延长可用时间。

  2. 10-bitmachineId=(1L<<10)=1024,即相同业务可以部署1024个副本(在Kubernetes概念里没有主从副本之分,这里直接沿用Kubernetes的定义)。一般情况下没有必要使用这么多位,所以会根据部署规模需要重新定义。

  3. 12-bitsequence=(1L<<12)1000=4096000,即单机每秒可生成约409W的ID,全局同业务集群可产生40960001024=419430W=41.9亿(TPS)。

从 SnowflakeId 设计上可以看出:

timestamp在高位,单实例SnowflakeId是会保证时钟总是向前的(校验本机时钟回拨),所以是本机单调递增的。受全局时钟同步/时钟回拨影响SnowflakeId是全局趋势递增的。

SnowflakeId不对任何第三方中间件有强依赖关系,并且性能也非常高。
位分配方案可以按照业务系统需要灵活配置,来达到最优使用效果。

强依赖本机时钟,潜在的时钟回拨问题会导致ID重复、处于短暂的不可用状态。
machineId需要手动设置,实际部署时如果采用手动分配machineId,会非常低效。

SnowflakeId之机器号分配问题

在SnowflakeId中根据业务设计的位分配方案确定了基本上就不再有变更了,也很少需要维护。但是machineId总是需要配置的,而且集群中是不能重复的,否则分区原则就会被破坏而导致ID唯一性原则破坏,当集群规模较大时machineId的维护工作是非常繁琐,低效的。

有一点需要特别说明的,SnowflakeId的MachineId是逻辑上的概念,而不是物理概念。 想象一下假设MachineId是物理上的,那么意味着一台机器拥有只能拥有一个MachineId,那会产生什么问题呢?

目前 CosId 提供了以下五种 MachineId 分配器

  1. ManualMachineIdDistributor: 手动配置machineId,一般只有在集群规模非常小的时候才有可能使用,不推荐。
  2. StatefulSetMachineIdDistributor: 使用Kubernetes的StatefulSet提供的稳定的标识ID(HOSTNAME=service-01)作为机器号。
  3. RedisMachineIdDistributor: 使用Redis作为机器号的分发存储,同时还会存储MachineId的上一次时间戳,用于启动时时钟回拨的检查。
  4. JdbcMachineIdDistributor: 使用关系型数据库作为机器号的分发存储,同时还会存储MachineId的上一次时间戳,用于启动时时钟回拨的检查。
  5. ZookeeperMachineIdDistributor: 使用ZooKeeper作为机器号的分发存储,同时还会存储MachineId的上一次时间戳,用于启动时时钟回拨的检查。

SnowflakeId之时钟回拨问题

时钟回拨的致命问题是会导致ID重复、冲突(这一点不难理解),ID重复显然是不能被容忍的。 在SnowflakeId算法中,按照MachineId分区ID,我们不难理解的是不同MachineId是不可能产生相同ID的。所以我们解决的时钟回拨问题是指当前MachineId的时钟回拨问题,而不是所有集群节点的时钟回拨问题。

MachineId时钟回拨问题大体可以分为俩种情况:

  1. 运行时时钟回拨:即在运行时获取的当前时间戳比上一次获取的时间戳小。这个场景的时钟回拨是很容易处理的,一般SnowflakeId代码实现时都会存储lastTimestamp用于运行时时钟回拨的检查,并抛出时钟回拨异常。
    时钟回拨时直接抛出异常是不太好地实践,因为下游使用方几乎没有其他处理方案(噢,我还能怎么办呢,等吧),时钟同步是唯一的选择,当只有一种选择时就不要再让用户选择了。
    ClockSyncSnowflakeId是SnowflakeId的包装器,当发生时钟回拨时会使用ClockBackwardsSynchronizer主动等待时钟同步来重新生成ID,提供更加友好的使用体验。

  2. 启动时时钟回拨:即在启动服务实例时获取的当前时钟比上次关闭服务时小。此时的lastTimestamp是无法存储在进程内存中的。当获取的外部存储的机器状态大于当前时钟时钟时,会使用ClockBackwardsSynchronizer主动同步时钟。
    LocalMachineStateStorage:使用本地文件存储MachineState(机器号、最近一次时间戳)。因为使用的是本地文件所以只有当实例的部署环境是稳定的,LocalMachineStateStorage才适用。
    RedisMachineIdDistributor:将MachineState存储在Redis分布式缓存中,这样可以保证总是可以获取到上次服务实例停机时机器状态。

SnowflakeId之JavaScript数值溢出问题

JavaScript的Number.MAX_SAFE_INTEGER只有53-bit,如果直接将63位的SnowflakeId返回给前端,那么会产生值溢出的情况(所以这里我们应该知道后端传给前端的long值溢出问题,迟早会出现,只不过SnowflakeId出现得更快而已)。 很显然溢出是不能被接受的,一般可以使用以下俩种处理方案:

  1. 将生成的63-bitSnowflakeId转换为String类型。直接将long转换成String。
    使用SnowflakeFriendlyId将SnowflakeId转换成比较友好的字符串表示:{timestamp}-{machineId}-{sequence} -> 20210623131730192-1-0
  2. 自定义SnowflakeId位分配来缩短SnowflakeId的位数(53-bit)使 ID 提供给前端时不溢出
    使用SafeJavaScriptSnowflakeId(JavaScript 安全的 SnowflakeId)

号段链模式(SegmentChainId)

号段链模式是增加版的号段模式,一般的号段模式是:
在这里插入图片描述
从上面的设计图中,不难看出号段模式基本设计思路是通过每次获取一定长度(Step)的可用ID(Id段/号段),来降低网络IO请求次数,提升性能。

  • 强依赖第三方号段分发器,可用性受到第三方分发器影响。

  • 每次号段用完时获取NextMaxId需要进行网络IO请求,此时的性能会比较低。

  • 单实例ID单调递增,全局趋势递增。
    从设计图中不难看出Instance 1每次获取的NextMaxId,一定比上一次大,意味着下一次的号段一定比上一次大,所以从单实例上来看是单调递增的。
    多实例各自持有的不同的号段,意味着同一时刻不同实例生成的ID是乱序的,但是整体趋势的递增的,所以全局趋势递增。

  • ID乱序程度受到Step长度以及集群规模影响(从趋势递增图中不难看出)。
    假设集群中只有一个实例时号段模式就是单调递增的。Step越小,乱序程度越小。当Step=1时,将无限接近单调递增。需要注意的是这里是无限接近而非等于单调递增,具体原因你可以思考一下这样一个场景:
    号段分发器T1时刻给Instance 1分发了ID=1,T2时刻给Instance 2分发了ID=2。因为机器性能、网络等原因,Instance 2网络IO写请求先于Instance 1到达。那么这个时候对于数据库来说,ID依然是乱序的。

为了解决上面的缺点问题,在号段模式的基础上面,cosID添加了号段链模式:
在这里插入图片描述

  • 稳定性:SegmentId的稳定性问题(P9999=46.624(us/op))主要是因为号段用完之后同步进行NextMaxId的获取导致的(会产生网络IO)。
  • SegmentChainId (P9999=0.208(us/op))引入了新的角色PrefetchWorker用以维护和保证安全距离,理想情况下使得获取ID的线程几乎完全不需要进行同步的等待NextMaxId获取,性能可达到近似 AtomicLong 的 TPS 性能:12743W+/s JMH 基准测试 。
  • 适应性:从SegmentId介绍中我们知道了影响ID乱序的因素有俩个:集群规模、Step大小。集群规模是我们不能控制的,但是Step是可以调节的。Step应该近可能小才能使得ID单调递增的可能性增大。
    Step太小会影响吞吐量,那么我们如何合理设置Step呢?答案是我们无法准确预估所有时点的吞吐量需求,那么最好的办法是吞吐量需求高时,Step自动增大,吞吐量低时Step自动收缩。
  • SegmentChainId引入了饥饿状态的概念,PrefetchWorker会根据饥饿状态检测当前安全距离是否需要膨胀或者收缩,以便获得吞吐量与有序性之间的权衡,这便是SegmentChainId的自适应性。

项目中使用

添加maven依赖

        <dependency>
            <groupId>me.ahoo.cosid</groupId>
            <artifactId>cosid-spring-boot-starter</artifactId>
            <version>2.15.2</version>
        </dependency>

在项目使用配置使用雪花算法

添加算法算法的配置:

cosid:
  namespace: ${spring.application.name}   # 命名空间,防止多个不同的项目相互冲突
  enabled: true  # 是否启动cosId
  machine:
    enabled: true  # 是否启用machine管理
    stable: true  # 服务重启后,是否保持machine ID的稳定,如果不配置,会自动管理,具体方式取决于distributor type
    distributor:
      type: MANUAL    # machine id管理方式,MANUAL 手动,STATEFUL_SET 基于Kubernetes StatefulSet顺序
      # JDBC 基于关系数据库管理 ,REDIS  等
      manual:
        machine-id: 1  # 手动分配的id,不同的实例不重复即可。
  snowflake:
    enabled: true  # 启动雪花算法
    share:
      enabled: true
      clock-sync: true  # 如果出现时钟回拨的现象,不抛异常,而是等待

因为cosId对于分布式ID的生成进行了抽象设计,在业务中统一使用一个方法生成分布式Id,具体是什么算法都是由配置实现的

 /**
     * 使用cosID生成唯一id,具体使用哪种算法,通过配置实现
     * 
     * @return
     */
    public static String getStringUidOfCosId() {
        IdGenerator idGenerator = SpringUtil.getBean(IdGenerator.class);
        return idGenerator.generateAsString();
    }

    /**
     * 使用cosID生成唯一id,具体使用哪种算法,通过配置实现
     *
     * @return
     */
    public static long getLongUidOfCosId() {
        IdGenerator idGenerator = SpringUtil.getBean(IdGenerator.class);
        return idGenerator.generate();
    }

    // 测试代 码
    private static void testUid() {
        String uid = JwUidUtil.getStringUidOfCosId();
        System.out.println(uid);
        for (int i = 0; i < 20; i++) {
            long uid2 = JwUidUtil.getLongUidOfCosId();
            System.out.println(uid2);
        }
    }

输出结果:
在这里插入图片描述

使用号段链模式
添加maven依赖

        <dependency>
            <groupId>me.ahoo.cosid</groupId>
            <artifactId>cosid-spring-boot-starter</artifactId>
            <version>2.15.2</version>
        </dependency>
        <1-- 因为要使用redis管理号段,所以加上这个依赖 -->
        <dependency>
            <groupId>me.ahoo.cosid</groupId>
            <artifactId>cosid-spring-redis</artifactId>
            <version>2.15.2</version>
        </dependency>

配置:

cosid:
  namespace: ${spring.application.name}   # 命名空间,防止多个不同的项目相互冲突
  enabled: true  # 是否启动cosId
  machine:
    enabled: true  # 是否启用machine管理
    stable: true  # 服务重启后,是否保持machine ID的稳定,如果不配置,会自动管理,具体方式取决于distributor type
    distributor:
      type: MANUAL    # machine id管理方式,MANUAL 手动,STATEFUL_SET 基于Kubernetes StatefulSet顺序
      # JDBC 基于关系数据库管理 ,REDIS  等
      manual:
        machine-id: 1  # 手动分配的id,不同的实例不重复即可。
  snowflake:
    enabled: false  # 关闭雪花算法
    share:
      enabled: true
      clock-sync: true  # 如果出现时钟回拨的现象,不抛异常,而是等待
  segment:
    enabled: true  # 启用号段模式
    mode: CHAIN    # 默认模式,号段链模式
    distributor:
      type: REDIS  # 使用redis管理号段
      redis:
        timeout: 6000
    chain:
      prefetch-worker:
        core-pool-size: 1   # 守护者线程池大小

再扫执行测试方法输出为:
在这里插入图片描述
可以看到生成的id是递增的。

这里只是简单的配置演示,真实的应用中,可以根据自己的需要进行配置调整,cosID还支持配置不同的业务使用不同的id算法,可以参考官方例子,点击跳转

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐