【强推】你可能没听过的,分布式唯一随机数ID生成器,值得收藏

一、前言

作为一个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更多玩法。

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

长乐smile

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值