分布式id生成器解决方案

采用 MySQL 实现方式

在只使用单数据库时,使用自增主键ID无疑是最适合的。但在集群、主从架构上时就会有一些问题,比如 : 主键的全局唯一


在集群环境下除自增ID 外的其它创建主键的方案

1> 通过应用程序生成一个 GUID,然后和数据一起插入切分后的集群

优点 : 维护简单,实现也容易

缺点 : 应用的计算成本较大,且 GUID 的长度比较长,占用数据库存储空间较大,涉及到应用的开发


2> 通过独立的应用程序事先在数据库中生成一系列唯一的 ID,各应用程序通过接口或者自己去读取再和数据一起插入到切分后的集群中

优点 : 全局唯一主键简单,维护相对容易

缺点 : 实现复杂,需要应用开发,且 ID表 要频繁查和频繁更新,插入数据时,影响性能


3> 通过 中心数据库服务器 利用数据库自身的自增类型 (如 MySQL 的 auto_increment 字段),或者自增对象 (如 Oracle 的 Sequence) 等先生成一个唯一ID 再和数据一起插入切分后的集群 (不推荐)

优点 : 没有特别明显的优点

缺点 : 实现较为复杂,且整体可用性维系在这个中心数据库服务器上,一旦这里 crash,所有的集群都无法进行插入操作


4> 通过集群编号加集群内的自增 (auto_increment类型) 两个字段 共同组成唯一主键 (虽然是两个字段,但是这方式存储空间最小,仅仅多了一个 smallint 两个字节)

优点 : 实现简单,维护也比较简单,对应用透明

缺点 : 引用关联操作相对比较复杂,需要两个字段,主键占用空间较大,在使用 InnoDB 的时候这一点的副作用很明显


5> 通过设置每个集群中自增 ID 起始点 (auto_increment_offset),将各个集群的 ID 进行绝对的分段来实现全局唯一。当遇到某个集群数据增长过快后,通过命令调整下一个 ID 起始位置跳过可能存在的冲突

优点 : 实现简单,且比较容易根据 ID 大小直接判断出数据处在哪个集群,对应用透明

缺点 : 维护相对较复杂,需要高度关注各个集群 ID 增长状况


6> 通过设置每个集群中自增 ID 起始点 (auto_increment_offset) 以及 ID 自增步长 (auto_increment_increment),让目前每个集群的起始点错开 1,步长选择大于将来基本不可能达到的切分集群数,达到将 ID 相对分段的效果来满足全局唯一的效果 (避免重合需要多种方案结合)

实现 : N台数据库,第一台 mysql 主键从1 开始每次加 N,第二台从2开始每次加 N,以此类推,在获取数据时如果第一台 Server 获取失败,则从第二台 Server 上获取

优点 : 实现简单,后期维护简单,对应用透明

缺点 : 第一次设置相对较为复杂



MySQL 中使用 UUID函数 作为主键带来的问题

对于 InnoDB 这种聚集主键类型的引擎来说,数据会按照主键进行物理排序,这对 auto_increment int 是个好消息,因为后一次插入的主键位置总是在最后。但是对 uuid 来说,这却是个坏消息,因为 uuid 是杂乱无章的,每次插入的主键位置是不确定的,可能在开头,也可能在中间,在进行主键物理排序的时候,势必会造成大量的 IO操作影响效率,因此不适合使用 UUID 做物理主键。比较适合的做法是把 uuid 作为逻辑主键,物理主键依然使用 自增ID



Redis 生成 ID

当使用数据库来生成 ID 性能不够要求的时候,可以尝试使用 Redis 来生成ID。这主要依赖于 Redis 是单线程的,所以也可以用生成全局唯一的 ID。可以用 Redis 原子操作 INCR 和 INCRBY 来实现。可以使用 Redis 集群来获取更高的吞吐量。假如一个集群中有 5台 Redis,可以初始化每台 Redis 的值分别是1,2,3,4,5,然后步长都是5。各个Redis生成的ID为 :

A : 1,6,11,16,21...

B : 2,7,12,17,22...

C : 3,8,13,18,23...

D : 4,9,14,19,24...

E : 5,10,15,20,25...

随便负载到哪个机确定好,未来很难做修改。但是 3-5 台服务器基本能够满足器上,都可以获得不同的 ID。但是步长和初始值一定要事先确定,使用Redis集群也可以防止单点故障的问题。另外,比较适合使用 Redis 来生成每天从 0 开始的流水号,比如 订单号=日期+当日自增长号。可以每天在 Redis 中生成一个 Key,使用 INCR进行累加

优点 : 1> 不依赖于数据库,灵活方便,且性能优于数据库

     2> 数字ID天然排序,对分页或者需要排序的结果很有帮助

缺点 : 1> 如果系统中没有 Redis,还需要引入新的组件,增加系统复杂度

     2> 需要编码和配置的工作量比较大



Twitter 的 snowflake算法

snowflake算法 的原理就是用 64位整数来表示主键,其结构如下图 :

1bit

41bit

10bit

12bit

符号位

毫秒数

机房ID

机器ID

递增序列

 

1 bit符号位 : 表示正负,方便使用负数标识不正确的ID

41 bit毫秒时间 : 2^41 / (365 * 24 * 3600 * 1000) ≈ 69年

10 bit机房ID + 机器ID : 最大值为1023

12 bit递增序列 : 最大值为4095


GitHub 访问地址 : https://github.com/twitter/snowflake

/**

 * User: mew <p />

 * Time: 18/3/6 17:33  <p />

 * Version: V1.0  <p />

 * Description: Twitter_Snowflake

 * SnowFlake的结构如下(每部分用-分开):

 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

 * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0

 * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值 (当前时间截 - 开始时间截) 得到的值,这里的的开始时间截,一般是 id 生成器开始使用的时间

 * 由程序来指定的 (如下下面程序 IdWorker类 的 startTime属性)

 * 41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69

 * 10位的数据机器位,可以部署在1024个节点,包括5位 datacenterId 和5位 workerId

 * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒 (同一机器,同一时间截) 产生4096个ID序号加起来刚好64位,为一个Long型

 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右 <p />

 */

public class SnowflakeIdWorker {

    // ==============================Fields===========================================

    /** 开始时间截 (2015-01-01) */

    private final long twepoch = 1420041600000L;

    /** 机器id所占的位数 */

    private final long workerIdBits = 5L;

    /** 数据标识id所占的位数 */

    private final long datacenterIdBits = 5L;

    /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */

    private final long maxWorkerId = ~(-1L << workerIdBits);

    /** 支持的最大数据标识id,结果是31 */

    private final long maxDatacenterId = ~(-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 << 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)

     * @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 当前时间戳

     */

    private long tilNextMillis(long lastTimestamp) {

        long timestamp = timeGen();

        while (timestamp <= lastTimestamp) {

            timestamp = timeGen();

        }

        return timestamp;

    }

    /**

     * 返回以毫秒为单位的当前时间

     *

     * @return 当前时间(毫秒)

     */

    private long timeGen() {

        return System.currentTimeMillis();

    }

    /**

     * 测试

     */

    public static void main(String[] args) {

        SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);

        for (int i = 0; i < 1000; i++) {

            long id = idWorker.nextId();

            System.out.println(Long.toBinaryString(id));

            System.out.println(id);

        }

    }

}

snowflake算法可以根据自身项目的需要进行一定的修改,比如估算未来的数据中心个数,每个数据中心的机器数以及统一毫秒可以能的并发数来调整在算法中所需要的 bit数

优点 : 1> 不依赖于数据库,灵活方便,且性能优于数据库

     2> ID按照时间在单机上是递增的

缺点 : 1> 在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,也许有时候也会出现不是全局递增的情况



利用 zookeeper生成唯一ID

zookeeper 主要通过其 znode数据版本来生成序列号,可以生成32位和64位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号。

很少会使用 zookeeper来生成唯一ID。主要是由于需要依赖 zookeeper,并且是多步调用API,如果在竞争较大的情况下,需要考虑使用分布式锁。因此,性能在高并发的分布式环境下,也不甚理想



MongoDB 的 ObjectId

MongoDB 的 ObjectId 和 snowflake 算法类似,它设计成轻量型的,不同的机器都能用全局唯一的同种方法方便地生成它。MongoDB 从一开始就设计用来作为分布式数据库,处理多个节点是一个核心要求,使其在分片环境中要容易生成得多。


评论区
Rick ©2018