关于java:分布式唯一ID解决方案雪花算法

3次阅读

共计 6628 个字符,预计需要花费 17 分钟才能阅读完成。

浏览大略须要 3 分钟

附源码

[toc]

前言

单体架构的服务的日子曾经一去不复返了。

以后零碎业务和数据存储的复杂度都在晋升,分布式系统是目前应用十分广泛的解决方案。

全局惟一 ID 简直是所有设计零碎时都会遇到的,全局惟一 ID 在存储和检索中有至关重要的作用。

ID 生成器

在应用程序中,常常须要全局惟一的 ID 作为数据库主键。如何生成全局惟一 ID?

首先,须要确定全局惟一 ID 是整型还是字符串?如果是字符串,那么现有的 UUID 就齐全满足需要,不须要额定的工作。毛病是字符串作为 ID 占用空间大,索引效率比整型低。

如果采纳整型作为 ID,那么首先排除掉 32 位 int 类型,因为范畴太小,必须应用 64 位 long 型。

采纳整型作为 ID 时,如何生成自增、全局惟一且不反复的 ID?

数据库自增

数据库自增 ID 是咱们在数据量较小的零碎中常常应用的,利用数据库的自增 ID,从 1 开始,根本能够做到间断递增。Oracle 能够用 SEQUENCE,MySQL 能够用主键的 AUTO_INCREMENT,尽管不能保障全局惟一,但每个表惟一,也根本满足需要。

数据库自增 ID 的毛病是数据在插入前,无奈取得 ID。数据在插入后,获取的 ID 尽管是惟一的,但肯定要等到事务提交后,ID 才算是无效的。有些双向援用的数据,不得不插入后再做一次更新,比拟麻烦。

在咱们开发过程中,遇到一种 主主数据库同步(简略能够了解为,同样的 sql 再另一台数据库再执行一次)的场景,如果应用数据库自增 ID,就会呈现主键不统一、或主键抵触问题。

分布式 ID 生成器

计划一:UUID

分布式环境不举荐应用

uuid 是咱们比拟先想到的办法,在 java.util; 包中就有对应办法。这是一个具备 rfc 规范的 uuid:https://www.ietf.org/rfc/rfc4…

uuid 有很好的性能(本地调用),没有网络耗费。

然而,uuid 不易存储(生成了字符串、存储过长、很多场景不实用);信息不平安(基于 MAC 地址生成、可能会造成泄露,这个破绽曾被用于寻找梅丽莎病毒的制作者地位。
);无奈保障递增(或趋势递增);其余博主反馈,截取前 20 位做惟一 ID,在大数量(大略只有 220w)状况下会有反复问题。

UUID.randomUUID().toString()

计划二:snowflake(雪花算法)

这是目前应用较多分布式 ID 解决方案,举荐应用

背景 Twitter 云云就不介绍了,就是前段时间封了懂王账号的 Twitter。

算法介绍

SnowFlake 算法生成 id 的后果是一个 64bit 大小的整数,它的构造如下图:

  • 1 位,不必。二进制中最高位为 1 的都是正数,然而咱们生成的 id 个别都应用整数,所以这个最高位固定是 0
  • 41 位,用来记录时间戳(毫秒)。

    • 41 位能够示意 2^{41}-1 个数字,
    • 如果只用来示意正整数(计算机中负数蕴含 0),能够示意的数值范畴是:0 至 2^{41}-1,减 1 是因为可示意的数值范畴是从 0 开始算的,而不是 1。
    • 也就是说 41 位能够示意 2^{41}-1 个毫秒的值,转化成单位年则是 (2^{41}-1) / (1000 60 60 24 365) = 69 年
  • 10 位,用来记录工作机器 id。

    • 能够部署在 2^{10} = 1024 个节点,包含 5 位 datacenterId 和 5 位 workerId
    • 5 位(bit)能够示意的最大正整数是 2^{5}-1 = 31,即能够用 0、1、2、3、….31 这 32 个数字,来示意不同的 datecenterId 或 workerId
  • 12 位,序列号,用来记录同毫秒内产生的不同 id。

    • 12 位(bit)能够示意的最大正整数是 2^{12}-1 = 4095,即能够用 0、1、2、3、….4094 这 4095 个数字,来示意同一机器同一时间截(毫秒)内产生的 4095 个 ID 序号。

因为在 Java 中 64bit 的整数是 long 类型,所以在 Java 中 SnowFlake 算法生成的 id 就是 long 来存储的。

SnowFlake 能够保障

  1. 同一台服务器所有生成的 id 按工夫趋势递增
  2. 整个分布式系统内不会产生反复 id(因为有 datacenterId 和 workerId 来做辨别)

存在的问题:

  1. 机器 ID(5 位)和数据中心 ID(5 位)配置没有解决,分布式部署的时候会应用雷同的配置,任然有 ID 反复的危险。
  2. 应用的时候须要实例化对象,没有造成开箱即用的工具类。
  3. 强依赖机器时钟,如果机器上时钟回拨,会导致发号反复或者服务会处于不可用状态。(这点在失常状况下是不会产生的)

针对下面问题,这里提供一种解决思路,workId 应用服务器 hostName 生成,dataCenterId 应用 IP 生成,这样能够最大限度避免 10 位机器码反复,然而因为两个 ID 都不能超过 32,只能取余数,还是不免产生反复,然而理论应用中,hostName 和 IP 的配置个别间断或相近,只有不是刚好相隔 32 位,就不会有问题,况且,hostName 和 IP 同时相隔 32 的状况更加是简直不可能的事,平时做的分布式部署,个别也不会超过 10 台容器。

生产上应用 docker 配置个别是一次编译,而后分布式部署到不同容器,不会有不同的配置。这种状况就对下面提到的呈现了不确定状况,这个在评论中会再出一篇参考文章。

源码

Java 版雪花 ID 生成算法

package com.my.blog.website.utils;
 
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
 
import java.net.Inet4Address;
import java.net.UnknownHostException;
 
/**
 * Twitter_Snowflake<br>
 * SnowFlake 的构造如下(每局部用 - 离开):<br>
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
 * 1 位标识,因为 long 根本类型在 Java 中是带符号的,最高位是符号位,负数是 0,正数是 1,所以 id 个别是负数,最高位是 0 <br>
 * 41 位工夫截(毫秒级),留神,41 位工夫截不是存储以后工夫的工夫截,而是存储工夫截的差值(以后工夫截 - 开始工夫截)
 * 失去的值),这里的的开始工夫截,个别是咱们的 id 生成器开始应用的工夫,由咱们程序来指定的(如下上面程序 IdWorker 类的 startTime 属性)。41 位的工夫截,能够应用 69 年,年 T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
 * 10 位的数据机器位,能够部署在 1024 个节点,包含 5 位 datacenterId 和 5 位 workerId<br>
 * 12 位序列,毫秒内的计数,12 位的计数顺序号反对每个节点每毫秒 (同一机器,同一时间截) 产生 4096 个 ID 序号 <br>
 * 加起来刚好 64 位,为一个 Long 型。<br>
 * SnowFlake 的长处是,整体上依照工夫自增排序,并且整个分布式系统内不会产生 ID 碰撞(由数据中心 ID 和机器 ID 作辨别),并且效率较高,经测试,SnowFlake 每秒可能产生 26 万 ID 左右。*/
public class SnowflakeIdWorker {
 
    // ==============================Fields===========================================
    /** 开始工夫截 (2015-01-01) */
    private final long twepoch = 1489111610226L;
 
    /** 机器 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;
 
    private static SnowflakeIdWorker idWorker;
 
    static {idWorker = new SnowflakeIdWorker(getWorkId(),getDataCenterId());
    }
 
    //==============================Constructors=====================================
    /**
     * 构造函数
     * @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("workerId can't be greater than %d or less than 0", maxWorkerId));
        }
        if (dataCenterId > maxDataCenterId || dataCenterId < 0) {throw new IllegalArgumentException(String.format("dataCenterId 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();
    }
 
    private static Long getWorkId(){
        try {String hostAddress = Inet4Address.getLocalHost().getHostAddress();
            int[] ints = StringUtils.toCodePoints(hostAddress);
            int sums = 0;
            for(int b : ints){sums += b;}
            return (long)(sums % 32);
        } catch (UnknownHostException e) {
            // 如果获取失败,则应用随机数备用
            return RandomUtils.nextLong(0,31);
        }
    }
 
    private static Long getDataCenterId(){int[] ints = StringUtils.toCodePoints(SystemUtils.getHostName());
        int sums = 0;
        for (int i: ints) {sums += i;}
        return (long)(sums % 32);
    }
 
 
    /**
     * 动态工具类
     *
     * @return
     */
    public static synchronized Long generateId(){long id = idWorker.nextId();
        return id;
    }
 
    //==============================Test=============================================
    /** 测试 */
    public static void main(String[] args) {System.out.println(System.currentTimeMillis());
        long startTime = System.nanoTime();
        for (int i = 0; i < 50000; i++) {long id = SnowflakeIdWorker.generateId();
            System.out.println(id);
        }
        System.out.println((System.nanoTime()-startTime)/1000000+"ms");
    }
}

参考原文:https://blog.csdn.net/xiaopen…

分享和在看是对我最大的激励。我是 pub 哥,咱们下期见!

正文完
 0