生成分布式ID的四种常见方案对比

简介:

这篇博客从零开始,介绍了分布式id的使用场景和重要性,并且列举了目前最常见的分布式id生成方法。包括 UUID、单独自增主键、雪花算法、Redis的incr命令等。

分布式id:分布式集群环境下的全局唯一ID

为什么需要分布式ID?

举例:在Mysql分表的情况下,如果每个表都使用自增主键,那么后期数据将无法根据id区分唯一的数据项。主键会存在冲突(因为都是按顺序自增)



一、分布式ID方案之 UUID(可以用)

UUID:全称是universally Unique Identifier (通用唯一识别码),生成uuid重复的问题几乎可以忽略不急,概率非常低。JAVA语言中得到uuid可以使用java.util包提供的方法 java.util.UUID.randomUUID()
缺点: 内容长且随机,如果uuid用作索引对查询的性能提升很小
使用:使用简单,只要能忍受带来的缺点就可以使用,并且目前也有很多公司在用



二、分布式ID方案之 独立数据库自增ID(不推荐)

自增方案

  1. 独立数据库,创建一个不保存业务数据的专门用来生成主键的表
  2. 每次插入数据前,向主键表插入一条数据,利用其自增的主键当做业务表的数据主键


优缺点分析

  1. 优点:解决了业务表分表后使用自增id重复的问题,并且可以生成整形等利用索引的格式
  2. 缺点:可靠性和性能都受到影响,生成id必须要链接数据库增加了性能消耗。并且给系统增加了不稳定因素,Mysql挂了就没法儿用了。

使用:很少使用,不是很推荐



三、分布式ID方案之 雪花算法Snowflake(可以用)

3.1 Snowflake介绍

什么是snowflake: 是推特推出的一个用于生成分布式id的策略,基于这个算法生成的id是一个long型。在java中long型是一个8字节,算下来有64bit。
国内的互联⽹公司也基于上述的⽅案封装了⼀些分布式ID⽣成器,⽐如滴滴的tinyid(基于数据库实现)、百度的uidgenerator(基于SnowFlake)和美团的leaf(基于数据库和SnowFlake)

snowflake原理
39e5b31a45f63d31ef954559fe9cb09.png

  1. 符号位: 固定为0,二进制表示最高位是符号位,0代表正数,1代表负数
  2. 时间戳: 41个二进制数用来记录时间截,表示某一个毫秒 (毫秒级)
  3. 机器id:代表当前算法运行机器的id
  4. 序列号:12位,用来记录某个机器同一个毫秒内产生的不同序列号,代表同一个机器同一个毫秒可以产生的ID序号。

优点:

  1. 由于有机器号,可以保证在分布式环境下的id唯一性
  2. 序列号的机制,保证了其在单机上生成的id的有序性

缺点:

  1. 依赖于时间一致性,如果出现时间回拨的情况,就可能出现问题
  2. 在单机上递增。在分布式多态机器上,只是大致递增趋势不会严格递增。

3.2 JAVA的snowflake源码

/**
* 官方推出,Scala编程语言来实现的
* Java前辈用Java语言实现了雪花算法
*/
public class IdWorker{

    //下面两个每个5位,加起来就是10位的工作机器id
    private long workerId;    //工作id
    private long datacenterId;   //数据id
    //12位的序列号
    private long sequence;

    public IdWorker(long workerId, long datacenterId, long sequence){
        // sanity check for workerId
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
        }
        System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
                          timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);

        this.workerId = workerId;
        this.datacenterId = datacenterId;
        this.sequence = sequence;
    }

    //初始时间戳
    private long twepoch = 1288834974657L;

    //长度为5位
    private long workerIdBits = 5L;
    private long datacenterIdBits = 5L;
    //最大值
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    //序列号id长度
    private long sequenceBits = 12L;
    //序列号最大值
    private long sequenceMask = -1L ^ (-1L << sequenceBits);

    //工作id需要左移的位数,12位
    private long workerIdShift = sequenceBits;
    //数据id需要左移位数 12+5=17位
    private long datacenterIdShift = sequenceBits + workerIdBits;
    //时间戳需要左移位数 12+5+5=22位
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    //上次时间戳,初始值为负数
    private long lastTimestamp = -1L;

    public long getWorkerId(){
        return workerId;
    }

    public long getDatacenterId(){
        return datacenterId;
    }

    public long getTimestamp(){
        return System.currentTimeMillis();
    }

    //下一个ID生成算法
    public synchronized long nextId() {
        long timestamp = timeGen();

        //获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
        if (timestamp < lastTimestamp) {
            System.err.printf("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp);
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds",
                                                     lastTimestamp - timestamp));
        }

        //获取当前时间戳如果等于上次时间戳
        //说明:还处在同一毫秒内,则在序列号加1;否则序列号赋值为0,从0开始。
        if (lastTimestamp == timestamp) {  // 0  - 4095
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }

        //将上次时间戳值刷新
        lastTimestamp = timestamp;

        /**
* 返回结果:
* (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数
* (datacenterId << datacenterIdShift) 表示将数据id左移相应位数
* (workerId << workerIdShift) 表示将工作id左移相应位数
* | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。
* 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
*/
    return ((timestamp - twepoch) << timestampLeftShift) |
    (datacenterId << datacenterIdShift) |
    (workerId << workerIdShift) |
    sequence;
    }

    //获取时间戳,并与上次时间戳比较
    private long tilNextMillis(long lastTimestamp) {
    long timestamp = timeGen();
    while (timestamp <= lastTimestamp) {
    timestamp = timeGen();
    }
    return timestamp;
    }

    //获取系统时间戳
    private long timeGen(){
    return System.currentTimeMillis();
    }




    public static void main(String[] args) {
    IdWorker worker = new IdWorker(21,10,0);
    for (int i = 0; i < 100; i++) {
    System.out.println(worker.nextId());
    }
    }

    }



四、分布式id之 Redis的Incr(推荐)

Incr命令,会将存储在key上的数字加1。如果该键不存在或包含错误类型的值,请先将该键值设置为“0”,再进行加1操作。借用此特性,我们可以天然获得唯一且递增的分布式id。

假设key为"id",则redis中的数据变化如下

  1. 执行前:<>
  2. 第一次执行Incr 创建key并赋值 0 <“id”,“0”>,再执行incr 结果为<“id”,“1”>
  3. 第二次执行incr 结果为 <“id”,“2”>

java使用案例

public static void main(String[] args) {
    Jedis jedis = new Jedis("111.229.248.243", 6379);
    Long id = jedis.incr("id");//<id,0>
    System.out.println(id);
}

五、总结

分布式id的重要性在如今的分布式系统环境中的是不言而喻的。上文中提到的UUID、数据库自增主键、雪花算法、和利用redis的incr命令实现分布式id等方法,都能达到分布式id的效果。
1. 对于业务场景较小且后期拓展可能性较小的项目,使用UUID最为简单。
2. 数据库自增主键和Redis的incr命令的话,都是利用第三方生成唯一自增值的方式来实现的分布式id,但是使用数据库自增主键的话,需要和数据库绑定,需要额外的维护且受数据量大小的影响,使用redis则简单许多比较推荐。
3. 雪花算法的话,像是在大量服务器的场景下使用会比较省事,但是也需要保证时间同步的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
雪花算法是种生成分布式ID的算法,它可以生成一个64位的ID,其中包含了时间戳、数据中心ID和机器ID等信息。下面是雪花算法生成分布式ID的软件设计模型: 1. 定义一个Snowflake类,该类包含以下属性: - datacenter_id: 数据中心ID,占5位,取值围为0~31。 - worker_id: 机器ID,占5位,取值范围为0~31。 - sequence: 序列号,占12位,取值范围为0~4095。 - last_timestamp: 上一次生成ID的时间戳。 2. 实现Snowflake类的构造函数,初始化datacenter_id和worker_id属性。 3. 实现一个next_id方法,该方法用于生成下一个ID。具体实现如下: - 获取当前时间戳,单位为毫秒。 - 如果当前时间戳小于上一次生成ID的时间戳,则说明系统时钟回退过,抛出异常。 - 如果当前时间戳等于上一次生成ID的时间戳,则将序列号加1。 - 如果当前时间戳大于上一次生成ID的时间戳,则将序列号重置为0,并将last_timestamp属性更新为当前时间戳。 - 将datacenter_id、worker_id、时间戳和序列号按照一定的位数组合成一个64位的ID。 - 返回生成ID。 4. 在分布式系统中,每个节点都需要创建一个Snowflake实例,并指定不同的datacenter_id和worker_id。每个节点生成ID都是唯一的,且具有时间顺序。 下面是一个Python实现的雪花算法生成分布式ID的代码示例: ```python import time class Snowflake: def __init__(self, datacenter_id, worker_id): self.datacenter_id = datacenter_id self.worker_id = worker_id self.sequence = 0 self.last_timestamp = -1 def next_id(self): timestamp = int(time.time() * 1000) if timestamp < self.last_timestamp: raise Exception("Clock moved backwards. Refusing to generate id") if timestamp == self.last_timestamp: self.sequence = (self.sequence + 1) & 4095 if self.sequence == 0: timestamp = self.wait_next_millis(self.last_timestamp) else: self.sequence = 0 self.last_timestamp = timestamp return ((timestamp - 1288834974657) << 22) | (self.datacenter_id << 17) | (self.worker_id << 12) | self.sequence def wait_next_millis(self, last_timestamp): timestamp = int(time.time() * 1000) while timestamp <= last_timestamp: timestamp = int(time.time() * 1000) return timestamp ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值