Java 生成唯一性标识解决方案与重复概率分析

来源:互联网 发布:淘宝直播怎么做的 编辑:程序博客网 时间:2024/06/07 03:51

应用背景

在分布式Session存储方案中,提到了分布式环境下,Session的四种存储方案,其中Session数据集中存储的方案是将Session集中存储在单独的服务器或集群上,也是比较常用的方式之一,但是这里面涉及到应用服务器每次请求Session服务器都必须携带一个唯一性标识,用以获取存储在Session服务器上的Session数据(想一想传统的Session实现方式),而如何生成这个唯一性标识,便是本文讨论的重点。


分析与实现

实现方案有多种,这里简单介绍一下下面的解决方案:

public static long createSessionID() {long t1 = 0x7FFFFFFF & System.currentTimeMillis();return t1 << 32 | Math.abs(random.nextInt());}
上面的代码是实际应用中的代码,具有可行性,接下来就简单分析其实现原理,并讨论其生成的标识重合概率。


实现原理与重合概率分析

这里涉及到位运算与移位运算,先简单解释下这几个运算的含义(数值计算均先转换成二进制再进行计算)。

  1. 与运算符    ( &):两个操作数中位都为1,结果才为1,否则结果为0,例:1001 & 1000 = 1000;
  2. 或运算符    ( | ):两个位只要有一个为1,那么结果就是1,否则就为0,例:1001 & 1000 = 1001;
  3. 左移运算符(<<):对二进制向左移位,后面补0,即num << 1,相当于num * (2 ^ 1),num << 32,则相当于num * (2 ^ 32),例:1001 << 2 = 10010。
注:以上计算均为二进制,(1001)2 = 9,(1000)2 = 8(哦,用这个编辑器不会打下标,括号后面的2是下标)。

在上面的代码中,0x7FFFFFFF是16进制,转换为10进制等于2,147,483,647,转换成2进制等于0111 1111 1111 1111 1111 1111 1111 1111,此数即Java中int类型的最大值。而random.nextInt()的生成随机数的范围是-2 ^ 31 ~ 2 ^ 31 - 1,通过Math.abs()获取绝对值后,其生成随机数的范围变成了0 ~ 2 ^ 31,即0 ~ 2,147,483,648,同时System.currentTimeMillis()是获取当前系统时间以毫秒为单位,即是说只要不在同一毫秒的时间内生成ID,t1的结果都将发生变化,而后一步的计算将依据t1的结果与随机数,即两个不确定的数的值---系统时间与0 ~ 2 ^ 31内的随机数,那么其重复概率会是多少呢?

首先假设在相同的毫秒时间内,即t1是相同的,那么t1 << 32的结果也是相同的,此时需要依据生成的随机数来保证最后生成的标识ID的差异。所以理论上,在同一毫秒时间内,需要同时至少有2 ^ 31 + 2次(从0开始)生成,才能保证一定有重复生成的ID。但是我们知道不管是C/C++还是JAVA,其随机数的生成其实是一种伪随机数,既是说我们可以通过模拟来计算其生成重复的概率,代码如下:
public static void main(String[] args) {final int SIZE = 10000;// 随机数范围final int COUNT = 10; // 实验次数int repeatCount[] = new int[COUNT];// 重复次数// 重复10次,计算其平均数for (int n = 0; n < COUNT; n ++) {repeatCount[n] = 0;Map<Integer, Integer> map = new LinkedHashMap<Integer, Integer>();// 初始化数据for (int i = 0; i < SIZE; i ++) {map.put(i, 0);}// 生成随机数并将次数存储for (int i = 0; i < SIZE; i ++) {int ranInt = random.nextInt(SIZE);map.put(ranInt, map.get(ranInt) + 1);}Iterator<Integer> keySet = map.keySet().iterator();// 遍历容器计算其重复次数while (keySet.hasNext()) {Integer key = keySet.next();if (map.get(key) > 1) {// 重复次数为大于1的次数减去1的和repeatCount[n] = repeatCount[n] + (map.get(key) - 1);}// System.out.println(key + " : " + map.get(key));}System.out.println("Repeat count:" + repeatCount[n]);}double sum = 0;for (int n = 0; n < COUNT; n ++) {sum += repeatCount[n];}System.out.println("Average:" + sum / COUNT);System.out.println("Repeat probability:" + Math.round(sum / COUNT / SIZE * 1000) / 1000.0);}
上面的代码,SIZE表示随机数范围,这里并没有直接使用2,147,483,648(会卡死机的),但是同样可以求得其概率,我们将通过改变SIZE的值来看看概率的变化。当SIZE=10000时,结果如下:
Repeat count:3676Repeat count:3645Repeat count:3694Repeat count:3674Repeat count:3709Repeat count:3709Repeat count:3653Repeat count:3720Repeat count:3677Repeat count:3663Average:3682.0Repeat probability:0.368
经过对10次重复次数求出平均次数约为3682.0次,其生成重复数的概率大约为0.368,改变SIEZ值,使SIZE=100000,结果如下:
Repeat count:36902Repeat count:36715Repeat count:36955Repeat count:36753Repeat count:36711Repeat count:36798Repeat count:36798Repeat count:36788Repeat count:36692Repeat count:36745Average:36785.7Repeat probability:0.368
WHAT?!!!可以看出,尽管将随机数的范围扩大了十倍,但是其重复概率居然惊人的相似,都是约等于0.368(这里做了保留小数后3位数的操作),有点惊讶,那在调整一次SIZE的大小,使SIZE=1000000,运行结果如下:

Repeat count:367806Repeat count:367866Repeat count:367486Repeat count:368017Repeat count:367709Repeat count:367480Repeat count:367600Repeat count:368049Repeat count:367663Repeat count:368449Average:367812.5Repeat probability:0.368
重复概率居然还是约等于0.368(再次重申,这里的0.368是保留3位小数后的结果,实际情况经过测试发现还是存在细微的偏差,但是保留3位小数的结果相等),好吧,那我们对于上面的在同一毫秒时间内生成重复标识符的概率假设为36.8%,即在同一毫秒的时间内如果有1000次生成,那么极大的可能会有368次左右的标识符重复。在实际运用中,我们假设在1秒内,各个毫秒的请求分布均匀,那么当QPS为1000 * 1000 = 1,000,000时,每毫秒的时间内既有约368次的标识符生成重复,当QPS为1,000,000 / 368 约为2717.39次时每毫秒内会有1次标识符生成重复,当然了这里的QPS指的是同时登陆成功的访问请求,因为只有这个时候才需要生成标识符ID。

那么对于第二种情况,即不同毫秒时间会有多少概率生成相同的标识符呢?
我们先将随机数固定,来测试相邻的一分钟内以毫秒为单位生成的标识情况,代码如下:
public static void main(String[] args) {// System.out.println(System.currentTimeMillis());final int SIZE = 1000 * 60 * 60;// 实现次数final int COUNT = 100;// 实现次数int repeatCount[] = new int[COUNT];// 重复次数long timeArr[] = new long[SIZE];long initTime = System.currentTimeMillis();// 固定随机数long fixRandom = Math.abs(random.nextInt());for (int n = 0; n < COUNT; n ++) {repeatCount[n] = 0;long initTimeTemp = initTime;Map<Long, Integer> map = new LinkedHashMap<Long, Integer>();for (int i = 0; i < SIZE; i ++) {timeArr[i] = initTimeTemp + i;long t1 = 0x7FFFFFFF & timeArr[i];long id = t1 << 32 | fixRandom;map.put(id, !map.containsKey(id) ? 1 : map.get(id) + 1);}Iterator<Long> keySet = map.keySet().iterator();// 遍历容器计算其重复次数while (keySet.hasNext()) {Long key = keySet.next();if (map.get(key) > 1) {// 重复次数为大于1的次数减去1的和repeatCount[n] = repeatCount[n] + (map.get(key) - 1);}// System.out.println(key + " : " + map.get(key));}// System.out.println("Repeat count:" + repeatCount[n]);}double sum = 0;for (int n = 0; n < COUNT; n ++) {sum += repeatCount[n];}System.out.println("Average:" + sum / COUNT);System.out.println("Repeat probability:" + sum / COUNT / SIZE);}
上面的代码测试了相邻一分钟内且固定随机数的生成标识结果,并重复了100次同样时间内生成结果,其最后运行结果如下:
Average:0.0Repeat probability:0.0
哦,跑了几分钟最后得到这种结果,这样就没办法从第一次的概率乘以这次的概率来计算其概率结果了,但是我们可以发现的是,尽管固定了随机数的值,但是相邻的时间内其生成相同标识ID的概率几乎为0。

总结

综上所述,对于时间变量与随机数变量,只要保证时间变量不同,即在不同毫秒时间内,生成标识重复的概率几乎为0,而随机数的作用则保证了即使在同一毫秒时间内,其生成标识重复的概率大约为36.8%,即其在QPS大约为2717 - 1 = 2716次的时候,能够几乎不生成重复的标识(2716 / 1000 * 0.368 < 1)。











0 0
原创粉丝点击