为什么分布式系统需要用到ID生成系统

在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。如在美团点评的金融、支付、餐饮、酒店、猫眼电影等产品的系统中,数据日渐增长,对数据库的分库分表后需要有一个唯一ID来标识一条数据或消息,数据库的自增ID显然不能满足需求;特别一点的如订单、骑手、优惠券也都需要有唯一ID做标识。此时一个能够生成全局唯一ID的系统是非常必要的。

概括下来,业务系统对ID号的要求有哪些呢?

ID生成系统的需求

1.全局唯一性:不能出现重复的ID,最基本的要求。
2.趋势递增:MySQL InnoDB引擎使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应尽量使用有序的主键保证写入性能。
3.单调递增:保证下一个ID一定大于上一个ID。
4.信息安全:如果ID是连续递增的,恶意用户就可以很容易的窥见订单号的规则,从而猜出下一个订单号,如果是竞争对手,就可以直接知道我们一天的订单量。所以在某些场景下,需要ID无规则。

第3、4两个需求是互斥的,无法同时满足。

同时,在大型分布式网站架构中,除了需要满足ID生成自身的需求外,还需要ID生成系统可用性极高。想象以下,如果ID生成系统瘫痪,那么整个业务无法进行下去,那将是一次灾难。
因此,总结ID生成系统还需要满足如下的需求:
1.高可用,可用性达到5个9或4个9。
2.高QPS,性能不能太差,否则容易造成线程堵塞。
3.平均延迟和TP999(保证99.9%的请求都能成功的最低延迟)延迟都要尽可能低。

ID生成系统的类型

UUID

UUID是指在一台机器在同一时间中生成的数字在所有机器中都是唯一的。按照开放软件基金会(OSF)制定的标准计算,用到了以太网卡地址、纳秒级时间、芯片ID码和许多可能的数字
UUID由以下几部分的组合:
(1)当前日期和时间。
(2)时钟序列。
(3)全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。
标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12),以连字号分为五段形式的36个字符,示例:550e8400-e29b-41d4-a716-446655440000
Java标准类库中已经提供了UUID的API。

UUID.randomUUID()

优点

  • 性能非常高:本地生成,没有网络消耗。

缺点

  • 不易存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
  • 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
  • ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用。

SnowFlake雪花算法

雪花ID生成的是一个64位的二进制正整数,然后转换成10进制的数。64位二进制数由如下部分组成:

 
snowflake id生成规则
  • 1位标识符:始终是0,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0。
  • 41位时间戳:41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截 )得到的值,这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的。
  • 10位机器标识码:可以部署在1024个节点,如果机器分机房(IDC)部署,这10位可以由 5位机房ID + 5位机器ID 组成。
  • 12位序列:毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号

优点

  • 简单高效,生成速度快。
  • 时间戳在高位,自增序列在低位,整个ID是趋势递增的,按照时间有序递增。
  • 灵活度高,可以根据业务需求,调整bit位的划分,满足不同的需求。

缺点

  • 依赖机器的时钟,如果服务器时钟回拨,会导致重复ID生成。
  • 在分布式环境上,每个服务器的时钟不可能完全同步,有时会出现不是全局递增的情况。

snowflake Java实现

/**
* 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 = 1420041600000L; /** 机器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; //==============================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 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
} /**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
} //==============================Test=============================================
/** 测试 */
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);
}
}
}

数据库自增ID机制

主要思路是采用数据库自增ID + replace_into实现唯一ID的获取。

create table t_global_id(
id bigint(20) unsigned not null auto_increment,
stub char(1) not null default '',
primary key (id),
unique key stub (stub)
) engine=MyISAM;
# 每次业务可以使用以下SQL读写MySQL得到ID号
replace into t_golbal_id(stub) values('a');
select last_insert_id();

replace into跟insert功能类似,不同点在于:replace into首先尝试插入数据列表中,如果发现表中已经有此行数据(根据主键或唯一索引判断)则先删除,再插入。否则直接插入新数据。
当然为了避免数据库的单点故障,最少需要两个数据库实例,通过区分auto_increment的起始值和步长来生成奇偶数的ID。如下:

Server1:
auto-increment-increment = 2
auto-increment-offset = 1 Server2:
auto-increment-increment = 2
auto-increment-offset = 2

优点

  • 简单。充分借助数据库的自增ID机制,可靠性高,生成有序的ID。

缺点

  • ID生成依赖数据库单机的读写性能。
  • 依赖数据库,当数据库异常时整个系统不可用。

对于MySQL的性能问题,可以用如下方案解决
在分布式环境中,我们可以部署N台数据库实例,每台设置成不同的初始值,自增步长为机器的台数。每台的初始值分别为1,2,3...N,步长为N。

 
MySQL数据库自增ID多机图

以上方案虽然解决了性能问题,但是也存在很大的局限性:

  • 系统水平扩容困难:系统定义好步长之后,增加机器之后调整步长困难。如果要添加机器怎么办?假设现在只有一台机器发号是1,2,3,4,5(步长是1),这个时候需要扩容机器一台。可以这样做:把第二台机器的初始值设置得比第一台超过很多,比如14(假设在扩容时间之内第一台不可能发到14),同时设置步长为2,那么这台机器下发的号码都是14以后的偶数。然后摘掉第一台,把ID值保留为奇数,比如7,然后修改第一台的步长为2。让它符合我们定义的号段标准,对于这个例子来说就是让第一台以后只能产生奇数。扩容方案看起来复杂吗?貌似还好,现在想象一下如果我们线上有100台机器,这个时候要扩容该怎么做?简直是噩梦。
  • 数据库压力大:每次获取一个ID都必须读写一次数据库。当然对于这种问题,也有相应的解决方案,就是每次获取ID时都批量获取一个区间的号段到内存中,用完之后再来获取。数据库的性能提高了几个量级。

第三方软件生成(Redis)

Redis实现了一个原子操作INCR和INCRBY实现递增的操作。当使用数据库性能不够时,可以采用Redis来代替,同时使用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

优点

  • 不依赖于数据库,灵活方便,且性能优于数据库。
  • 数字ID天然排序,对分页或者需要排序的结果很有帮助。

缺点:

  • 如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
  • 需要编码和配置的工作量比较大。这个都不是最大的问题。

关于分布式全局唯一ID的生成,各个互联网公司有很多实现方案,比如美团点评的Leaf-snowflake,用zookeeper解决了各个服务器时钟回拨的问题,弱依赖zookeeper。以及Leaf-segment类似上面数据库批量ID获取的方案。

参考

分布式全局唯一ID生成策略的更多相关文章

  1. 常见分布式全局唯一ID生成策略

    全局唯一的 ID 几乎是所有系统都会遇到的刚需.这个 id 在搜索, 存储数据, 加快检索速度 等等很多方面都有着重要的意义.工业上有多种策略来获取这个全局唯一的id,针对常见的几种场景,我在这里进行 ...

  2. 分布式全局唯一ID生成策略​

    一.背景 分布式系统中我们会对一些数据量大的业务进行分拆,如:用户表,订单表.因为数据量巨大一张表无法承接,就会对其进行分库分表. 但一旦涉及到分库分表,就会引申出分布式系统中唯一主键ID的生成问题. ...

  3. (4.24)【mysql、sql server】分布式全局唯一ID生成方案

    参考:分布式全局唯一ID生成方案:https://blog.csdn.net/linzhiqiang0316/article/details/80425437 分表生成唯一ID方案 sql serve ...

  4. 分布式全局唯一ID的实现

    分布式全局唯一ID的实现 前言 上周末考完试,这周正好把工作整理整理,然后也把之前的一些素材,整理一番,也当自己再学习一番. 一方面正好最近看到几篇这方面的文章,另一方面也是正好工作上有所涉及,所以决 ...

  5. 框架篇:分布式全局唯一ID

    前言 每一次HTTP请求,数据库的事务的执行,我们追踪代码执行的过程中,需要一个唯一值和这些业务操作相关联,对于单机的系统,可以用数据库的自增ID或者时间戳加一个在本机递增值,即可实现唯一值.但在分布 ...

  6. Mysql系列七:分库分表技术难题之分布式全局唯一id解决方案

    一.前言 在前面的文章Mysql系列四:数据库分库分表基础理论中,已经说过分库分表需要应对的技术难题有如下几个: 1. 分布式全局唯一id 2. 分片规则和策略 3. 跨分片技术问题 4. 跨分片事物 ...

  7. 分布式全局唯一ID与自增序列

    包含时间顺序的ID 此场景最简单的实现方案,就是采用 twitter 的 Snowflake 算法.ID总长64位,第1位不可用,41位表示时间戳,10位表示生成机器的id,后12位表示序列号. 为什 ...

  8. 关于全局唯一ID生成方法

    引:最近业务开发过程中需要涉及到全局唯一ID生成.之前零零总总的收集过一些相关资料,特此整理以便后用 本博客已经迁移至:http://cenalulu.github.io/ 本篇博文已经迁移,阅读全文 ...

  9. 数据库分库分表(一)常见分布式主键ID生成策略

    主键生成策略 系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,下面介绍一些常见的ID生成策略. Sequence ID UUID GUID COMB Snowflake 最开始的自增ID为了实 ...

随机推荐

  1. 2019-1-9-WPF-最小的代码使用-DynamicRenderer-书写

    title author date CreateTime categories WPF 最小的代码使用 DynamicRenderer 书写 lindexi 2019-1-9 14:7:26 +080 ...

  2. ROS开发过程中遇到:Could not find a package configuration file provided by "qt_build" with any of the following names: qt_buildConfig.cmake qt_build-config.cmake........

    最近在搭建QT开发ROS 界面的环境,遇到了很多问题,参考了很多资料,最后发现有些问题其实没有那么复杂,只是我们对整体环境还不了解,熟悉了以后你会发现有些问题就迎刃而解了. 在这个过程中,我首先新建了 ...

  3. Android Tween和Frame 动画

    关于动画的实现,Android提供了Animation,在Android SDK介绍了2种Animation模式: 1. Tween Animation:通过对场景里的对象不断做图像变换(平移.缩放. ...

  4. phpstorm2017破解版 2017.3.4 官网中文版

    phpstorm2017破解版是一款强大的PHP编程工具,新云软件园提供phpstorm激活下载,最新版PhpStorm 2017正式版改进了PHP 7支持,改进代码完成功能,PhpStorm 是最好 ...

  5. VS Code导入已存在的Vue.js工程

    打开vscode------->文件--------->打开文件夹--------->选择工程文件夹-------->确定 查看---->终端或者使用"Ctrl ...

  6. 虎牙在全球 DNS 秒级生效上的实践

    本文整理自虎牙中间件团队在 Nacos Meetup 的现场分享,阿里巴巴中间件受权发布. 这次分享的是全球 DNS 秒级生效在虎牙的实践,以及由此产生的一些思考,整体上,分为以下5各部分: 背景介绍 ...

  7. 根据User Agent参数的各个字段Mozilla/5.0/4.0-AppleWebKit/Chrome/Safari/Firefox/Opera/MSIE来确定/判断客户端使用什么浏览器

    下面给你一一解答以及给你介绍: //Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/533.21.1 (KHTML, like ...

  8. hdu 1277 全文检索 (直接映射查找 || 自动机)

    Problem - 1277 无聊做水题的时候发现的一道题目.这道题第一反应可以用自动机来解决.当然,条件是各种限制,从而导致可以用直接映射标记的方法来搜索.具体的做法就像RK算法一样,将字符串has ...

  9. Redis在Laravel项目中的应用实例详解

    https://mp.weixin.qq.com/s/axIgNPZLJDh9VFGVk7oYYA 在初步了解Redis在Laravel中的应用 那么我们试想这样的一个应用场景 一个文章或者帖子的浏览 ...

  10. error LNK2001: unresolved external symbol __imp___rt_sdiv

    这个问题搞了我 5 天(包括双休日), 我一定要记录下来. 问题描述 用 Visual Studio 2008 编译 WinCE 7 平台的应用程序,编译没问题,链接时出现了一堆 Link error ...