@[toc]
上了微服务之后,很多本来很简略的问题当初都变简单了,例如全局 ID 这事!

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

当数据库分库分表之后,本来的主键自增就不不便持续应用了,须要找到一个新的适合的计划,松哥的需要就是在这样的状况下提出的。

接下来咱们一起来捋一捋。

1. 两种思路

整体上来说,这个问题有两种不同的思路:

  • 让数据库本人搞定
  • Java 代码来解决主键,而后直接插入数据库中即可。

这两种思路又对应了不同的计划,咱们一个一个来看。

2. 数据库本人搞定

数据库本人搞定,就是说我在数据插入的时候,仍然不思考主键的问题,心愿持续应用数据库的主键自增,然而很显著,本来默认的主键自增当初没法用了,咱们必须有新的计划。

2.1 批改数据库配置

数据库分库分表之后的构造如下图(假如数据库中间件用的 MyCat):

此时如果本来的 db1、db2、db3 持续各自主键自增,那么对于 MyCat 而言,主键就不是自增了,主键就会反复,用户从 MyCat 中查问到的数据主键就有问题。

找到问题的起因,那么剩下的就好解决了。

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

首先咱们能够通过如下 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 能够批改自增的起始值和每次增长的步长,当初假如我有 db1、db2 和 db3,我就能够别离设置这三个库中表的自增起始值为 1、2、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 leafCREATE 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=trueleaf.snowflake.zk.address=192.168.91.130leaf.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,两种模式皆可。