浏览大略须要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哥,咱们下期见!