在日常的项目开发中,我们经常会遇到需要生成唯一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. 高并发&性能优化(一)------总体介绍

    [开篇词] 本文主要通过一些经典的高并发场景,以及一些基本的运维工具来讲述一些关于高并发以及性能优化相关的内容,主要包括性能瓶颈的定位,性能调优的思路和技巧等. [性能的衡量指标] ?什么是性能 性能 ...

  2. 数据操纵DML

    数据操纵DML 1. 在dept表中插入两行数据 (1)50,'IT','SHENYANG';(2)60,'HR','DALIAN'; 2. 设置保存点beforeup 3. 更新dept表,将60号 ...

  3. vue组件获取和vue-cli的基本了解

    Vue获取组件的一些方法 this.$refs.xxx 给标签绑定ref属性,获取的是当前DOM对象 给组件绑定ref属性,获取的是组件实例对象 this.$parent 获取当前组件的父组件,为一个 ...

  4. Android开发之数组类的面试题目,android工程师java程序员必备

    1,定义一个长度为5的数组 int [] arr=new int[5]; 2,写出静态初始化一个数组的方法 int [] arr={1,2,3,4}; 3,写出可变参数的使用规则    1,只能做为方 ...

  5. C III

    http://cossacksworld.ucoz.co.uk/load/c_iii_files/79 http://cossacksworld.ucoz.co.uk/load/c_iii_files ...

  6. 【python】迭代器与生成器到底是什么?看完你就知道

    迭代器跟生成器,与上篇文章讲的装饰器一样,都是属于我的一个老大难问题. 通常就是遇到的时候就去搜一下,结果在一大坨各种介绍博客中看了看,回头又忘记了. 你是不是也是这样呢? 俗话说:好记性不如烂笔头, ...

  7. Centos7.6系统下docker的安装

    一.环境说明 系统:CentOS7.6 软件:Docker19.03 二.Docker的安装 2.1.在线安装 (1) 设置仓库,安装所需的软件包. yum-utils 提供了 yum-config- ...

  8. PyTorch ResNet 使用与源码解析

    本章代码:https://github.com/zhangxiann/PyTorch_Practice/blob/master/lesson8/resnet_inference.py 这篇文章首先会简 ...

  9. SqlAnalyzer1.01 源码

    源码下载:https://files.cnblogs.com/files/heyang78/SqlAnalyzer-20200529-2.rar 现有功能:不带函数允许嵌套的select ...fro ...

  10. Ubuntu更换国内源--解决终端下载速度慢的问题

    目前我已知的更改国内源的方法基本上就两种,第一种,把/etc/apt/sources.list文件里的源更换一下,改成阿里云或者其它源.第二种,更换在设置中software&updates(软 ...