在日常的项目开发中,我们经常会遇到需要生成唯一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); Mapmap = new ConcurrentHashMap (); 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 map1 = new ConcurrentHashMap (); 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生成的方式的考量就需要更加全面,如并发量、持久化、获取方式等,都需要具体问题具体分析,这里不再深入探讨,希望本文对大家能有所帮助,其中如有不足与不正确的地方还望指出与海涵。
关注微信公众号,查看更多技术文章。