一、 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生成策略