在分布式系统中,需要对大量的数据、消息、请求等进行唯一的标识,例如分布式数据库的ID需要满足唯一且多数据库同步,在单一系统中,使用数据库自增主键可以满足需求,但是在分布式系统中就需要一个能够生成全局唯一ID的系统,而且还要满足高可用。

数据库自增长字段

本文只整理MySQL的自增字段方案,OracleSQL Server的自增长方案就不介绍了。

MySQL自增列使用auto_increment标识字段达到自增,在创建表时将某一列定义为auto_increment,则改列为自增列。这定了auto_increment的列必须建立索引。

auto_increment使用说明

  • 如果把一个NULL插入到一个auto_increment数据列中,MySQL将自动生成下一个序列编号。编号从1开始,并以1为基数递增;

  • 把0插入auto_increment数据列的效果与插入NULL值一样,但是不建议这样做,还是以插入NULL值为好;

  • 当插入记录时,没有为auto_increment明确指定值,则等同于插入NULL值;

  • 当插入记录时,如果为auto_increment数据列明确指定了一个数值,则会出现两种情况,情况一,如果插入的值与已有的编号重复,则会出现出错信息,因为auto_increment数据列的值必须是唯一的;情况二,如果插入的值大于已编号的值,则会把该值插入到数据列中,并使在下一个编号将这个新值开始递增。也即可以跳过一些编号;

  • 如果用update命令更新自增列,如果列值与已有的值重复,则会出错。如果大于已有值,则下一个编号从该值开始递增。

相关配置

MySQL中的自增长字段,在做数据库的主主同步时需要在参数文件中设置自增长的两个相关配置:

  • auto_increment:自增长字段从哪个数开始,取值范围是:1~65535

  • auto_increment_increment:自增长字段每次递增的量,即步长,默认值是1,取值范围是1~65535

优化方案:在配置集群的MySQL时,需要将n台服务器的auto_increment_increment都配置为n,而要把auto_increment_offset分别配置为1,2,……,n。这样才可以避免多台服务器更新时自增长字段的值之间出现冲突。

优缺点

优点:

  • 很小的数据存储空间,简单,代码方便,性能可以接受

  • 数字ID天然排序,容易记忆,对分页或者需要排序的结果很有帮助

缺点:

  • 如果存在大量的数据,可能会超出自增长的取值范围

  • 在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成,有单点故障的风险

  • 很难处理分布式存储的数据表,尤其是需要合并表的情况下

  • 安全性低,因为是有规律的,容易被非法获取数据

UUID

UUID全称是Universally Unique Identifier,即通用唯一识别码。

UUID是由一组32位数的16进制数字所构成,所以理论上UUID的总数为16^32=2^128,约等于3.4*10^38

UUID的标准形式包括32个16进制数字,以连字号分为5段,例如:1FA504B0-2F82-21D3-0A0C-1205E72C3301UUID经由一定的算法机器生成,为了保证UUID的唯一性,规范定义了包括网卡MAC地址、时间戳、名字空间、随机数或伪随机数、时序等元素,以及从这些元素生成UUID的算法。

优缺点

优点

  • 本地生成ID,不需要进行远程调用,时延低,性能好

缺点

  • UUID过长,16字节共128位,通常以36长度的字符串标识,很多场景不适用,比如用UUID做数据库索引字段

  • 没有排序,无法保证趋势递增

  • 不可读

UUID的变种

UUID to Int64

为了解决UUID不可读的问题,可以使用UUID to Int64的方法。

/// <summary>
/// 根据GUID获取唯一数字序列
/// </summary>
public static long GuidToInt64()
{
byte[] bytes = Guid.NewGuid().ToByteArray();
return BitConverter.ToInt64(bytes, 0);
}  

Comb(combined guid/timestamp)

为了解决UUID无序的问题,NHibernate在其主键生成方法中提供了Comb算法。保留GUID的10个字节,用另6个字节表示GUID生成的时间(DateTime)。

/// <summary>
/// Generate a new <see cref="Guid"/> using the comb algorithm.
/// </summary>
private Guid GenerateComb()
{
byte[] guidArray = Guid.NewGuid().ToByteArray(); DateTime baseDate = new DateTime(1900, 1, 1);
DateTime now = DateTime.Now; // Get the days and milliseconds which will be used to build
//the byte string
TimeSpan days = new TimeSpan(now.Ticks - baseDate.Ticks);
TimeSpan msecs = now.TimeOfDay; // Convert to a byte array
// Note that SQL Server is accurate to 1/300th of a
// millisecond so we divide by 3.333333
byte[] daysArray = BitConverter.GetBytes(days.Days);
byte[] msecsArray = BitConverter.GetBytes((long)
(msecs.TotalMilliseconds / 3.333333)); // Reverse the bytes to match SQL Servers ordering
Array.Reverse(daysArray);
Array.Reverse(msecsArray); // Copy the bytes into the guid
Array.Copy(daysArray, daysArray.Length - 2, guidArray,
guidArray.Length - 6, 2);
Array.Copy(msecsArray, msecsArray.Length - 4, guidArray,
guidArray.Length - 4, 4); return new Guid(guidArray);
}

  

Redis生成分布式ID

Redis是单线程的,并且提供了原子操作INCRINCRBY,也可以用来生成高性能的分布式ID

如果使用Redis集群来生成分布式ID的话,需要做同数据库ID相似的配置:起点与步长。

另外虽然INCRINCRBY是原子性的,但是如果对获取Id进行了封装,那么要考虑对封装的方法进行线程安全性的考量。

优点

  • 不依赖数据库,灵活方便,且性能优于数据库

  • 数字ID天然排序,对分页或者需要排序的结果很有帮助

缺点

  • 需要引入Redis

  • 需要编码和配置的工作量比较大

Twitter的snowflake算法

snowflakeTwitter的开源分布式ID生成算法,其生成结构如下(每部分用-分开):

0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

第一位为未使用,接下来的41位为毫秒级时间(41位的长度可以使用69年),然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点) ,最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)

一共加起来刚好64位,为一个Long型。(转换成字符串后长度最多19)

snowflake生成的ID整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenterworkerId作区分),并且效率较高。经测试snowflake每秒能够产生26万个ID

官网:https://github.com/twitter-archive/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按照时间在单机上是递增的

缺点

  • 在单机上是递增的,但是在分布式环境中,每台机器上的时钟不可能完全同步,可能会出现不是全局递增的情况

原文地址:https://wangjinlong.xyz/2018/09/17/distributed-system-id-generation/

分布式系统ID生成方案汇总的更多相关文章

  1. 分布式ID生成方案汇总

    1.目标 1.1.全局唯一 不能出现重复的ID,全局唯一是最基本的要求. 1.2.趋势有序 业务上分页查询需求,排序需求,如果ID直接有序,则不必建立更多的索引,增加查询条件. 而且Mysql Inn ...

  2. 分布式系统唯一ID生成方案汇总【转】

    转自:http://www.cnblogs.com/haoxinyue/p/5208136.html 系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,也常常为这个问题而纠结.生成ID的方法有很 ...

  3. 分布式系统唯一ID生成方案汇总

    系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,也常常为这个问题而纠结.生成ID的方法有很多,适应不同的场景.需求以及性能要求.所以有些比较复杂的系统会有多个ID生成的策略.下面就介绍一些常见 ...

  4. [转]分布式系统唯一ID生成方案汇总

    系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,也常常为这个问题而纠结.生成ID的方法有很多,适应不同的场景.需求以及性能要求.所以有些比较复杂的系统会有多个ID生成的策略.下面就介绍一些常见 ...

  5. 分布式系统唯一ID生成方案汇总 转

    系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,也常常为这个问题而纠结.生成ID的方法有很多,适应不同的场景.需求以及性能要求.所以有些比较复杂的系统会有多个ID生成的策略.下面就介绍一些常见 ...

  6. 分布式系统ID生成方案

    自增ID 不错,可以限度抑制ID的大小.但需要有一个中心化的节点作为解决原子性问题.可以选用Redis,MySQL,Zookeeper.成本有点高. UUID 分布式,而且唯一!缺点是生产的ID太长. ...

  7. 分布式系统唯一ID生成方案

    分布式系统唯一ID生成方案汇总 数据库自增主键 最常见的方式.利用数据库,全数据库唯一. 优点: 1)简单,代码方便,性能可以接受. 2)数字ID天然排序,对分页或者需要排序的结果很有帮助. 缺点: ...

  8. 分布式唯一ID生成方案是什么样的?(转)

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

  9. 基于数据库构建分布式的ID生成方案

    在分布式系统中,生成全局唯一ID,有很多种方案,但是在这多种方案中,每种方案都有有缺点,下面我们之针对通过常用数据库来生成分布式ID的方案,其它方法会在其它文中讨论: 1,RDBMS生成ID: 这里我 ...

随机推荐

  1. PAT天梯赛L1-020 帅到没朋友

    题目链接:点击打开链接 当芸芸众生忙着在朋友圈中发照片的时候,总有一些人因为太帅而没有朋友.本题就要求你找出那些帅到没有朋友的人. 输入格式: 输入第一行给出一个正整数N(<=100),是已知朋 ...

  2. UVa 11292 勇者斗恶龙(The Dragon of Loowater)

    首先先看一下这道题的英文原版... 好吧,没看懂... 大体意思就是: 有一条n个头的恶龙,现在有m个骑士可以雇佣去杀死他,一个能力值为x的勇士可以砍掉直径不超过x的头,而且需要支付x个金币.如何雇佣 ...

  3. 去除掉svn目录

    使用svn提交时,造成有些文件锁住不能使用,百度得到下面相似问题以及解决方法: /*********************************************************** ...

  4. mysql 常用函数。。

    FIND_IN_SET(str,strlist) ,strlist 是 一个 由 逗号 分割的字符串,要注意 strlist 不能有逗号.. 它 等于  where str in (1,2,3***) ...

  5. Codeforces - 722C 区间合并

    要求断裂的数列之和的最大值,只需在断裂处的下标修改为一个足够负无穷大的值就可以用线段树维护 这道题数据还是弱了点,如果n和ai均取最大可能我这个程序早就爆ll了(4e4的时候炸了),毕竟本来想着用GC ...

  6. python中 列表 字典 元组的了解

    #######列表######1.列表的特性 server = [['http'],['ssh'],['ftp']] server1 = [['mysql'],['firewalld']]  连接  ...

  7. eclipse中找不到base64包的解决方法

    eclipse中找不到base64包的解决方法 2017年08月26日 11:05:26 yzp_leo 阅读数:634 标签: javaeclipsebase64更多 个人分类: 日记   ecli ...

  8. 安装eclipse for ee

    去官网下载最新版本版本的linux版本的eclipse for ee,下载到Downloads文件夹. 解压文件夹 sudo tar -zxvf eclipse-jee-2018-09-linux-g ...

  9. Robot Framework 的安装和配置

    Robot Framework 的安装和配置 在使用 RF(Rebot framework)的时候需要 Python 或 Jython 环境,具体可根据自己的需求来确定.本文以在有 Python 的环 ...

  10. shell 终端字符颜色

    终端的字符颜色是用转义序列控制的,是文本模式下的系统显示功能,和具体的语言无关,shell,python,perl等均可以调用. 转义序列是以 ESC 开头,可以用 \033 完成相同的工作(ESC ...