聊胜于无
Java之唯一无序数生成

Wayne Huang

2011年10月




1

在许多项目中ID号是一个永恒的主题。在绝大多数情况下,这个唯一ID产生相对比较容易,毕竟现在众多的项目都是基于数据库的,只要把数据库的主键拿出来作为ID就可以确保ID在整个系统中的唯一性了。但也存在一些特殊情况。比如,一个在线订单生成。考虑到订单的特殊性,有时候会被要求订单号要没有规律不连续。但是我们也知道订单号是必须具有唯一性的。然而,一般数据库的主键都是采用自增数作为主键的。因此,这里如果再用主键作为订单号就会存在问题。当然,除了这个情况外,还有许多其他情况。诸如,需要自动的生成一个随机的用户ID等。

考虑到以上的背景,我这篇将继上一篇《聊胜于无 Java之Caesar与Vigenere实现》做一个唯一的伪随机数生成。作为对上一篇看上去挺简陋甚至不怎么安全的加密算法做一个具有现实意义的应用实现。话说,这两个坑爹的算法,如果加以合理的组合运用还是挺有用处的。其实大多数的安全问题,都是因为人为因素造成的和算法本身问题没太大的关系。



2

众所周知,在计算机中,用算法实现的真实随机数是不可能存在的。当然,如果要从哲学角度考虑的话,那就是偶然和必然的问题了。就拿抛硬币来说,你可以把抛掷的结果看成是随机的,但是根据力学分析去分析每一个抛掷过程,你发现抛硬币的结果又不是随机的。



2.1

扯那么多废话,再回到我们的主题上。如果要生成唯一随机ID,众所周知的办法是用GUID来实现。但是,如果你了解GUID的生成,就会知道GUID的生成只是将发生相同GUID的几率降到一定数值之下而已,并不能百分百保证不重复,虽然这个重复的概率比中彩票都低。而且,诸如用户ID这种情况,要用户每次登陆都输入那么长一串GUID显然是不现实的。

那么,我们来看看第二种严格符合题意的做法。现在,我们被要求的有两点:第一是随机的一个数,第二是唯一的。那么,生成一个随机数还是比较简单的,那就用Random做就OK了。接下来要满足唯一性,那也不难,把每次生成结果保存起来,之后每生成一个随机ID,就到历史里去查一下有没有重复,如果有重复就重新生成一个,周而复始直到满足唯一性。乍看之下这个算法真是"天衣无缝"啊~,话说很多人都一开始会觉得这个算法的确不错。但是,仔细想想却存在问题。如果是几个这样的ID问题不是很大,但如果要生成数量众多的ID呢?"理想情况"下,每个ID都要到历史里去遍历一遍所有的ID。但是,对于限定ID位数的情况下,随着已生成ID的数量增加,发生冲突的几率也会提高。这样的话,将直接导致算法不可用。



2.2

根据上面的分析,是不是觉得实现这样一个"小问题",还需要很多的精力去思考呢?很多时候,我们看似简单的问题,真正实现起来却会遇到很多问题。如果作为一个普通的coder,那问题不是很严重。但是,如果作为一个leader,不能正确的预见到所要面临的问题,那就没有成为leader的资格。

那我们现在透过现象看本质。我们现在最容易拿到具有唯一性的数据是什么?是数据的ID,但这是一个有序数列。那么我们接下来要做什么?就是让这个有序数列看不出有序,让人产生迷惑感。那什么能让人对"有意义"的东西产生迷惑呢?对,就是加密算法。加密算法的本质就是将一组有意义的数据通过处理变成无意义的。这样看来,算法的思路就很简单了。就是把ID经过某个加密算法处理后即可。



3

根据刚才的想法,那么我们首先想到的应该是MD5这类算法,但是那个生成的签名是固定的128bit数据,转换成可输入的字符,有32个字符。这样的话,用GUID效果或许会更好。那么,其次我们应该想到的是AES算法,他的雪崩效应令人映像深刻。但是AES每个加密块都是固定长度,而且加密之后还要字符串化,最主要的是AES加密算法实现也比较复杂。所以,综合以上考虑,是否突然发现或许可以用Caesar实现呢?



3.1

那么,根据思路,我们实现了如下的代码。



import java.util.Random;

public class RandomId {
    private Random random;
    private String table;
    public RandomId() {
        random = new Random();
        table = "0123456789";
    }
    public String randomId(int id) {
        String ret = null,
            num = String.format("%05d", id);
        int key = random.nextInt(10), 
            seed = random.nextInt(100);
        Caesar caesar = new Caesar(table, seed);
        num = caesar.encode(key, num);
        ret = num 
            + String.format("%01d", key) 
            + String.format("%02d", seed);
        
        return ret;
    }
    public static void main(String[] args) {
        RandomId r = new RandomId();
        for (int i = 0; i < 30; i += 1) {
            System.out.println(r.randomId(i));
        }
    }
}



这段代码是基于上一篇《聊胜于无 Java之Caesar与Vigenere实现》的代码实现的。接受一个有序的ID数字序列,输出一个至少8位的唯一随机的字符串。



3.2

通过执行代码,我们得到一组随机的由数字组成的ID字符串。由于生成的原理,所以不会因为生成ID的数量增加而造成算法性能下降。接下来我们看看,这个随机的序列真是唯一的么?从算法可知,根据这个随机序列,我们能够还原出原来的唯一ID。这也就证明,这个随机的序列是唯一。并且,在第三方不知道key和seed的长度与位置的情况下,以及算法实现的情况下,是很难还原出原本有序的ID的。而且,由于这类ID一般会保存在数据库中,同时一个有序ID能够对应多个无序的ID,所以,即使知道整个生成的细节,也很难根据有序ID伪造出一个合法的无序ID。