abbshr / abbshr.github.io

人们往往接受流行,不是因为想要与众不同,而是因为害怕与众不同

Home Page:http://digitalpie.cf

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

如何高效产生不重复的随机数序列?

abbshr opened this issue · comments

你或许考虑过这个问题. 可是我曾用一个十分粗暴地办法"解决"了这个问题: 每生成一个序列检测是否有重复, 如果有,重新生成再进入这个检测, 否则存储在hash的一个位置.

这学期春季面试时, 当和面试官谈起Yinle.me在提取码生成过程中无碰撞4位随机数如何高效生成的问题, 我才发现不经意间错过了一个值的思考的问题. 我可以把这个问题描述为: "对于Event Driven的Web Server, 如果需要处理上千上万的并发连接, 怎么高效的为每个连接分配UUID (就是在这个server生存周期内无碰撞的数)?", 我脑海中立刻闪现第二个简单粗暴的办法: 预先生成一个bitmap, 记录所有可能的值, 再由主线程顺序分配, 当我说完之后自己都乐了... 如果一个记录占用1byte空间, 4位要1M左右空间, 这确实不算什么, 假如位数稍微增大, 空间占用就会指数级增长, 显然这个方法在给定字符空间大的时候将不再适用.

那该怎么办呢? 可见我在概率/随机领域知识的匮乏... 我向面试官请教有什么高明的办法, 他提示我用堆或者队列, 我又考虑半天, 仍没结论.

现在整理一下思绪, 先把上面的各种问题放在一边, 来想这样一个问题: 何为随机?

不翻书我们也能答上来几个关键点吧:

  • 不可预知
  • 均匀分布

即然这样那你有没有想过编程语言中的随机数是怎么来的? 仅凭几行代码就能模拟随机吗? 你应该知道在C语言里它叫伪随机数.

对于所学所用, 人们往往知其然而不知其所以然. 凭感性的认识, "伪随机数"好像就是随机的, 于是我们往往叫他随机数. 可是伪随机数

是使用一个确定性的算法计算出来的似乎是随机的数序,因此伪随机数实际上并不随机!

就是说, 伪随机数其实是按照某种计算方法得出, 服从某种分布规律的, 也是可以预测的. 这显然不符合对随机性的要求.

那么如何得到真正的随机数? 就是要保证过程的不确定性!

/dev/random来说, Linux内核会为其维护一个熵池, 专门存储从各处搜集来的噪声. 比如: 鼠标在某一时刻的位置, 驱动产生的中断信号, 键盘中断信号, 甚至是附近电磁场变化. 这就保证了从熵池中取出数据的不可预知性.

现在我们回到产生不重复的随机数这个问题上来, 要求产生的随机数不重复本身就是种悖论. 既然是随机过程, 就没办法控制其结果.

但我们可以hack一下这个问题: 如何产生一组无重复的id?

或许少量id我们可以选择bitmap或者循环检测. 现在考虑上亿id的分配问题, 要保证在高效利用空间/时间的前提下每个id不重复, 假设id空间是8位[a-z0-9], (36^8), 怎么做?

为了方便, 可能首选就是UUID/GUID, 它是一个128bit的16进制数字. 下面是一个来自stackoverflow的UUID生成算法:

function generateUUID(){
    var d = new Date().getTime();
    var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = (d + Math.random()*16)%16 | 0;
    d = Math.floor(d/16);
      return (c=='x' ? r : (r&0x7|0x8)).toString(16);
    });
    return uuid;
};

理论依据就是小概率事件, UUID的每个字符都是伪随机的, 这样小概率的乘积结果就几乎可以忽略不计了, 也就是几乎不会冲突.

与被陨石击中的概率比较的话,已知一个人每年被陨石击中的概率估计为170亿分之1,也就是说概率大约是0.00000000006 (6 x 10-11),等同于在一年内置立数十兆笔UUID并发生一次重复。换句话说,每秒产生10亿笔UUID,100年后只产生一次重复的概率是50%。如果地球上每个人都各有6亿笔UUID,发生一次重复的概率是50%。

产生重复UUID并造成错误的情况非常低,是故大可不必考虑此问题。

或许讨论来讨论去还是回到Hash算法抗碰撞能力设计中. Hash运算就有点类似寻找无重复串的过程, 目的是将不同规模, 内容不等的输入映射到规模相同的不同输出中, 如SHA-*, MD5等HASH算法.

那么如何利用HASH帮住我们为每个连接/会话生成唯一id? 这和如何生成sessionId问题差不多, 就是将每个连接的特征值(比如访问时间, 携带数据, ip地址, pid, 文件描述符, 内存地址等等信息)作为输入, 为其生成长度相等的无重复字符串.

但是无论什么方法, 也只是满足一定数量级内无相似度或较低相似度, 但是从业内普遍的密码保存方式可以知道设计良好的HASH算法足矣应付碰撞问题, 对于极小概率事件也没什么可担心的.

int seq=0;
function generateID(){
    return skip32(++seq,“password”);
}

这样如何?