分布式全局惟一ID生成策略

为何分布式系统须要用到ID生成系统

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

归纳下来,业务系统对ID号的要求有哪些呢?java

ID生成系统的需求

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

第三、4两个需求是互斥的,没法同时知足。sql

同时,在大型分布式网站架构中,除了须要知足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获取的方案。

做者:Misout连接:https://www.jianshu.com/p/9d7ebe37215e来源:简书

相关文章
相关标签/搜索