2

分布式 ID

 2 years ago
source link: https://lemon-cs.github.io/2022/02/07/%E9%A1%B9%E7%9B%AE%E6%9E%B6%E6%9E%84/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8FID/#Java%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%96%B9%E5%BC%8F%E6%8E%A5%E5%85%A5
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Lemon-CS

分布式 ID

发表于 2022-02-07|更新于 2021-02-13|分布式
字数总计:9k|阅读时长:32 分钟 | 阅读量:8

分布式 ID

何为 ID?

日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示,比如用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单。

我们现实生活中也有各种 ID,比如身份证 ID 对应且仅对应一个人、地址 ID 对应且仅对应

简单来说,ID 就是数据的唯一标识

何为分布式 ID?

分布式 ID 是分布式系统下的 ID。分布式 ID 不存在与现实生活中,属于计算机系统中的一个概念。

我简单举一个分库分表的例子。

我司的一个项目,使用的是单机 MySQL 。但是,没想到的是,项目上线一个月之后,随着使用人数越来越多,整个系统的数据量将越来越大。

单机 MySQL 已经没办法支撑了,需要进行分库分表(推荐 Sharding-JDBC)。

在分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?

这个时候就需要生成分布式 ID 了。

分布式 ID 需要满足哪些要求?

分布式 ID 作为分布式系统中必不可少的一环,很多地方都要用到分布式 ID。

一个最基本的分布式 ID 需要满足下面这些要求:

  • 全局唯一 :ID 的全局唯一性肯定是首先要满足的!
  • 高性能 : 分布式 ID 的生成速度要快,对本地资源消耗要小。
  • 高可用 :生成分布式 ID 的服务要保证可用性无限接近于 100%。
  • 方便易用 :拿来即用,使用方便,快速接入!

除了这些之外,一个比较好的分布式 ID 还应保证:

  • 安全 :ID 中不包含敏感信息。
  • 有序递增 :如果要把 ID 存放在数据库的话,ID 的有序性可以提升数据库写入速度。并且,很多时候 ,我们还很有可能会直接通过 ID 来进行排序。
  • 有具体的业务含义 :生成的 ID 如果能有具体的业务含义,可以让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。
  • 独立部署 :也就是分布式系统单独有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的。

分布式 ID 常见解决方案

数据库主键自增

这种方式就比较简单直白了,就是通过关系型数据库的自增主键产生来唯一的 ID。

以 MySQL 举例,我们通过下面的方式即可。

1. 创建一个数据库表。

CREATE TABLE sequence_id (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
stub char(10) NOT NULL DEFAULT ‘’,
PRIMARY KEY (id),
UNIQUE KEY stub (stub)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

stub 字段无意义,只是为了占位,便于我们插入或者修改数据。并且,给 stub 字段创建了唯一索引,保证其唯一性。

2. 通过 replace into 来插入数据。

BEGIN;
REPLACE INTO sequence_id (stub) VALUES (‘stub’);
SELECT LAST_INSERT_ID();
COMMIT;

插入数据这里,我们没有使用 insert into 而是使用 replace into 来插入数据,具体步骤是这样的:

1) 第一步: 尝试把数据插入到表中。

2) 第二步: 如果主键或唯一索引字段出现重复数据错误而插入失败时,先从表中删除含有重复关键字值的冲突行,然后再次尝试把数据插入到表中。

这种方式的优缺点也比较明显:

  • 优点 :实现起来比较简单、ID 有序递增、存储消耗空间小
  • 缺点 : 支持的并发量不大、存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊! )、每次获取 ID 都要访问一次数据库(增加了对数据库的压力,获取速度也慢)

数据库号段模式

数据库主键自增这种模式,每次获取 ID 都要访问一次数据库,ID 需求比较大的时候,肯定是不行的。

如果我们可以批量获取,然后存在在内存里面,需要用到的时候,直接从内存里面拿就舒服了!这也就是我们说的 基于数据库的号段模式来生成分布式 ID。

数据库的号段模式也是目前比较主流的一种分布式 ID 生成方式。像滴滴开源的 Tinyid 就是基于这种方式来做的。不过,TinyId 使用了双号段缓存、增加多 db 支持等方式来进一步优化。

以 MySQL 举例,我们通过下面的方式即可。

1. 创建一个数据库表。

CREATE TABLE sequence_id_generator (
id int(10) NOT NULL,
current_max_id bigint (20) NOT NULL COMMENT ‘当前最大 id’,
step int (10) NOT NULL COMMENT ‘号段的长度’,
version int (20) NOT NULL COMMENT ‘版本号’,
biz_type int (20) NOT NULL COMMENT ‘业务类型’,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

current_max_id 字段和 step 字段主要用于获取批量 ID,获取的批量 id 为: current_max_id ~ current_max_id+step

version 字段主要用于解决并发问题(乐观锁),biz_type 主要用于表示业余类型。

2. 先插入一行数据。

INSERT INTO sequence_id_generator (id, current_max_id, step, version, biz_type)
VALUES
(1, 0, 100, 0, 101);

3. 通过 SELECT 获取指定业务下的批量唯一 ID

SELECT current_max_id, step,version FROM sequence_id_generator where biz_type = 101

id current_max_id step version biz_type
1 0 100 1 101

4. 不够用的话,更新之后重新 SELECT 即可。

UPDATE sequence_id_generator SET current_max_id = 0+100, version=version+1 WHERE version = 0 AND biz_type = 101
SELECT current_max_id, step,version FROM sequence_id_generator where biz_type = 101

id current_max_id step version biz_type
1 100 100 1 101

相比于数据库主键自增的方式,数据库的号段模式对于数据库的访问次数更少,数据库压力更小。

另外,为了避免单点问题,你可以从使用主从模式来提高可用性。

数据库号段模式的优缺点:

  • 优点 :ID 有序递增、存储消耗空间小
  • 缺点 :存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊! )

NoSQL

一般情况下,NoSQL 方案使用 Redis 多一些。我们通过 Redis 的 incr 命令即可实现对 id 原子顺序递增。

127.0.0.1:6379> set sequence_id_biz_type 1
OK
127.0.0.1:6379> incr sequence_id_biz_type
(integer) 2
127.0.0.1:6379> get sequence_id_biz_type
“2”

为了提高可用性和并发,我们可以使用 Redis Cluser。Redis Cluser 是 Redis 官方提供的 Redis 集群解决方案(3.0 + 版本)。

除了 Redis Cluser 之外,你也可以使用开源的 Redis 集群方案 Codis (大规模集群比如上百个节点的时候比较推荐)。

除了高可用和并发之外,我们知道 Redis 基于内存,我们需要持久化数据,避免重启机器或者机器故障后数据丢失。Redis 支持两种不同的持久化方式:快照(snapshotting,RDB)只追加文件(append-only file, AOF)。 并且,Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。

关于 Redis 持久化,我这里就不过多介绍。不了解这部分内容的小伙伴,可以看看 JavaGuide 对于 Redis 知识点的总结

Redis 方案的优缺点:

  • 优点 : 性能不错并且生成的 ID 是有序递增的
  • 缺点 : 和数据库主键自增方案的缺点类似

除了 Redis 之外,MongoDB ObjectId 经常也会被拿来当做分布式 ID 的解决方案。

MongoDB ObjectId 一共需要 12 个字节存储:

  • 0~3:时间戳
  • 3~6: 代表机器 ID
  • 7~8:机器进程 ID
  • 9~11 :自增值

MongoDB 方案的优缺点:

  • 优点 : 性能不错并且生成的 ID 是有序递增的
  • 缺点 : 需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID) 、有安全性问题(ID 生成有规律性)

UUID 是 Universally Unique Identifier(通用唯一标识符) 的缩写。UUID 包含 32 个 16 进制数字(8-4-4-4-12)。

JDK 就提供了现成的生成 UUID 的方法,一行代码就行了。

// 输出示例:cb4a9ede-fa5e-4585-b9bb-d60bce986eaa
UUID.randomUUID()

RFC 4122 中关于 UUID 的示例是这样的:

我们这里重点关注一下这个 Version (版本),不同的版本对应的 UUID 的生成规则是不同的。

5 种不同的 Version (版本) 值分别对应的含义(参考维基百科对于 UUID 的介绍):

  • 版本 1 : UUID 是根据时间和节点 ID(通常是 MAC 地址)生成;
  • 版本 2 : UUID 是根据标识符(通常是组或用户 ID)、时间和节点 ID 生成;
  • 版本 3、版本 5 : 版本 5 - 确定性 UUID 通过散列(hashing)名字空间(namespace)标识符和名称生成;
  • 版本 4 : UUID 使用随机性伪随机性生成。

下面是 Version 1 版本下生成的 UUID 的示例:

JDK 中通过 UUIDrandomUUID() 方法生成的 UUID 的版本默认为 4。

UUID uuid = UUID.randomUUID();
int version = uuid.version();// 4

另外,Variant (变体) 也有 4 种不同的值,这种值分别对应不同的含义。这里就不介绍了,貌似平时也不怎么需要关注。

需要用到的时候,去看看维基百科对于 UUID 的 Variant (变体) 相关的介绍即可。

从上面的介绍中可以看出,UUID 可以保证唯一性,因为其生成规则包括 MAC 地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素,计算机基于这些规则生成的 UUID 是肯定不会重复的。

虽然,UUID 可以做到全局唯一性,但是,我们一般很少会使用它。

比如使用 UUID 作为 MySQL 数据库主键的时候就非常不合适:

  • 数据库主键要尽量越短越好,而 UUID 的消耗的存储空间比较大(32 个字符串,128 位)。
  • UUID 是无顺序的,InnoDB 引擎下,数据库主键的无序性会严重影响数据库性能。

最后,我们再简单分析一下 UUID 的优缺点 (面试的时候可能会被问到的哦!) :

  • 优点 :生成速度比较快、简单易用
  • 缺点 : 存储消耗空间大(32 个字符串,128 位) 、 不安全(基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)、无序(非自增)、没有具体业务含义、需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID)

Snowflake (雪花算法)

Snowflake 是 Twitter 开源的分布式 ID 生成算法。Snowflake 由 64 bit 的二进制数字组成,这 64bit 的二进制被分成了几部分,每一部分存储的数据都有特定的含义:

  • 第 0 位: 符号位(标识正负),始终为 0,没有用,不用管。
  • 第 1~41 位 :一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 2 ^41 毫秒(约 69 年)
  • 第 42~52 位 :一共 10 位,一般来说,前 5 位表示机房 ID,后 5 位表示机器 ID(实际项目中可以根据实际情况调整)。这样就可以区分不同集群 / 机房的节点。
  • 第 53~64 位 :一共 12 位,用来表示序列号。 序列号为自增值,代表单台机器每毫秒能够产生的最大 ID 数 (2^12 = 4096), 也就是说单台机器每毫秒最多可以生成 4096 个 唯一 ID。

如果你想要使用 Snowflake 算法的话,一般不需要你自己再造轮子。有很多基于 Snowflake 算法的开源实现比如美团 的 Leaf、百度的 UidGenerator,并且这些开源实现对原有的 Snowflake 算法进行了优化。

另外,在实际项目中,我们一般也会对 Snowflake 算法进行改造,最常见的就是在 Snowflake 算法生成的 ID 中加入业务类型信息。

我们再来看看 Snowflake 算法的优缺点 :

  • 优点 :生成速度比较快、生成的 ID 有序递增、比较灵活(可以对 Snowflake 算法进行简单的改造比如加入业务 ID)
  • 缺点 : 需要解决重复 ID 问题(依赖时间,当机器时间不对的情况下,可能导致会产生重复 ID)。

UidGenerator (百度)

UidGenerator 是百度开源的一款基于 Snowflake (雪花算法) 的唯一 ID 生成器。

不过,UidGenerator 对 Snowflake (雪花算法) 进行了改进,生成的唯一 ID 组成如下。

可以看出,和原始 Snowflake (雪花算法) 生成的唯一 ID 的组成不太一样。并且,上面这些参数我们都可以自定义。

UidGenerator 官方文档中的介绍如下:

自 18 年后,UidGenerator 就基本没有再维护了,我这里也不过多介绍。想要进一步了解的朋友,可以看看 UidGenerator 的官方介绍

Leaf (美团)

Leaf 是美团开源的一个分布式 ID 解决方案 。这个项目的名字 Leaf(树叶) 起源于德国哲学家、数学家莱布尼茨的一句话: “There are no two identical leaves in the world”(世界上没有两片相同的树叶) 。这名字起得真心挺不错的,有点文艺青年那味了!

Leaf 提供了 号段模式Snowflake (雪花算法) 这两种模式来生成分布式 ID。并且,它支持双号段,还解决了雪花 ID 系统时钟回拨问题。不过,时钟问题的解决需要弱依赖于 Zookeeper 。

Leaf 的诞生主要是为了解决美团各个业务线生成分布式 ID 的方法多种多样以及不可靠的问题。

Leaf 对原有的号段模式进行改进,比如它这里增加了双号段避免获取 DB 在获取号段的时候阻塞请求获取 ID 的线程。简单来说,就是我一个号段还没用完之前,我自己就主动提前去获取下一个号段(图片来自于美团官方文章:《Leaf—— 美团点评分布式 ID 生成系统》)。

根据项目 README 介绍,在 4C8G VM 基础上,通过公司 RPC 方式调用,QPS 压测结果近 5w/s,TP999 1ms。

Tinyid (滴滴)

Tinyid 是滴滴开源的一款基于数据库号段模式的唯一 ID 生成器。

数据库号段模式的原理我们在上面已经介绍过了。Tinyid 有哪些亮点呢?

为了搞清楚这个问题,我们先来看看基于数据库号段模式的简单架构方案。(图片来自于 Tinyid 的官方 wiki:《Tinyid 原理介绍》

在这种架构模式下,我们通过 HTTP 请求向发号器服务申请唯一 ID。负载均衡 router 会把我们的请求送往其中的一台 tinyid-server。

这种方案有什么问题呢?在我看来(Tinyid 官方 wiki 也有介绍到),主要由下面这 2 个问题:

  • 获取新号段的情况下,程序获取唯一 ID 的速度比较慢。
  • 需要保证 DB 高可用,这个是比较麻烦且耗费资源的。

除此之外,HTTP 调用也存在网络开销。

Tinyid 的原理比较简单,其架构如下图所示:

相比于基于数据库号段模式的简单架构方案,Tinyid 方案主要做了下面这些优化:

  • 双号段缓存 :为了避免在获取新号段的情况下,程序获取唯一 ID 的速度比较慢。 Tinyid 中的号段在用到一定程度的时候,就会去异步加载下一个号段,保证内存中始终有可用号段。
  • 增加多 db 支持 :支持多个 DB,并且,每个 DB 都能生成唯一 ID,提高了可用性。
  • 增加 tinyid-client :纯本地操作,无 HTTP 请求消耗,性能和可用性都有很大提升。

Tinyid 的优缺点这里就不分析了,结合数据库号段模式的优缺点和 Tinyid 的原理就能知道。

分布式 ID 方案

下表为一些常用方案对比:

图片

目前流行的分布式 ID 解决方案有两种:「号段模式」和「雪花算法」。

「号段模式」依赖于数据库,但是区别于数据库主键自增的模式。假设 100 为一个号段 100,200,300,每取一次可以获得 100 个 ID,性能显著提高。

「雪花算法」是由符号位 + 时间戳 + 工作机器 id + 序列号组成的,如图所示:

图片

符号位为 0,0 表示正数,ID 为正数。

时间戳位不用多说,用来存放时间戳,单位是 ms。

工作机器 id 位用来存放机器的 id,通常分为 5 个区域位 + 5 个服务器标识位。

序号位是自增。

雪花算法能存放多少数据?时间范围:2^41 / (3652460601000) = 69 年 工作进程范围:2^10 = 1024 序列号范围:2^12 = 4096,表示 1ms 可以生成 4096 个 ID。

根据这个算法的逻辑,只需要将这个算法用 Java 语言实现出来,封装为一个工具方法,那么各个业务应用可以直接使用该工具方法来获取分布式 ID,只需保证每个业务应用有自己的工作机器 id 即可,而不需要单独去搭建一个获取分布式 ID 的应用。下面是推特版的 Snowflake 算法:

public class SnowFlake {  

/**
     * 起始的时间戳
     */
    private final static long START_STMP = 1480166465631L;

/**
     * 每一部分占用的位数
     */
    private final static long SEQUENCE_BIT = 12; //序列号占用的位数
    private final static long MACHINE_BIT = 5;   //机器标识占用的位数
    private final static long DATACENTER_BIT = 5;//数据中心占用的位数

/**
     * 每一部分的最大值
     */
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

/**
     * 每一部分向左的位移
     */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

private long datacenterId;  //数据中心
    private long machineId;     //机器标识
    private long sequence = 0L; //序列号
    private long lastStmp = -1L;//上一次时间戳

public SnowFlake(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }

/**
     * 产生下一个ID
     *
     * @return
     */
    public synchronized long nextId() {
        long currStmp = getNewstmp();
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

if (currStmp == lastStmp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                currStmp = getNextMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }

lastStmp = currStmp;

return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
                | datacenterId << DATACENTER_LEFT       //数据中心部分
                | machineId << MACHINE_LEFT             //机器标识部分
                | sequence;                             //序列号部分
    }

private long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }

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

public static void main(String[] args) {
        SnowFlake snowFlake = new SnowFlake(2, 3);

for (int i = 0; i < (1 << 12); i++) {
            System.out.println(snowFlake.nextId());
        }

}
}

今天主要分析一下以下 9 种,分布式 ID 生成器方式以及优缺点:

  • 数据库自增 ID
  • 数据库多主模式
  • Redis
  • 雪花算法(SnowFlake)
  • 滴滴出品(TinyID)
  • 百度 (Uidgenerator)
  • 美团(Leaf)

那么它们都是如何实现?以及各自有什么优缺点?我们往下看

图片

图片源自网络

以上图片源自网络,如有侵权联系删除

1. 基于 UUID

在 Java 的世界里,想要得到一个具有唯一性的 ID,首先被想到可能就是 UUID,毕竟它有着全球唯一的特性。那么 UUID 可以做分布式ID 吗?答案是可以的,但是并不推荐!

public static void main(String[] args) {  
String uuid = UUID.randomUUID().toString().replaceAll("-","");
System.out.println(uuid);
}

UUID 的生成简单到只有一行代码,输出结果 c2b8c2b9e46c47e3b30dca3b0d447718,但 UUID 却并不适用于实际的业务需求。像用作订单号 UUID 这样的字符串没有丝毫的意义,看不出和订单相关的有用信息;而对于数据库来说用作业务主键ID,它不仅是太长还是字符串,存储性能差查询也很耗时,所以不推荐用作分布式ID

优点:

  • 生成足够简单,本地生成无网络消耗,具有唯一性

缺点:

  • 无序的字符串,不具备趋势自增特性
  • 没有具体的业务含义
  • 长度过长 16 字节 128 位,36 位长度的字符串,存储以及查询对 MySQL 的性能消耗较大,MySQL 官方明确建议主键要尽量越短越好,作为数据库主键 UUID 的无序性会导致数据位置频繁变动,严重影响性能。

2. 基于数据库自增 ID

基于数据库的 auto_increment 自增 ID 完全可以充当分布式ID,具体实现:需要一个单独的 MySQL 实例用来生成 ID,建表结构如下:

CREATEDATABASE`SEQ_ID`;  
CREATETABLE SEQID.SEQUENCE_ID (
idbigint(20) unsignedNOTNULL auto_increment,
valuechar(10) NOTNULLdefault'',
PRIMARY KEY (id),
) ENGINE=MyISAM;
insert into SEQUENCE_ID(value)  VALUES ('values');

当我们需要一个 ID 的时候,向表中插入一条记录返回主键ID,但这种方式有一个比较致命的缺点,访问量激增时 MySQL 本身就是系统的瓶颈,用它来实现分布式服务风险比较大,不推荐!

优点:

  • 实现简单,ID 单调自增,数值类型查询速度快

缺点:

  • DB 单点存在宕机风险,无法扛住高并发场景

3. 基于数据库集群模式

前边说了单点数据库方式不可取,那对上边的方式做一些高可用优化,换成主从模式集群。害怕一个主节点挂掉没法用,那就做双主模式集群,也就是两个 Mysql 实例都能单独的生产自增 ID。

那这样还会有个问题,两个 MySQL 实例的自增 ID 都从 1 开始,会生成重复的 ID 怎么办?

解决方案:设置起始值自增步长

MySQL_1 配置:

set @@auto_increment_offset = 1;     -- 起始值
set @@auto_increment_increment = 2; -- 步长

MySQL_2 配置:

set @@auto_increment_offset = 2;     -- 起始值
set @@auto_increment_increment = 2; -- 步长

这样两个 MySQL 实例的自增 ID 分别就是:

1、3、5、7、9 2、4、6、8、10

那如果集群后的性能还是扛不住高并发咋办?就要进行 MySQL 扩容增加节点,这是一个比较麻烦的事。

图片

在这里插入图片描述

从上图可以看出,水平扩展的数据库集群,有利于解决数据库单点压力的问题,同时为了 ID 生成特性,将自增步长按照机器数量来设置。

增加第三台 MySQL 实例需要人工修改一、二两台 MySQL实例的起始值和步长,把第三台机器的ID 起始生成位置设定在比现有最大自增ID 的位置远一些,但必须在一、二两台 MySQL实例 ID 还没有增长到第三台MySQL实例起始ID 值的时候,否则自增ID 就要出现重复了,必要时可能还需要停机修改

优点:

  • 解决 DB 单点问题

缺点:

  • 不利于后续扩容,而且实际上单个数据库自身压力还是大,依旧无法满足高并发场景。

4. 基于数据库的号段模式

号段模式是当下分布式 ID 生成器的主流实现方式之一,号段模式可以理解为从数据库批量的获取自增 ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表 1000 个 ID,具体的业务服务将本号段,生成 1~1000 的自增 ID 并加载到内存。表结构如下:

CREATETABLE id_generator (  
idint(10) NOTNULL,
max_id bigint(20) NOTNULLCOMMENT'当前最大id',
step int(20) NOTNULLCOMMENT'号段的布长',
biz_type int(20) NOTNULLCOMMENT'业务类型',
versionint(20) NOTNULLCOMMENT'版本号',
PRIMARY KEY (`id`)
)

biz_type :代表不同业务类型

max_id :当前最大的可用 id

step :代表号段的长度

version :是一个乐观锁,每次都更新 version,保证并发时数据的正确性

等这批号段 ID 用完,再次向数据库申请新号段,对 max_id 字段做一次 update 操作,update max_id= max_id + step,update 成功则说明新号段获取成功,新的号段范围是 (max_id ,max_id +step]

update id_generator 
set max_id = #{max_id+step}, version = version + 1
where version = # {version}
and biz_type = XXX

由于多业务端可能同时操作,所以采用版本号 version 乐观锁方式更新,这种分布式ID 生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。

5. 基于 Redis 模式

Redis 也同样可以实现,原理就是利用 redisincr 命令实现 ID 的原子性自增。

127.0.0.1:6379> set seq_id 1     // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id // 增加1,并返回递增后的数值
(integer) 2

redis 实现需要注意一点,要考虑到 redis 持久化的问题。redis 有两种持久化方式 RDBAOF

  • RDB 会定时打一个快照进行持久化,假如连续自增但 redis 没及时持久化,而这会 Redis 挂掉了,重启 Redis 后会出现 ID 重复的情况。
  • AOF 会对每条写命令进行持久化,即使 Redis 挂掉了也不会出现 ID 重复的情况,但由于 incr 命令的特殊性,会导致 Redis 重启恢复的数据时间过长。

6. 基于雪花算法(Snowflake)模式

雪花算法(Snowflake)是 twitter 公司内部分布式项目采用的 ID 生成算法,开源后广受国内大厂的好评,在该算法影响下各大公司相继开发出各具特色的分布式生成器。

图片

在这里插入图片描述

以上图片源自网络,如有侵权联系删除

Snowflake 生成的是 Long 类型的 ID,一个 Long 类型占 8 个字节,每个字节占 8 比特,也就是说一个 Long 类型占 64 个比特。

Snowflake ID 组成结构:正数位(占 1 比特)+ 时间戳(占 41 比特)+ 机器ID(占 5 比特)+ 数据中心(占 5 比特)+ 自增值(占 12 比特),总共 64 比特组成的一个 Long 类型。

  • 第一个 bit 位(1bit):Java 中 long 的最高位是符号位代表正负,正数是 0,负数是 1,一般生成 ID 都为正数,所以默认为 0。
  • 时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的 ID 从更小的值开始;41 位的时间戳可以使用 69 年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69 年
  • 工作机器 id(10bit):也被叫做 workId,这个可以灵活配置,机房或者机器号组合都可以。
  • 序列号部分(12bit),自增值支持同一毫秒内同一个节点可以生成 4096 个 ID

根据这个算法的逻辑,只需要将这个算法用 Java 语言实现出来,封装为一个工具方法,那么各个业务应用可以直接使用该工具方法来获取分布式 ID,只需保证每个业务应用有自己的工作机器 id 即可,而不需要单独去搭建一个获取分布式 ID 的应用。

Java 版本的 Snowflake 算法实现:

/**  
* Twitter的SnowFlake算法,使用SnowFlake算法生成一个整数,然后转化为62进制变成一个短地址URL * <p>
* https://github.com/beyondfengyu/SnowFlake */public class SnowFlakeShortUrl {

/**
* 起始的时间戳 */ privatefinalstaticlong START_TIMESTAMP = 1480166465631L;

/**
* 每一部分占用的位数 */ privatefinalstaticlong SEQUENCE_BIT = 12; //序列号占用的位数
privatefinalstaticlong MACHINE_BIT = 5; //机器标识占用的位数
privatefinalstaticlong DATA_CENTER_BIT = 5; //数据中心占用的位数

/** * 每一部分的最大值 */ privatefinalstaticlong MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
privatefinalstaticlong MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
privatefinalstaticlong MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);

/**
* 每一部分向左的位移 */ privatefinalstaticlong MACHINE_LEFT = SEQUENCE_BIT;
privatefinalstaticlong DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
privatefinalstaticlong TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;

privatelong dataCenterId; //数据中心
privatelong machineId; //机器标识
privatelong sequence = 0L; //序列号
privatelong lastTimeStamp = -1L; //上一次时间戳

private long getNextMill() {
long mill = getNewTimeStamp();
while (mill <= lastTimeStamp) {
mill = getNewTimeStamp();
}
return mill;
}

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

/**
* 根据指定的数据中心ID和机器标志ID生成指定的序列号 * * @param dataCenterId 数据中心ID
* @param machineId 机器标志ID
*/ public SnowFlakeShortUrl(long dataCenterId, long machineId) {
if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
thrownew IllegalArgumentException ("DtaCenterId can't be greater than MAX_DATA_CENTER_NUM or less than 0!");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
thrownew IllegalArgumentException ("MachineId can't be greater than MAX_MACHINE_NUM or less than 0!");
}
this.dataCenterId = dataCenterId;
this.machineId = machineId;
}

/**
* 产生下一个ID * * @return
*/
public synchronized long nextId() {
long currTimeStamp = getNewTimeStamp();
if (currTimeStamp < lastTimeStamp) {
thrownew RuntimeException ("Clock moved backwards. Refusing to generate id");
}

if (currTimeStamp == lastTimeStamp) {
//相同毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列数已经达到最大
if (sequence == 0L) {
currTimeStamp = getNextMill();
}
} else {
//不同毫秒内,序列号置为0
sequence = 0L;
}

lastTimeStamp = currTimeStamp;

return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT //时间戳部分
| dataCenterId << DATA_CENTER_LEFT //数据中心部分
| machineId << MACHINE_LEFT //机器标识部分
| sequence; //序列号部分
}

public static void main(String[] args) {
SnowFlakeShortUrl snowFlake = new SnowFlakeShortUrl(2, 3);

for (int i = 0; i < (1 << 4); i++) {
//10进制
System.out.println(snowFlake.nextId());
}
}
}

7. 百度(uid-generator)

uid-generator 是由百度技术部开发,项目 GitHub 地址 github.com/baidu/uid-g…

uid-generator 是基于 Snowflake 算法实现的,与原始的 snowflake 算法不同在于,uid-generator 支持自定义时间戳工作机器ID序列号 等各部分的位数,而且 uid-generator 中采用用户自定义 workId 的生成策略。

uid-generator 需要与数据库配合使用,需要新增一个 WORKER_NODE 表。当应用启动时会向数据库表中去插入一条数据,插入成功后返回的自增 ID 就是该机器的 workId 数据由 host,port 组成。

对于 uid-generator ID 组成结构

workId,占用了 22 个 bit 位,时间占用了 28 个 bit 位,序列化占用了 13 个 bit 位,需要注意的是,和原始的 snowflake 不太一样,时间的单位是秒,而不是毫秒,workId 也不一样,而且同一应用每次重启就会消费一个 workId

参考文献 github.com/baidu/uid-g…

8. 美团(Leaf)

Leaf 由美团开发,github 地址:github.com/Meituan-Dia…

Leaf 同时支持号段模式和 snowflake 算法模式,可以切换使用。

先导入源码 github.com/Meituan-Dia… ,在建一张表 leaf_alloc

DROPTABLEIFEXISTS`leaf_alloc`;  

CREATETABLE`leaf_alloc` (
`biz_tag`varchar(128) NOTNULLDEFAULT''COMMENT'业务key',
`max_id`bigint(20) NOTNULLDEFAULT'1'COMMENT'当前已经分配了的最大id',
`step`int(11) NOTNULLCOMMENT'初始步长,也是动态调整的最小步长',
`description`varchar(256) DEFAULTNULLCOMMENT'业务key的描述',
`update_time`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'数据库维护的更新时间',
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;

然后在项目中开启号段模式,配置对应的数据库信息,并关闭 snowflake 模式

leaf.name=com.sankuai.leaf.opensource.test
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://localhost:3306/leaf_test?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
leaf.jdbc.username=root
leaf.jdbc.password=root
leaf.snowflake.enable=false
#leaf.snowflake.zk.address=
#leaf.snowflake.port=

启动 leaf-server 模块的 LeafServerApplication 项目就跑起来了

号段模式获取分布式自增 ID 的测试 url :http:<//localhost>:8080/api/segment/get/leaf-segment-test

监控号段模式:http://localhost:8080/cache

snowflake 模式

Leaf 的 snowflake 模式依赖于 ZooKeeper,不同于原始snowflake 算法也主要是在 workId 的生成上,LeafworkId 是基于 ZooKeeper 的顺序 Id 来生成的,每个应用在使用 Leaf-snowflake 时,启动时都会都在 Zookeeper 中生成一个顺序 Id,相当于一台机器对应一个顺序节点,也就是一个 workId

leaf.snowflake.enable=true
leaf.snowflake.zk.address=127.0.0.1
leaf.snowflake.port=2181

snowflake 模式获取分布式自增 ID 的测试 url:http://localhost:8080/api/snowflake/get/test

9、滴滴(Tinyid)

Tinyid 由滴滴开发,Github 地址:github.com/didi/tinyid…

Tinyid 是基于号段模式原理实现的与 Leaf 如出一辙,每个服务获取一个号段(1000,2000]、(2000,3000]、(3000,4000]

图片

在这里插入图片描述

Tinyid 提供 httptinyid-client 两种方式接入

Http 方式接入

(1)导入 Tinyid 源码:

git clone github.com/didi/tinyid…

(2)创建数据表:

CREATETABLE`tiny_id_info` (  
`id`bigint(20) unsignedNOTNULL AUTO_INCREMENT COMMENT'自增主键',
`biz_type`varchar(63) NOTNULLDEFAULT''COMMENT'业务类型,唯一',
`begin_id`bigint(20) NOTNULLDEFAULT'0'COMMENT'开始id,仅记录初始值,无其他含义。初始化时begin_id和max_id应相同',
`max_id`bigint(20) NOTNULLDEFAULT'0'COMMENT'当前最大id',
`step`int(11) DEFAULT'0'COMMENT'步长',
`delta`int(11) NOTNULLDEFAULT'1'COMMENT'每次id增量',
`remainder`int(11) NOTNULLDEFAULT'0'COMMENT'余数',
`create_time`timestampNOTNULLDEFAULT'2010-01-01 00:00:00'COMMENT'创建时间',
`update_time`timestampNOTNULLDEFAULT'2010-01-01 00:00:00'COMMENT'更新时间',
`version`bigint(20) NOTNULLDEFAULT'0'COMMENT'版本号',
PRIMARY KEY (`id`),
UNIQUEKEY`uniq_biz_type` (`biz_type`)
) ENGINE=InnoDB AUTO_INCREMENT=1DEFAULTCHARSET=utf8 COMMENT'id信息表';

CREATETABLE`tiny_id_token` (
`id`int(11) unsignedNOTNULL AUTO_INCREMENT COMMENT'自增id',
`token`varchar(255) NOTNULLDEFAULT''COMMENT'token',
`biz_type`varchar(63) NOTNULLDEFAULT''COMMENT'此token可访问的业务类型标识',
`remark`varchar(255) NOTNULLDEFAULT''COMMENT'备注',
`create_time`timestampNOTNULLDEFAULT'2010-01-01 00:00:00'COMMENT'创建时间',
`update_time`timestampNOTNULLDEFAULT'2010-01-01 00:00:00'COMMENT'更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1DEFAULTCHARSET=utf8 COMMENT'token信息表';

INSERTINTO`tiny_id_info` (`id`, `biz_type`, `begin_id`, `max_id`, `step`, `delta`, `remainder`, `create_time`, `update_time`, `version`)
VALUES
(1, 'test', 1, 1, 100000, 1, 0, '2018-07-21 23:52:58', '2018-07-22 23:19:27', 1);

INSERTINTO`tiny_id_info` (`id`, `biz_type`, `begin_id`, `max_id`, `step`, `delta`, `remainder`, `create_time`, `update_time`, `version`)
VALUES
(2, 'test_odd', 1, 1, 100000, 2, 1, '2018-07-21 23:52:58', '2018-07-23 00:39:24', 3);


INSERTINTO`tiny_id_token` (`id`, `token`, `biz_type`, `remark`, `create_time`, `update_time`)
VALUES
(1, '0f673adf80504e2eaa552f5d791b644c', 'test', '1', '2017-12-14 16:36:46', '2017-12-14 16:36:48');

INSERTINTO`tiny_id_token` (`id`, `token`, `biz_type`, `remark`, `create_time`, `update_time`)
VALUES
(2, '0f673adf80504e2eaa552f5d791b644c', 'test_odd', '1', '2017-12-14 16:36:46', '2017-12-14 16:36:48');

(3)配置数据库:

datasource.tinyid.names=primary
datasource.tinyid.primary.driver-class-name=com.mysql.jdbc.Driver
datasource.tinyid.primary.url=jdbc:mysql://ip:port/databaseName?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
datasource.tinyid.primary.username=root
datasource.tinyid.primary.password=123456

(4)启动 tinyid-server 后测试

获取分布式自增ID: 
http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&token=0f673adf80504e2eaa552f5d791b644c'
返回结果: 3

批量获取分布式自增ID:
http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&token=0f673adf80504e2eaa552f5d791b644c&batchSize=10'返回结果: 4,5,6,7,8,9,10,11,12,13

Java 客户端方式接入

重复 Http 方式的(2)(3)操作

<dependency>            
<groupId>com.xiaoju.uemc.tinyid</groupId> <artifactId>tinyid-client</artifactId> <version>${tinyid.version}</version>
</dependency>
tinyid.server = localhost:9999
tinyid.token = 0f673adf80504e2eaa552f5d791b644c

testtinyid.token 是在数据库表中预先插入的数据,test 是具体业务类型,tinyid.token 表示可访问的业务类型

// 获取单个分布式自增ID
Long id = TinyId . nextId( " test " );
// 按需批量分布式自增
IDList< Long > ids = TinyId . nextId( " test " , 10 );

参考文章:
(……) 「Java 面试指北」为什么需要分布式 ID?大厂的分布式 ID 生成方案是什么样的?| JavaGuide - SegmentFault 思否
一口气说出 9 种分布式 ID 生成方式,面试官有点懵了! (qq.com)
【面朝大厂】面试官:说几种常用的分布式 ID 解决方案 (qq.com)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK