Java唯一ID生成策略

Java
358
0
0
2023-06-17

一、 Java 原生API提供UUID生成方法

说明:

 public final class UUID  extends  Object implements Serializable, Comparable<UUID>

一个表示不可变的通用唯一标识符(UUID)的类。UUID表示128位值。

这些全局标识符存在不同的变体。 该类的方法是用于操纵Leach-Salz变体,尽管构造函数允许创建UUID的任何变体(如下所述)。

变体2(Leach-Salz)UUID的布局如下:最重要的长度包括以下无符号字段:

xFFFFFFFF00000000 time_low
x00000000FFFF0000 time_mid
x000000000000F000 version
x0000000000000FFF time_hi 

最不重要的长度包括以下五个符号字段:

xC000000000000000 variant
x3FFF000000000000 clock_seq
x0000FFFFFFFFFFFF node 

变量字段包含一个标识UUID的布局的UUID 。上述的位布局仅用于有效UUID为2的变体值,其指示里奇- SALZ变体。

版本字段保存描述此类型的值UUID。UUID有四种不同的基本类型:基于时间,DCE安全性,基于名称和随机生成的UUID。 这些类型的版本值分别为1,2,3和4。

 public  static  UUID randomUUID()

静态工厂检索一个类型(伪随机生成)的UUID。  `UUID`是使用加密强 伪随机数 生成器生成的。

Example Code:

 import java.util.UUID;

public class Util {
    public static  void  main(String args[]) {
        UUID uuid = UUID.randomUUID();
         String  strUUID = uuid.toString();
        System.out.println(uuid + strUUID);
    }
} 
 //UUID字符串表示形式由此BNF描述: 

 UUID                   = <time_low> "-" <time_mid> "-"
                          <time_high_and_version> "-"
                          <variant_and_sequence> "-"
                          <node>
 time_low               =*<hexOctet>
 time_mid               =*<hexOctet>
 time_high_and_version  =*<hexOctet>
 variant_and_sequence   =*<hexOctet>
 node                   =*<hexOctet>
 hexOctet               = <hexDigit><hexDigit>
 hexDigit               =
       "" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
       | "a" | "b" | "c" | "d" | "e" | "f"
       | "A" | "B" | "C" | "D" | "E" | "F"

优点:

1、本地生成ID,不需要进行远程调用,时延低。

2、扩展性好,基本可以认为没有性能上限。

3、全球唯一。

4、在遇见 数据迁移 、系统数据合并或者数据库变更的情况下可以从容应对。

缺点:

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

2、UUID过长,往往用 字符串 表示(数据库主键基本用整型类型表示), 作为主键建立索引查询效率低 ,常见优化方案为转化两个uint64整数存储或者折半存储(折半后不能保证唯一性)

3、存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。

4、传输数据量大。

5、不可读。

二、UUID的变种

1、UUID to int64

Example Code :

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

2、解决UUID无序问题

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

Example Code :

 /// <summary> 
/// Generate a new <see cref="Guid"/> using the comb algorithm. 
/// </summary> 
 private  Guid GenerateComb()
{
    byte[] guidArray = Guid.NewGuid().ToByteArray();
 
    DateTime baseDate = new DateTime(, 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.333333    
    byte[] daysArray = BitConverter.GetBytes(days.Days);
    byte[] msecsArray = BitConverter.GetBytes((long)
      (msecs.TotalMilliseconds /.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 -, guidArray,
      guidArray.Length -, 2);
    Array.Copy(msecsArray, msecsArray.Length -, guidArray,
      guidArray.Length -, 4);
 
    return new Guid(guidArray);
} 

用上面的算法测试一下,得到如下的结果:作为比较,前面3个是使用COMB算法得出的结果,最后12个字符串是时间序(统一毫秒生成的3个UUID),过段时间如果再次生成,则12个字符串会比图示的要大。后面3个是直接生成的GUID。

如果想把时间序放在前面,可以生成后改变12个字符串的位置,也可以修改算法类的最后两个Array.Copy。

三、时间戳

说明:

直接取当前毫秒时间戳,用 整型 类型表示。

优点:

效率高,为 整型数据

缺点:

如果并发量超过1000,会生成重复ID,对于高并发的场景无法胜任

四、独立的ID生成服务

说明:

专门搭建一个系统用来给各个接入系统分配唯一ID,每个系统每次来请求的时候返回一段ID,系统拿到自己用,用完后,再来申请,再次分配下一区段的,以此类推。

优点:

性能效率没问题,区间分配,效率很高

缺点:

可靠性要求非常高,如果ID生成服务出现故障,那对其它所有系统来说都是灾难

五、SnowFlake算法(雪花算法)

说明:

这是 twitter 的一个id生成算法

Twitter-Snowflake算法产生的背景相当简单,为了满足Twitter每秒上万条消息的请求,每条消息都必须分配一条唯一的id,这些id还需要一些大致的顺序(方便客户端排序),并且在 分布式系统 中不同机器产生的id必须不同。

首先我们需要一个long类型的变量来保存这个生成的id,第一位固定为0,因为id都是正数嘛,还剩63位,用x位表示毫秒时间戳,用y位表示进程id,用z位表示同一个时间戳下的序列号,x+y+z=63。

原理图如下:

算法 解释:

1、第一部分,1位为标识位,不用。

2、第二部分,41位,用来记录当前时间与标记时间twepoch的毫秒数的差值,41位的时间截,可以使用69年,T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69

3、第三部分,10位,用来记录当前节点的信息,支持2的10台机器

4、第四部分,12位,用来支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号

Example Code :

 /**
 * Twitter_Snowflake<br>
 * SnowFlake的结构如下(每部分用-分开):<br>
 * - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分)
 */
public class SnowflakeIdWorker {
 
    /** 开始时间截 (-01-01) */
    private final long twepoch =L;
 
    /** 机器id所占的位数 */
    private final long workerIdBits =L;
 
    /** 数据标识id所占的位数 */
    private final long datacenterIdBits =L;
 
    /** 支持的最大机器id,结果是 (这个移位算法可以很快的计算出几位 二进制 数所能表示的最大十进制数) */
    private final long maxWorkerId = -L ^ (-1L << workerIdBits);
 
    /** 支持的最大数据标识id,结果是 */
    private final long maxDatacenterId = -L ^ (-1L << datacenterIdBits);
 
    /** 序列在id中占的位数 */
    private final long sequenceBits =L;
 
    /** 机器ID向左移位 */
    private final long workerIdShift = sequenceBits;
 
    /** 数据标识id向左移位(12+5) */
    private final long datacenterIdShift = sequenceBits + workerIdBits;
 
    /** 时间截向左移位(5+5+12) */
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
 
    /** 生成序列的掩码,这里为 (0b111111111111=0xfff=4095) */
    private final long sequenceMask = -L ^ (-1L << sequenceBits);
 
    /** 工作机器ID(~31) */
    private long workerId;
 
    /** 数据中心ID(~31) */
    private long datacenterId;
 
    /** 毫秒内序列(~4095) */
    private long sequence =L;
 
    /** 上次生成ID的时间截 */
    private long lasttimestamp = -L;
 
    /**
     * 构造函数
     * @param workerId 工作ID (~31)
     * @param datacenterId 数据中心ID (~31)
     */
    public SnowflakeIdWorker(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId <) {
            throw new IllegalArgument Exception (String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId <) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
 
    /**
     * 获得下一个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 +) & sequenceMask;
            //毫秒内序列溢出
            if (sequence ==) {
                //阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //时间戳改变,毫秒内序列重置
        else {
            sequence =L;
        }
 
        //上次生成ID的时间截
        lastTimestamp = timestamp;
 
        //移位并通过或运算拼到一起组成位的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();
    }
} 

评价:

41的时间戳,存储当前时间戳与开始时间戳的差值,大概可以用69年,当然x,y,z可以自己根据情况分配,不是固定的。

此方法同样是本地生成,效率非常高,唯一性满足度很高,只需要以上一个类就行了,每个进程启动时,分配不同的processId即可。

六、数据库自增序列或字段

说明:

最常见的方式。利用数据库,全数据库唯一。

优点:

1)简单,代码方便,性能可以接受。

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

缺点:

1、不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理。

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

3、在性能达不到要求的情况下,比较难于扩展。

4、如果遇见多个系统需要合并或者涉及到数据迁移会相当痛苦。

5、分表分库的时候会有麻烦。

优化方案:

针对主库单点,如果有多个Master库,则每个Master库设置的起始数字不一样,步长一样,可以是Master的个数。比如:Master1 生成的是 1,4,7,10,Master2生成的是2,5,8,11 Master3生成的是 3,6,9,12。这样就可以有效生成集群中的唯一ID,也可以大大降低ID生成数据库操作的负载。

七、参考文章

【JAVA】系统唯一ID生成方案讨论

常见分布式全局唯一ID生成策略及算法的对比

全局唯一ID生成器(SnowFlakeId算法JAVA实现)

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

雪花算法-全局唯一ID生成器

更多内容查看:

分布式ID生成器

全局唯一ID生成策略

分布式系统里用户ID生成有什么好的方法和规则能满足“唯一、尽量短、不能直接看出规则”这几个条件?

十位用户唯一ID生成策略