一、前言
作为一个IT人,分布式唯一ID耳朵都听起老茧了,什么雪花算法 (SnowFlake ) 、UUID、Mongdb objectID、美团Leaf-segment、中央数据库序列自增等等。
今天,笔者要给大家介绍的是一款,几乎很少有人知道的随机数ID生成器。随机数ID通常可用于生成合同编号、订单编号、专利编号等一系列重要业务编号场景,防止被他人恶意扫描数据或猜测数据。
试想一下:假如合同编号都是有序生成的,那么恶意用户是不是可以通过编号猜测其他用户的合同编号,在系统某些接口没有做好越权防范时,会造成数据泄露。又比如,订单编号如果都是有序自增的,那么恶意用户是否可以在凌晨00:00新下一个订单,到23:59分再下一个单,将首尾两个订单编号相减,就得出平台当日订单数?竞对可以轻松拿到己方商业数据,这并不是一件好事。所以才有必要将重要业务ID,使用随机数ID来实现。
本文介绍的该随机数id生成器,采用PreshingRandom算法,该算法利用素数P(满足p≡3(mod 4))特性实现的,有关该算法的原理详解,我会在文末贴出来,比较痴迷算法的同学如果着急了解算法原理,请戳:http://preshing.com/20121224/how-to-generate-a-sequence-of-unique-random-integers/。java版本实现请戳:https://gist.github.com/steveash/fe241f54aa6d7fb73da9
如有无法理解的,请微信搜索:Java高手真经 与作者一起交流
二、随机数id生成器PrngIdGenerator算法原理
英文原版直通车:http://preshing.com/20121224/how-to-generate-a-sequence-of-unique-random-integers/
该随机数id生成器算法,利用素数(也叫质数)二次方剩余特性而设计的,主要利用素数的以下特性,当一个素数p满足p≡3(mod 4) ,即对4求余=3时,那么该素数一定满足以下条件:
(a).当2n<p时,(n*n)%p = ida 是唯一的;
(b).当p/2<n<p时,p-(n*n)%p=idb 也一定是唯一的,且ida和idb永不重复;
(c).ida和idb均小于p。
(备注:ida我们暂且叫:二次方剩余,idb我们暂且叫:二次方剩余差)
千万不要着急,这里看不懂没关系,接下来我们会通过原理演算,让大家更直观明了理解该算法原理。
三、PrngIdGenerator原理演算
首先,我们需要找到满足p≡3(mod 4)条件的素数,20以内的素数有:3,5,7,11,13,17,19,满足p≡3(mod 4)的素数有:7,11,19,原文中使用的是11。接下来,让我们一起演算并证明以上(a)(b)(c)三个条件都成立。
(a)当2n<p时,(n*n)%p = ida 一定是唯一的。
本例中,p=11,当n=0,1,2,3,4,5时,二次方剩余计算结果如下:
0² mod 11≡ 0
1² mod 11≡ 1
2² mod 11≡ 4
3² mod 11≡ 9
4² mod 11≡ 5
图形演算表示:
(b)当p/2<n<p时,p-(n*n)%p=idb 也一定是唯一的。
本例中,p=11,当n=6,7,8,9,10,二次方剩余差计算结果如下:
11 - (6² mod 11)≡ 8
11 - (7² mod 11)≡ 6
11-(8² mod 11)≡ 2
11-(9² mod 11)≡ 7
11 - (10² mod 11)≡ 10
图形演算表示:
经过上面二次方剩余,二次方剩余差的计算结果,我们巧妙发现,如果一个素数p满足p≡3(mod 4)时,
1)当2n<p时,(n*n)%p = ida 是唯一的;
2)当p/2<n<p时,p-(n*n)%p=idb 也一定是唯一的,且ida和idb永不重复;
(c)接下来我们再次验证p满足对4求余恒等于3的质数p=19,当2n<p,n=0,1,2……9时,二次方剩余计算结果如下:
0² mod 19≡ 0
1² mod 19≡ 1
2² mod 19≡ 4
3² mod 19≡ 9
4² mod 19≡ 16
5² mod 19≡ 6
6² mod 19≡ 17
7² mod 19≡ 11
8² mod 19≡ 7
9² mod 19≡ 5
--------------
当p/2<n<p,n=10,11,12……18时,二次方剩余差计算结果如下:
19-10² mod 19≡ 14
19-11² mod 19≡ 12
19-12² mod 19≡ 8
19-13² mod 19≡ 2
19-14² mod 19≡ 13
19-15² mod 19≡ 3
19-16² mod 19≡ 10
19-17² mod 19≡ 15
19-18² mod 19≡ 18
图形演算表示:
(图画的差,大家忍住别喷)
有兴趣的同学,可以再随机选取几个素数验证。
显然,13是个素数,但是13≠3(mod 4),所以当我们推算的时候,发现:
0² mod 13≡ 0
1² mod 13≡ 1
2² mod 13≡ 4
3² mod 13≡ 9
4² mod 13≡ 3
5² mod 13≡ 12
6² mod 13≡ 10
13-7² mod 13≡ 3 (和4² mod 13≡ 3 已经重复)
13-8² mod 13≡ 1 (和1² mod 13≡ 1 已经重复)
13-9² mod 13≡ 10 (和6² mod 13≡ 10 已经重复)
13-10² mod 13≡ 4 (和2² mod 13≡ 4 已经重复)
13-11² mod 13≡ 9 (和3² mod 13≡ 9已经重复)
13-12² mod 13≡ 12 (和5² mod 13≡ 12已经重复)
所以质数13是不满足用作随机数id生成算法。
综上,基于上面演算的结果,我们可利用素数的该特性,设计如下算法。
四、PrngIdGenerator算法实现-java 版
/*
* PRNG随机唯一不重复ID生成器
*/
public class PrngIdGenerator {
public static final int MAX_INT_PRIME = 2147483587;
public static final int DEFAULT_SEED = 0;
private int busiIntPrime;
private final Random rand;
private final long seed1; // these are longs so that it will implicitly widen and not overflow
private final long seed2;
public PrngIdGenerator() {
this(new Random(DEFAULT_SEED),MAX_INT_PRIME);
}
public PrngIdGenerator(long seed) {
this(new Random(seed),MAX_INT_PRIME);
}
public PrngIdGenerator(long seed, int prime) {
this(new Random(seed),prime);
}
private PrngIdGenerator(Random random, int prime) {
this.rand = random;
if(prime < 100000){
//业务限制:素数必须大于等于6位,此处仅校验对4除余等于3,没有校验是否素数
throw new IllegalArgumentException("prime is too small to adapt to business!");
}else if(!(PrimeUtil.checkIsPrime(prime) && (prime%4==3))){
throw new IllegalArgumentException("checkIsPrime result illegal prime num or p≡3(mod 4) not true!");
}else{
this.busiIntPrime = prime;
}
this.seed1 = modPrime(Math.abs(rand.nextInt()));
this.seed2 = modPrime(Math.abs(rand.nextInt()));
}
/**
* 根据index 反向查找id数值
* @return id 返回下一个id
*/
public int next(long index) {
if (index > Integer.MAX_VALUE) {
throw new IllegalStateException("Cannot generate more than " + Integer.MAX_VALUE + " random sequence nos");
}
int result = valueForIndex((int) index);
return result;
}
/**
* 根据index 反向查找id数值
* @return id 返回idNum个id
*/
public List<Integer> next(long index,int idNum) {
List<Integer> idList = new ArrayList<>();
for (int i=0;i<idNum;i++){
idList.add(next(index-i));
}
return idList;
}
/**
* @Desc 根据当前索引位置计算得出对应位置ID
* 利用二次平方求余,将ID分散更加随机
* @param index
* @return
*/
public int valueForIndex(int index) {
if (index < 0) {
throw new IllegalArgumentException("cannot pass negative index");
}
if (index >= busiIntPrime) {
throw new IllegalArgumentException("prng num is get over!");
}
int first = calculateResidue(modPrime(index + seed1));
int result = calculateResidue(modPrime(first + seed2));
return result;
}
/** 计算二次剩余 */
private int calculateResidue(int value) {
assert (value >= 0 && value < busiIntPrime) : value;
int residue = modPrime((long) value * (long) value);
int result = value <= (busiIntPrime / 2) ? residue : busiIntPrime - residue;
assert (result >= 0 && result < busiIntPrime) : value;
return result;
}
/** 除余素数 */
private int modPrime(long value) {
int i = (int) (value % busiIntPrime);
assert i >= 0 : i + " from " + value;
return i;
}
}
代码解析:
-
该随机数id生成器,将根据传入next(long index)方法中的index,生成唯一不重复的id。
-
分布式架构中,对于相同业务请求获取id,必须保证seed从初始化开始保持不变,可进而确保变种种子seed1和seed2保持不变.
-
seed1和seed2保持一致时,只有当index不同且唯一的情况下才可以生成唯一的id.
-
否则,当seed1和seed2保持不变,而index相同,获取到的id,一定是同一个.
-
综上:分布式架构中,对于同一业务获取id,必须保证seed1、seed2始终保持不变,而index必须保持且唯一,每次自增.
-
必须满足条件:index分布式唯一+多线程安全.
-
实际的应用中,我们可以按照 seed 确保业务分离.index确保分布式唯一.
-
当然,不同的业务seed可以相同,这样的结果就是,当index相同时,两个业务获取产生的id是一致的。
-
比如获取id,两个业务index=n都等于n,seed也相同,那么获取到的ID一定是一样的。
说明:目前默认生成的随机数id,因是用Integer实现,最大位数是10位,支持最大的随机数为 2147483587(算法最大生成个数,取决于Integer.MAX),如需更长随机数id,可将变量都定义为Long类型。或者,在生成的随机数前补位,即可支持更长位数和更大随机数id。比如得到的随机数是357,那就补定长位数:10000000357 就可以了。保证10位数以内的数,不会产生冲突。
五、mysql实现随机数id和自增id性能对比测试报告
有的同学可能会问,随机数id由于其分散性,会不会对数据库性能带来很大损耗,从而形成瓶颈?
带着这样的疑问,我们在本地单机环境进行了相关测试,每次测试,都是同时开启3个线程执行。
测试机器环境:
操作系统:Mac
CPU:2.7 GHz Intel Core i5
内存:8 GB 1867 MHz DDR3
mysql:5.6
并发线程数:3个
部分测试数据及测试结论如下:
MySQL随机数ID和自增ID插入性能对比测试-表1
2.随机数id生成耗时和数据表insert耗时性能对比-表2
结论:从上述测试验证来看,随机数ID生成耗时和插入mysql的性能损耗,几乎忽略不计。
六、最后
本文是通过mysql的自增序列不断递增,通过PrngIdGenerator随机数ID生成器,映射成对应的随机数ID。此处的mysql也可以换成其他类型的自增ID,比如redis、zookeeper等,都是可以的。mysql只是其中一种实现而已。
七、联系作者
如有不明白之处,可微信搜索:Java高手真经 联系作者,一起交流探讨,解锁随机数ID更多玩法。