工作中经常会有生成唯一字符串的需求。通常最容易想到的是UUID。UUID的唯一性毋庸置疑,但是32位的长度也容易让人退避三舍。也曾经想过参考《短网址生成方案》来生成一串ID,但是试验了一下发现唯一性不太好。
最终采用的方案是时钟方案,简单来说就是用当前时间戳做唯一ID。
采用时间戳做ID,秒或毫秒都容易产生重复,换成纳秒在单节点上就没问题了。参考百度百科关于纳秒的描述就能清楚为什么纳秒级别的时间戳不会产生重复:
光在真空中一纳秒仅传播0.3米。个人电脑的微处理器执行一道指令(如将两数相加)约需2至4纳秒。
我们生成一条唯一ID所需的CPU指令绝不止一道,因此用纳秒作单机唯一ID是绰绰有余的。在测试中发现,即使是千分之一纳秒也足够我们在PC机上生成唯一ID了。
至于长度:对原始值做一次Base62处理,长度就能缩减到令人满意的程度。
不多废话,直接上代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
public static synchronized String gen() { StringBuilder builder = new StringBuilder(System.nanoTime() / 1000 + ""); if (SEQ.incrementAndGet() % 10 == 0) { SEQ.incrementAndGet(); } builder.append(FORMAT.format(SEQ.get())); if ((MAX_PAD_SIZE - 1) == SEQ.get()) { SEQ.set(1); } long v = Long.parseLong(builder.reverse().toString()); return Base62.encode(v); } |
这里用千分之一纳秒做基数(经测试,基数在10w分之一纳秒内都是安全的),再加上1~99的顺序号来生成唯一ID。最终可以保证在大于10纳秒(近似)的时间区间内不会产生重复值。
为了缩减长度,对字符串做了Base62
处理。在处理前又将纳秒数值做了一次翻转处理。不难想象,如果直接使用原始值来做Base62处理,因为时钟的特征,最终生成的值的前几位都是相同的。
来看一下这个程序生成的ID:
1 2 3 4 5 6 7 8 9 |
aSPog4cC d4t1xZdt g2tkZVqv jrinwXx5 m8ZIAKVr oUB5nzS5 rZa1gPAl uD12VZ3A 8dnItkTj |
八位的长度,唯一且整齐。下面是一个单元测试:
1 2 3 4 5 6 7 8 9 10 11 |
@Test public void gen() { int size = 10240; Set<String> set = new HashSet<>(); for (int i = 0; i < size; i++) { String code = ShortCode.gen(); //System.out.println(code); set.add(code); } Assert.assertEquals(size, set.size()); } |
这里只对10240的规模做了测试。因为唯一ID是基于时钟生成的,所以测试时整体规模的大小不影响ID的唯一性(和短链接方案不一样)。但是太小了也不行——顺序号会发挥作用。10240算是一个中庸的值,足够暴露问题,也不会有太多的冗余。
可以在github看到相关的代码 GitHub / UniqId
仍然需要强调一下:这个方案只能保证在(当前)单机上的唯一性,如果是集群范围内建议采用其他方案,或者加上一两位机器ID。
发表评论