在日常的项目开发中,我们经常会遇到需要生成唯一ID的业务场景,不同的业务对唯一ID的生成方式与要求都会不尽相同,一是生成方式多种多样,如UUID、雪花算法、数据库递增等;其次业务要求上也各有不同,有的只要保证唯一性即可,有的需要加上时间戳,有的要保证按顺序递增等。以下是我结合实际业务中的使用总结了几种唯一ID的生成方式,  要求就是在一般的应用场景下一方面能满足一定数据量级(千万级)的需要,另一方面使用方式相对简单轻量,不需要过多依赖第三方,同时从并发安全、冲突率、生成性能上做了一些简单的测试,大家可以略做参考

一、生成方式

1、UUID产生命令唯一标识,32位的字母数字组合

    /**
* 根据UUID产生命令唯一标识
*
* @throws InterruptedException
*/
public static String getUUIDHashCode() {
String orderSeq = UUID.randomUUID().toString();
return orderSeq; }

2、UUID取hash值+随机数,16位纯数字

    /**
* 根据UUID取hash值+随机数,产生命令唯一标识
*
* @throws InterruptedException
*/
public static String getOrderSeq() {
String orderSeq = Math.abs(UUID.randomUUID().toString().hashCode()) + "";
while (orderSeq.length() < 16) {
orderSeq = orderSeq + (int) (Math.random() * 10);
}
return orderSeq;
}

3、十六进制随机数 ,长度16的十六进制字符串

    //十六进制随机数  16位的十六进制字符串
public static String getRandomHexString() {
try {
StringBuffer result = new StringBuffer();
for (int i = 0; i < 16; i++) {
result.append(Integer.toHexString(new Random().nextInt(16)));
}
return result.toString().toUpperCase(); } catch (Exception e) {
// TODO: handle exception
e.printStackTrace(); }
return null; }

4、雪花算法

长度不超过20的纯数字,时间戳不同,长度会产生变化

    /** 开始时间截 */
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; /**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowFlake(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;
} /**
* 获得下一个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();
}

5、Redis Incr 命令

Redis Incr 命令会将 key 中储存的数字值增一。如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。

这里以jedis为例提供两种自增ID的生成方式

第一种方式直接通过Incr命令获取自增ID

JedisUtils.incr("inc-test1")

第二张方式获取带有时间戳的唯一编码,时间细度为分钟

    /**
* 基于Redis生成 时间+递增ID 的唯一编码
* @param key
* @return
*/
public static String getRedisTimeSeq(String key) { String time = DateUtils.parseDateToStr("yyyyMMddHHmm",new Date()); StringBuilder sBuilder = new StringBuilder(time); sBuilder.append(JedisUtils.incr(key+":"+time,120));//保证一分钟内KEY有效 return sBuilder.toString();
}

二、测试

下面我们从冲突率与时间性能上,对以上几种唯一ID的生成方式进行一个简单的测试,同时基于并发安全的考虑,测试分为单线程与多线程两种

    // ---------------测试---------------
public static void main(String[] args) throws InterruptedException { int length = 10000000;
SnowFlake snowFlake = new SnowFlake(1, 1); final CountDownLatch countDownLatch = new CountDownLatch(10); Map<String, String> map = new ConcurrentHashMap<String, String>();
long begin = System.currentTimeMillis();
for(int i=0;i<10;i++) {
Thread thread = new Thread(new Runnable() {
public void run() {
for (int i = 0; i != 1000000; ++i) { String str = String.valueOf(snowFlake.nextId()); // String str = StringUtils.getUUIDHashCode(); //根据UUID产生命令唯一标识 长度 32 字母数字组合
//
// String str = StringUtils.getOrderSeq();//根据UUID取hash值+随机数,产生命令唯一标识 长度 16位纯数字
//
// String str =StringUtils.getRandomHexString(); //长度16的16进制字符串 map.put(str, str); }
countDownLatch.countDown(); }
}); thread.start();
} countDownLatch.await(); System.out.println("冲突数为: " + (length - map.size()));
System.out.println("sync time: " + (System.currentTimeMillis() - begin)); Map<String, String> map1 = new ConcurrentHashMap<String, String>();
begin = System.currentTimeMillis();
for (int i = 0; i != length; ++i) { String str = String.valueOf(snowFlake.nextId()); // String str = StringUtils.getUUIDHashCode();//根据UUID产生命令唯一标识 // String str = StringUtils.getOrderSeq();//根据UUID取hash值+随机数,产生命令唯一标识 // String str =StringUtils.getRandomHexString(); map1.put(str, str); }
System.out.println("冲突数为: " + (length - map1.size()));
System.out.println("sync time: " + (System.currentTimeMillis() - begin));
}

测试结果如下:

生成方式 生成总数 并发 冲突数   耗时
UUID产生命令唯一标识
1000W 单线程 0  26166ms
UUID产生命令唯一标识
1000W 多线程 0  27915ms
根据UUID取hash值+随机数,产生命令唯一标识
1000W 单线程  0  25405ms
根据UUID取hash值+随机数,产生命令唯一标识
1000W  多线程  0  25023ms
十六位随机的十六进制字符串
1000W   单线程    0   25723ms
十六位随机的十六进制字符串

1000W    多线程   0  28094ms
雪花算法
1000W    单线程  0 10100ms
雪花算法
1000W   多线程  0 11713ms

针对 Redis Incr 命令进行了本地和局域网两种测试, 由于千万级数据耗时太长,数据量改为了百万级,结果如下:

生成方式 网络环境 生成总数 并发 冲突数 耗时
Redis Incr命令获取自增ID  本地  100W  单线程   0    72445ms
Redis Incr命令获取自增ID  本地  100W  多线程   0    47879ms 
Redis Incr命令获取自增ID 局域网  100W   单线程   0    71447ms
Redis Incr命令获取自增ID 局域网  100W   多线程   0    45888ms  

Redis Incr命令生成 时间+递增ID 的唯一编码

局域网  100W  单线程   0  236795ms

Redis Incr命令生成 时间+递增ID 的唯一编码

局域网  100W  多线程   0  39281ms

可以看到Redis相比前面一些轻量级的ID生成方式,生成效率上有明显差距,但在分布式环境下,且业务场景对全局唯一ID的生成样式有要求,   redis做为统一的ID生成器还是很有必要的。

由于测试受机器配置、网络带宽等条件影响,以上得出的结果只是一个简单的测试结果,证明这几种唯一ID生成方式具备一定的可用性,大家如果有兴趣可以进行更深入的测试与优化;

三、总结

其实在日常开发中唯一ID的的生成方式与整体服务的架构与复杂度是密切相关的,本文从并发安全、冲突率、性能等多个方面列举了几种唯一ID的生成方式,相对比较简单实用,但在更复杂的架构与业务场景下,对唯一ID生成的方式的考量就需要更加全面,如并发量、持久化、获取方式等,都需要具体问题具体分析,这里不再深入探讨,希望本文对大家能有所帮助,其中如有不足与不正确的地方还望指出与海涵。

关注微信公众号,查看更多技术文章。

 
 

实用向—总结一些唯一ID生成方式的更多相关文章

  1. 全局唯一ID的生成方式

    一.程序直接生成: 使用jdk中的concurrent包可以轻松实现唯一数字型ID的生成,且无需考虑单例.采用高效率的CAS无需考虑synchronized关键字 import java.util.c ...

  2. 分布式唯一ID生成服务

    SNService是一款基于分布式的唯一ID生成服务,主要用于提供大数量业务数据建立唯一ID的需要;服务提供最低10K/s的唯一ID请求处理.如果你部署服务的CPU资源达到4核的情况下那该服务最低可以 ...

  3. 如何在高并发分布式系统中生成全局唯一Id

    月整理出来,有兴趣的园友可以关注下我的博客. 分享原由,最近公司用到,并且在找最合适的方案,希望大家多参与讨论和提出新方案.我和我的小伙伴们也讨论了这个主题,我受益匪浅啊…… 博文示例: 1.     ...

  4. 高并发分布式系统中生成全局唯一Id汇总

    数据在分片时,典型的是分库分表,就有一个全局ID生成的问题.单纯的生成全局ID并不是什么难题,但是生成的ID通常要满足分片的一些要求:   1 不能有单点故障.   2 以时间为序,或者ID里包含时间 ...

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

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

  6. 全局唯一ID发号器的几个思路

    标识(ID / Identifier)是无处不在的,生成标识的主体是人,那么它就是一个命名过程,如果是计算机,那么它就是一个生成过程.如何保证分布式系统下,并行生成标识的唯一与标识的命名空间有着密不可 ...

  7. 人人网框架导入uidGenerator的ID生成方式

    人人网框架导入uidGenerator的ID生成方式 2019-03-11 LIUREN    SpringBoot2.0  uidGenerator  SpringBoot2.0  uidGener ...

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

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

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

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

随机推荐

  1. GitHub 热点速览 Vol.34:亚马逊、微软开源项目带你学硬核技术

    作者:HelloGitHub-小鱼干 摘要:站在巨人的肩膀上才能看得更远,本周上榜的 computervision-recipes 便是典型代表,这个由微软开源的计算机视觉最佳实践项目,多次上 Git ...

  2. MySQL添加外键报错 - referencing column 'xx' and referenced column 'xx' in foreign key constraint 'xx' are incompatible

    MySQL给两个表添加外键时,报错 翻译意思是:外键约束“xx”中的引用列“xx”和引用列“xx”不兼容 说明两个表关联的列数据类型不一致,比如:varchar 与 int,或者 int无符号 与 i ...

  3. mybatis逆向工程介绍

    项目的model一旦多了起来,复杂了起来,我们很自然的想到使用mybatis的逆向工程来生成相应的pojo和mapper,能节省很多精力. MyBatis Generator(MBG)是 MyBati ...

  4. 使用 Postman 做 API 自动化测试

    Postman 最基本的功能用来重放请求,并且配合良好的 response 格式化工具. 高级点的用法可以使用 Postman 生成各个语言的脚本,还可以抓包,认证,传输文件. 仅仅做到这些还不能够满 ...

  5. e3mall商城的归纳总结7之solr搭建和应用

    敬给读者的话 本文主要应用的技术是solr技术的搭建和应用,本文小编尽量写的更详细一些,让读者在不考虑项目的情况下也能正常完成solr的搭建,说完搭建之后,再说明运行solrj在项目中如何应用solr ...

  6. Illegal instruction报错 c/c++

    报错 # ./agent Illegal instruction# 原因 myLog(log4cplus::INFO_LOG_LEVEL, g_p_debugLog, "sendLog ip ...

  7. 点击穿透事件-----CSS新属性

    面试被问,一脸懵,被提示,还蒙,好丢脸的感觉....赶紧百度了解 .noclick{ pointer-events: none; /* 上层加上这句样式可以实现点击穿透 */ } 就是说重叠在一起的两 ...

  8. shader变体

    unity shader 变种(多重编译 multi_compile) https://www.jianshu.com/p/f34d896dde5d Unity Shader - Making mul ...

  9. mysql创建事务,分批次刷新大数据

    对于需要刷新的大数据量,当一次刷新数据量过大时,事务太大,会导致binLog文件太大,在不同的数据库同步时,可能遇到问题,先整理如下,分批次刷新数据 DELIMITER // # 设置//为结束符,否 ...

  10. Just an Old Puzzle(2019多校1007)

    Problem Description You are given a 4 × 4 grid, which consists of 15 number cells and an empty cell. ...