七种分布式全局 ID 生成策略,你更爱哪一种?

 

 


上了微服务以后,不少本来很简单的问题如今都变复杂了,例如全局 ID 这事!

 

松哥最近工做中恰好用到这块内容,因而调研了市面上几种常见的全局 ID 生成策略,稍微作了一下对比,供小伙伴们参考。算法

当数据库分库分表以后,本来的主键自增就不方便继续使用了,须要找到一个新的合适的方案,松哥的需求就是在这样的状况下提出的。数据库

接下来咱们一块儿来捋一捋。api

1. 两种思路

总体上来讲,这个问题有两种不一样的思路:缓存

  • 让数据库本身搞定
  • Java 代码来处理主键,而后直接插入数据库中便可。

这两种思路又对应了不一样的方案,咱们一个一个来看。安全

2. 数据库本身搞定

数据库本身搞定,就是说我在数据插入的时候,依然不考虑主键的问题,但愿继续使用数据库的主键自增,可是很明显,本来默认的主键自增如今无法用了,咱们必须有新的方案。服务器

2.1 修改数据库配置

数据库分库分表以后的结构以下图(假设数据库中间件用的 MyCat):网络

此时若是本来的 db一、db二、db3 继续各自主键自增,那么对于 MyCat 而言,主键就不是自增了,主键就会重复,用户从 MyCat 中查询到的数据主键就有问题。并发

找到问题的缘由,那么剩下的就好解决了。app

咱们能够直接修改 MySQL 数据库主键自增的起始值和步长。less

首先咱们能够经过以下 SQL 查看与此相关的两个变量的取值:

SHOW VARIABLES LIKE 'auto_increment%'

能够看到,主键自增的起始值和步长都是 1。

起始值好改,在定义表的时候就能够设置,步长咱们能够经过修改这个配置实现:

set @@auto_increment_increment=9;

修改后,再去查看对应的变量值,发现已经变了:

此时咱们再去插入数据,主键自增就不是每次自增 1,而是每次自增 9 了。

至于自增起始值其实很好设置,建立表的时候就能够设置了。

create table test01(id integer PRIMARY KEY auto_increment,username varchar(255)) auto_increment=8;

既然 MySQL 能够修改自增的起始值和每次增加的步长,如今假设我有 db一、db2 和 db3,我就能够分别设置这三个库中表的自增起始值为 一、二、3,而后自增步长都是 3,这样就能够实现自增了。

可是很明显这种方式不够优雅,并且处理起来很麻烦,未来扩展也不方便,所以不推荐。

2.2 MySQL+MyCat+ZooKeeper

若是你们分库分表工具刚好使用的是 MyCat,那么结合 Zookeeper 也能很好的实现主键全局自增。

MyCat 做为一个分布式数据库中间,屏蔽了数据库集群的操做,让咱们操做数据库集群就像操做单机版数据库同样,对于主键自增,它有本身的方案:

  1. 经过本地文件实现
  2. 经过数据库实现
  3. 经过本地时间戳实现
  4. 经过分布式 ZK ID 生成器实现
  5. 经过 ZK 递增方式实现

这里咱们主要来看方案 4。

配置步骤以下:

  • 首先修改主键自增方式为 4 ,4 表示使用 zookeeper 实现主键自增。

server.xml

  • 配置表自增,而且设置主键

schema.xml

设置主键自增,而且设置主键为 id 。

  • 配置 zookeeper 的信息

在 myid.properties 中配置 zookeeper 信息:

  • 配置要自增的表

sequence_conf.properties

注意,这里表名字要大写。

  1. TABLE.MINID 某线程当前区间内最小值
  2. TABLE.MAXID 某线程当前区间内最大值
  3. TABLE.CURID 某线程当前区间内当前值
  4. 文件配置的MAXID以及MINID决定每次取得区间,这个对于每一个线程或者进程都有效
  5. 文件中的这三个属性配置只对第一个进程的第一个线程有效,其余线程和进程会动态读取 ZK
  • 重启 MyCat 测试

最后重启 MyCat ,删掉以前建立的表,而后建立新表进行测试便可。

这种方式就比较省事一些,并且可扩展性也比较强,若是选择了 MyCat 做为分库分表工具,那么这种不失为一种最佳方案。

前面介绍这两种都是在数据库或者数据库中间件层面来处理主键自增,咱们 Java 代码并不须要额外工做。

接下来咱们再来看几种须要在 Java 代码中进行处理的方案。

3. Java 代码处理

3.1 UUID

最容易想到的就是 UUID (Universally Unique Identifier) 了,
UUID 的标准型式包含 32 个 16 进制数字,以连字号分为五段,形式为 8-4-4-4-12 的 36 个字符,这个是 Java 自带的,用着也简单,最大的优点就是本地生成,没有网络消耗,可是但凡在公司作开发的小伙伴都知道这个东西在公司项目中使用并很少。缘由以下:

  1. 字符串太长,对于 MySQL 而言,不利于索引。
  2. UUID 的随机性对于 I/O 密集型的应用很是不友好!它会使得聚簇索引的插入变得彻底随机,使得数据没有任何汇集特性。
  3. 信息不安全:基于 MAC 地址生成 UUID 的算法可能会形成 MAC 地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制做者位置。

所以,UUID 并不是最佳方案。

3.2 SNOWFLAKE

雪花算法是由 Twitter 公布的分布式主键生成算法,它可以保证不一样进程主键的不重复性,以及相同进程主键的有序性。在同一个进程中,它首先是经过时间位保证不重复,若是时间相同则是经过序列位保证。

同时因为时间位是单调递增的,且各个服务器若是大致作了时间同步,那么生成的主键在分布式环境能够认为是整体有序的,这就保证了对索引字段的插入的高效性。

例如 MySQL 的 Innodb 存储引擎的主键。使用雪花算法生成的主键,二进制表示形式包含 4 部分,从高位到低位分表为:1bit 符号位、41bit 时间戳位、10bit 工做进程位以及 12bit 序列号位。

  • 符号位 (1bit)

预留的符号位,恒为零。

  • 时间戳位 (41bit)

41 位的时间戳能够容纳的毫秒数是 2 的 41 次幂,一年所使用的毫秒数是:365 * 24 * 60 * 60 * 1000。经过计算可知:Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L);结果约等于 69.73 年。

ShardingSphere 的雪花算法的时间纪元从 2016 年 11 月 1 日零点开始,可使用到 2086 年,相信能知足绝大部分系统的要求。

  • 工做进程位 (10bit)

该标志在 Java 进程内是惟一的,若是是分布式应用部署应保证每一个工做进程的 id 是不一样的。该值默认为 0,可经过属性设置。

  • 序列号位 (12bit)

该序列是用来在同一个毫秒内生成不一样的 ID。若是在这个毫秒内生成的数量超过 4096 (2 的 12 次幂),那么生成器会等待到下个毫秒继续生成。

注意: 该算法存在 时钟回拨 问题,服务器时钟回拨会致使产生重复序列,所以默认分布式主键生成器提供了一个最大容忍的时钟回拨毫秒数。 若是时钟回拨的时间超过最大容忍的毫秒数阈值,则程序报错;若是在可容忍的范围内,默认分布式主键生成器会等待时钟同步到最后一次主键生成的时间后再继续工做。 最大容忍的时钟回拨毫秒数的默认值为 0,可经过属性设置。

下面松哥给出一个雪花算法的工具类,你们能够参考:

public class IdWorker {
    // 时间起始标记点,做为基准,通常取系统的最近时间(一旦肯定不能变更)
    private final static long twepoch = 1288834974657L;
    // 机器标识位数
    private final static long workerIdBits = 5L;
    // 数据中心标识位数
    private final static long datacenterIdBits = 5L;
    // 机器ID最大值
    private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 数据中心ID最大值
    private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    // 毫秒内自增位
    private final static long sequenceBits = 12L;
    // 机器ID偏左移12位
    private final static long workerIdShift = sequenceBits;
    // 数据中心ID左移17位
    private final static long datacenterIdShift = sequenceBits + workerIdBits;
    // 时间毫秒左移22位
    private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
    /* 上次生产id时间戳 */
    private static long lastTimestamp = -1L;
    // 0,并发控制
    private long sequence = 0L;

    private final long workerId;
    // 数据标识id部分
    private final long datacenterId;

    public IdWorker(){
        this.datacenterId = getDatacenterId(maxDatacenterId);
        this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
    }

    /**
     * @param workerId
     *            工做机器ID
     * @param datacenterId
     *            序列号
     */
    public IdWorker(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;
    }

    /**
     * 获取下一个ID
     *
     * @return
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            // 当前毫秒内,则+1
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 当前毫秒内计数满了,则等待下一秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        // ID偏移组合生成最终的ID,并返回ID
        long nextId = ((timestamp - twepoch) << timestampLeftShift)
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift) | sequence;

        return nextId;
    }

    private long tilNextMillis(final long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = this.timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }

    /**
     * <p>
     * 获取 maxWorkerId
     * </p>
     */
    protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
        StringBuffer mpid = new StringBuffer();
        mpid.append(datacenterId);
        String name = ManagementFactory.getRuntimeMXBean().getName();
        if (!name.isEmpty()) {
            /*
             * GET jvmPid
             */
            mpid.append(name.split("@")[0]);
        }
        /*
         * MAC + PID 的 hashcode 获取16个低位
         */
        return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
    }

    /**
     * <p>
     * 数据标识id部分
     * </p>
     */
    protected static long getDatacenterId(long maxDatacenterId) {
        long id = 0L;
        try {
            InetAddress ip = InetAddress.getLocalHost();
            NetworkInterface network = NetworkInterface.getByInetAddress(ip);
            if (network == null) {
                id = 1L;
            } else {
                byte[] mac = network.getHardwareAddress();
                id = ((0x000000FF & (long) mac[mac.length - 1])
                        | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
                id = id % (maxDatacenterId + 1);
            }
        } catch (Exception e) {
            System.out.println(" getDatacenterId: " + e.getMessage());
        }
        return id;
    }
}

用法以下:

IdWorker idWorker = new IdWorker(0, 0);
for (int i = 0; i < 1000; i++) {
    System.out.println(idWorker.nextId());
}

3.3 LEAF

Leaf 是美团开源的分布式 ID 生成系统,最先期需求是各个业务线的订单 ID 生成需求。在美团早期,有的业务直接经过 DB 自增的方式生成 ID,有的业务经过 Redis 缓存来生成 ID,也有的业务直接用 UUID 这种方式来生成 ID。以上的方式各自有各自的问题,所以美团决定实现一套分布式 ID 生成服务来知足需求目前 Leaf 覆盖了美团点评公司内部金融、餐饮、外卖、酒店旅游、猫眼电影等众多业务线。在4C8G VM 基础上,经过公司 RPC 方式调用,QPS 压测结果近 5w/s,TP999 1ms(TP=Top Percentile,Top 百分数,是一个统计学里的术语,与平均数、中位数都是一类。TP50、TP90 和 TP99 等指标经常使用于系统性能监控场景,指高于 50%、90%、99% 等百分线的状况)。

目前 LEAF 的使用有两种不一样的思路,号段模式和 SNOWFLAKE 模式,你能够同时开启两种方式,也能够指定开启某种方式(默认两种方式为关闭状态)。

咱们从 GitHub 上 Clone LEAF 以后,它的配置文件在 leaf-server/src/main/resources/leaf.properties 中,各项配置的含义以下:

能够看到,若是使用号段模式,须要数据库支持;若是使用 SNOWFLAKE 模式,须要 Zookeeper 支持。

3.3.1 号段模式

号段模式仍是基于数据库,可是思路有些变化,以下:

  1. 利用 proxy server 从数据库中批量获取 id,每次获取一个 segment (step 决定其大小) 号段的值,用完以后再去数据库获取新的号段,能够大大的减轻数据库的压力。
  2. 各个业务不一样的发号需求用 biz_tag 字段来区分,每一个 biz-tag 的 ID 获取相互隔离,互不影响。
  3. 若是有新的业务须要扩区 ID,只须要增长表记录便可。

若是使用号段模式,咱们首先须要建立一张数据表,脚本以下:

CREATE DATABASE leaf
CREATE TABLE `leaf_alloc` (
  `biz_tag` varchar(128)  NOT NULL DEFAULT '',
  `max_id` bigint(20) NOT NULL DEFAULT '1',
  `step` int(11) NOT NULL,
  `description` varchar(256)  DEFAULT NULL,
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;

insert into leaf_alloc(biz_tag, max_id, step, description) values('leaf-segment-test', 1, 2000, 'Test leaf Segment Mode Get Id')

这张表中各项字段的含义以下:

  • biz_tag:业务标记(不一样业务能够有不一样的号段序列)
  • max_id:当前号段下的最大 id
  • step:每次取号段的步长
  • description:描述信息
  • update_time:更新时间

配置完成后,启动项目,访问 http://localhost:8080/api/segment/get/leaf-segment-test 路径(路径最后面的 leaf-segment-test 是业务标记),便可拿到 ID。

能够经过以下地址访问到号段模式的监控页面 http://localhost:8080/cache。

号段模式优缺点:

优势

  • Leaf 服务能够很方便的线性扩展,性能彻底可以支撑大多数业务场景。
  • ID 号码是趋势递增的 8byte 的 64 位数字,知足上述数据库存储的主键要求。
  • 容灾性高:Leaf 服务内部有号段缓存,即便 DB 宕机,短期内 Leaf 仍能正常对外提供服务。
  • 能够自定义 max_id 的大小,很是方便业务从原有的 ID 方式上迁移过来。

缺点

  • ID 号码不够随机,可以泄露发号数量的信息,不太安全。
  • DB 宕机会形成整个系统不可用。

3.3.2 SNOWFLAKE 模式

SNOWFLAKE 模式须要配合 Zookeeper 一块儿,不过 SNOWFLAKE 对 Zookeeper 的依赖是弱依赖,把 Zookeeper 启动以后,咱们能够在 SNOWFLAKE 中配置 Zookeeper 信息,以下:

leaf.snowflake.enable=true
leaf.snowflake.zk.address=192.168.91.130
leaf.snowflake.port=2183

而后从新启动项目,启动成功后,经过以下地址能够访问到 ID:

http://localhost:8080/api/snowflake/get/test

3.4 Redis 生成

这个主要是利用 Redis 的 incrby 来实现,这个我以为没啥好说的。

3.5 Zookeeper 处理

zookeeper 也能作,可是比较麻烦,不推荐。

4. 小结

综上,若是项目中刚好使用了 MyCat,那么可使用 MyCat+Zookeeper,不然建议使用 LEAF,两种模式皆可。

相关文章
相关标签/搜索