分布式唯一ID生成:雪花算法的原理及实战使用、三大问题的解决

文章目录

  • 雪花算法
  • 一、原理
    • 1、第1位
    • 2、第2位~第42位
    • 3、第43位~第52位
    • 4、第53位~第64位:
    • 5、要点
    • 6、缺点
    • 7、解决方案
      • 1.时间回拨问题
      • 2.机器id分配及回收
      • 3.机器id上限
  • 二、使用雪花算法
    • 1、MyBatisPlus集成实现
    • 2、Java实现
  • 三、总结

雪花算法

一、原理

分布式唯一ID生成:雪花算法的原理及实战使用、三大问题的解决_第1张图片

1、第1位

二进制中最高位为1的是负数,而在随机ID中,只能为正数,故该位只能为0,无意义。

2、第2位~第42位

共41bit,是当前时间戳转换为二进制而来的。可以使用69年:year = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69

3、第43位~第52位

共10bit,是当前机器(服务器)的ID,其二进制数为210即1024,所以使用雪花算法的服务最多可以同时部署在1024台服务器上。需要注意的是这里10位中的5位是给机房的,5位是给机器(服务器)的,也就是一个雪花算法服务最多能部署在25个机房,每个机房最多有2^5个机器(服务器)。

4、第53位~第64位:

共12bit,是毫秒内的序列号,即统一毫秒内生成的第几个ID,其二进制数为2^12即4096,所以同一雪花算法服务在同一毫秒内可生成4096个序列号ID,如果超出了,就只能等待下一毫秒再生成。

将以上四个部分拼接起来做位或运算,就组成了一个64位的全局唯一ID。

5、要点

1、由于时间戳是雪花算法的组成部分,所以同一服务中生成的雪花算法ID呈现递增趋势。
2、在1024台服务器的分布式系统内,不会出现ID相同的情况。
3、1毫秒产生4096个ID,那么1秒就能产生4096000个ID,可见雪花算法的效率之高。

分布式唯一ID生成:雪花算法的原理及实战使用、三大问题的解决_第2张图片

6、缺点

  • 时间回拨问题:由于机器的时间是动态的调整的,有可能会出现时间跑到之前几毫秒,如果这个时候获取到了这种时间,则会出现数据重复。
  • 机器id分配及回收问题:目前机器id需要每台机器不一样,这样的方式分配需要有方案进行处理,同时也要考虑,如果该机器宕机了,对应的workerId分配后的回收问题。
  • 机器id上限:机器id是固定的bit,那么也就是对应的机器个数是有上限的,在有些业务场景下,需要所有机器共享同一个业务空间,那么10bit表示的1024台机器是不够的。

7、解决方案

业内的方案中对以上三个问题有这么几种处理,但是都没有彻底解决:

1.时间回拨问题

  • 采用直接抛异常方式:这种很不友好,太粗暴。
  • 采用等待跟上次时间的一段范围:这种算是简单解决,可以接受,但是如果等待一段时间后再出现回拨,则抛异常,可接受,但是不算彻底解决。

2.机器id分配及回收

  • 采用zookeeper的顺序节点分配:解决了分配,回收可采用zookeeper临时节点回收,但是临时节点不可靠,存在无故消失问题,因此也不可靠。
  • 采用DB中插入数据作为节点值:解决了分配,没有解决回收。

3.机器id上限

该问题在业内都没有处理,也就是说如果采用雪花算法,则必定会存在该问题,但是该问题也只有需要大量的业务机器共享的场景才会出现,这种情况,采用客户端 双Buffer + DB 这种非雪花算法的方案也未尝不可。

二、使用雪花算法

1、MyBatisPlus集成实现

1)、在实体类中的id上加入如下配置,指定类型为id_worker

@TableId(value = "id",type = IdType.ID_WORKER)
private Long id;

2)、在application.yml文件中配置数据中心id和机器id

mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml
  # 设置别名包扫描路径,通过该属性可以给包中的类注册别名
  type-aliases-package: com.ctc.entity
  global-config:
    datacenter-id: 1	#数据中心(机房)ID (取值范围:0-31)
    worker-id: 1	#机器(服务器)ID (取值范围:0-31)

2、Java实现

1)编码

public class SnowflakeIdWorker {
 
    // ==============================Fields===========================================//
    private final long twepoch = 1420041600000L;
 
    /** 机器id所占的位数 */
    private final long workerIdBits = 5L;
 
    /** 数据标识id所占的位数 */
    private final long datacenterIdBits = 5L;
 
    /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
 
    /** 支持的最大数据标识id,结果是31 */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
 
    /** 序列在id中占的位数 */
    private final long sequenceBits = 12L;
 
    /** 机器ID向左移12位 */
    private final long workerIdShift = sequenceBits;
 
    /** 数据标识id向左移17位(12+5) */
    private final long datacenterIdShift = sequenceBits + workerIdBits;
 
    /** 时间截向左移22位(5+5+12) */
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
 
    /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);
 
    /** 工作机器ID(0~31) */
    private long workerId;
 
    /** 数据中心ID(0~31) */
    private long datacenterId;
 
    /** 毫秒内序列(0~4095) */
    private long sequence = 0L;
 
    /** 上次生成ID的时间截 */
    private long lastTimestamp = -1L;
 
    //==============================Constructors=====================================//
    /**
     * 构造函数
     * @param workerId 工作ID (0~31)
     */
    public SnowflakeIdWorker(long workerId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        this.workerId = workerId;
        this.datacenterId = 1;
    }
    /**
     * 构造函数
     * @param workerId 工作ID (0~31)
     * @param datacenterId 数据中心ID (0~31)
     */
    public SnowflakeIdWorker(long workerId, long datacenterId) {
        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));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
 
    // ==============================Methods==========================================//
    /**
     * 获得下一个ID (该方法是线程安全的)
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
 
        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }
 
        //如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒内序列溢出
            if (sequence == 0) {
                //阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }
 
        //上次生成ID的时间截
        lastTimestamp = timestamp;
 
        //移位并通过或运算拼到一起组成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) 
                | (datacenterId << datacenterIdShift) 
                | (workerId << workerIdShift) 
                | sequence;
    }
 
    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }
 
    /**
     * 返回以毫秒为单位的当前时间
     * @return 当前时间(毫秒)
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }
}

2、测试

 public static void main(String[] args) {
     SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1, 1);
     for (int i = 0; i < 20; i++) {
         long id = idWorker.nextId();
         System.out.println(id+"--------"+Long.toBinaryString(id));
     }
 }

3、结果

647824078551519232--------100011111101100010001001000100101111000000100001000000000000
647824078551519233--------100011111101100010001001000100101111000000100001000000000001
647824078551519234--------100011111101100010001001000100101111000000100001000000000010
647824078551519235--------100011111101100010001001000100101111000000100001000000000011
647824078551519236--------100011111101100010001001000100101111000000100001000000000100
647824078555713536--------100011111101100010001001000100101111010000100001000000000000
647824078555713537--------100011111101100010001001000100101111010000100001000000000001
647824078555713538--------100011111101100010001001000100101111010000100001000000000010
647824078555713539--------100011111101100010001001000100101111010000100001000000000011
647824078555713540--------100011111101100010001001000100101111010000100001000000000100
647824078555713541--------100011111101100010001001000100101111010000100001000000000101
647824078555713542--------100011111101100010001001000100101111010000100001000000000110
647824078555713543--------100011111101100010001001000100101111010000100001000000000111
647824078555713544--------100011111101100010001001000100101111010000100001000000001000
647824078555713545--------100011111101100010001001000100101111010000100001000000001001
647824078555713546--------100011111101100010001001000100101111010000100001000000001010
647824078559907840--------100011111101100010001001000100101111100000100001000000000000
647824078559907841--------100011111101100010001001000100101111100000100001000000000001
647824078559907842--------100011111101100010001001000100101111100000100001000000000010
647824078559907843--------100011111101100010001001000100101111100000100001000000000011

三、总结

  • 总之就是用一个 64 bit 的数字中各个 bit 位来设置不同的标志位,区分每一个 id。
  • 实际中我们的机器并没有那么多,可以改进改算法,例如将10bit的机器id优化和我们系统相关的业务。
  • 雪花算法只是一种思想,活学活用,切记生搬硬套。

你可能感兴趣的:(Java,算法,分布式,id,java,linux)