分布式系统ID生成方案汇总

在分布式系统中,需要对大量的数据、消息、请求等进行唯一的标识,例如分布式数据库的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/

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏祝威廉

StreamingPro添加Scala script 模块支持

SQL 在解析字符串方面,能力还是有限,因为支持的算子譬如substring,split等有限,且不具备复杂的流程表达能力。我们内部有个通过JSON描述的DSL...

6930
来自专栏微服务生态

淘宝Tedis组件究竟是个啥(一)

淘宝的Tedis组件究竟是个啥呢?可能有一些朋友没有听过这个名字,有一些朋友会经常使用,那么今天我就来和大家深入分析一下,它的使用和原理。

11820
来自专栏冰霜之地

如何设计并实现一个线程安全的 Map ?(下篇)

在上篇中,我们已经讨论过如何去实现一个 Map 了,并且也讨论了诸多优化点。在下篇中,我们将继续讨论如何实现一个线程安全的 Map。说到线程安全,需要从概念开始...

54670
来自专栏java一日一条

2017年高频率的互联网校园招聘面试题

参加了2017年校招,面试了阿里、百度、腾讯、滴滴、美团、网易、去哪儿等公司,个人是客户端 Android 方向,总结了面试过程中频率出现较高的题目,希望对大家...

18120
来自专栏编程

JAVA面试常考系列八

题目一 JDBC是什么? JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为...

379110
来自专栏Android机器圈

Java设计模式总汇二(小白也要飞)

PS:上一篇我介绍了适配器设计模式、单例设计模式、静态代理设计模式、简单工厂设计模式,如果没有看过第一篇的小火鸡可以点这个看看http://www.cnblog...

35290
来自专栏linux驱动个人学习

Linux内存描述之内存页面page--Linux内存管理(四)

分页单元可以实现把线性地址转换为物理地址, 为了效率起见, 线性地址被分为固定长度为单位的组, 称为”页”, 页内部的线性地址被映射到连续的物理地址. 这样内核...

47310
来自专栏梧雨北辰的开发录

iOS面试知识总结之基本概念总结

凡经历过iOS面试的我们总会发觉,即使实际开发中做过许多项目,也难免为一个普通的面试题受挫。这也许不是因为我们技术不过关,而是因为在平时我们忽略了怎样将用到的知...

41870
来自专栏Danny的专栏

System.Data.SqlClient.SqlException: 将截断字符串或二进制数据

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/huyuyang6688/article/...

40840
来自专栏运维一切

ceph对象存储折腾记 原

###前言 一直想弄对象存储,以前弄过一次,不是很理解region是个什么东西,后来时间和工作上的原因没有再折腾,这两天闲了下来,再次折腾了一次。我是参考的ce...

17210

扫码关注云+社区

领取腾讯云代金券