创建独特的不可猜测的基数36 id

时间:2011-01-07 10:36:04

标签: php mysql primary-key unique base36

对于类似于URL缩短器服务的应用程序,我想创建不可猜测的id,我们都认为这是你所熟悉的。以下是此类ID的示例:

http://example.com/sd23t9

在数据库表中将这些作为主键插入时,生成这些技术的有效且高效的技术是什么?最小(或没有)冲突风险?

修改
Piskvor当然是一个很好的观点。我应该提到在达到36 ^ 6限制之前我意味着最小的碰撞风险。

编辑2
呃,废弃了,他的观点远远超过了当然。嗯。然后,或许(就像我已经在其他地方读过的那样)预先创建一个带有id的表格?当我受到36 ^ 6和非连续约束时,这会是最有效的技术吗?

6 个答案:

答案 0 :(得分:5)

Set ID length. // e.g. 6
do {
  Generate a short random ID of the given length
  Collision?
   - No:
      DONE!
   - Yes:
      increase ID length
 } while true

对于任何有限ID长度,始终存在冲突风险:假设您的示例中有[a-z0-9] {6}个ID,一旦您拥有2,176,782,336个ID,则冲突为100%保证(没有更多可用的密钥)。由于birthday effect,你很快就会发生碰撞。有了这么小的键空间,就没有办法避免碰撞 - 你需要进行某种碰撞恢复。

你可以生成一个ID,直到它没有碰撞 - 但是当关键空间被填充时,这将逐渐变慢:想象一个[az]的键空间,已经采用了[an]和[pz] - 现在每个新的随机ID更容易发生碰撞;当你完全填满键空间时,循环根本不会终止。

我建议的算法在这方面可能过于谨慎:如果它发现了冲突,它将逐渐尝试更长的ID(因为它假设“collision =>不可行来检查更短的密钥空间”)。虽然效率不高,但可能会在几次迭代中找到非碰撞ID。

答案 1 :(得分:3)

有点奇怪的想法。为什么不使用排列? 例如 你有一组值[0-9a-z] 当你生成第一个id。你按照字典顺序进行第一次排列。然后是第二个等等。 为了使它看起来不那么可猜测你可以改变字典顺序的规则。说'a'在't'之后或之类似。你也可以使用元组而不是完整的排列。 这将确保没有碰撞。

这个想法实际上是关于制作某种双向散列函数。基本上如果你能够以某种方式编码数字“1”以获得类似“q8d3dw”的东西并且能够将“q8d3dw”解码回“1”,你可以确定这个函数将为你提供所有值的唯一字符串从1到36 ^ 6.

问题实际上是选择此功能。简单的方法是将“1”与“000000”,“2”与“000001”,“12”与“00000b”相关联。基本上按字典顺序排列所有可用字符串,并在等于id的位置上选择字符串。然而,这很容易猜到。所以你可以做的是人为地改变词典顺序的规则。说而不是正常的顺序(0,1,2,3 ... a,b,c ... x,y,z)你可以稍微改变它并获得类似(a,5,t,3)的东西...)。这将产生更多混淆的结果。然而它仍然是可以猜测的,因为第一个元素是“aaaaaa”,第二个元素是“aaaaa5”,然后是“aaaaat”。因此,您可以进一步更改字典顺序的规则,使它们取决于角色的位置。说出第一个char id(a,5,t,3 ...)的顺序为第二个(y,7,3,r ...)等。

现在,我不会发布任何伪代码,因为它会很长。除非你有兴趣创建这种算法以获得乐趣,否则我不建议你选择这条路线。但是,如果你使用这条路线,它可能是一种非常有效的方法来生成这个id而没有碰撞的可能性。我建议你阅读Donald Knuth博士的第4卷“计算机编程艺术”。实现这些算法有很多建议。

答案 2 :(得分:2)

@ivan:这是一个实现。

首先你需要6个字符串,这里是生成它们的代码:

$letters = range('a', 'z');
$numbers = range(0, 9);
$char_list = array_merge_recursive($letters, $numbers);
for ($i = 0; $i <= 5; $i++) {
    shuffle($char_list);
    echo '$mysterykey[' . $i . "] = '" . implode($char_list) . "';\n";
}

然后你可以使用函数中的输出:

function int_to_x36($value) {
    $mysterykey[0] = 'awkbs81t3jyg20v4qhoxzcuenr65dfimlp97';
    $mysterykey[1] = 'ut17qclz96n3msky8jwp4r25gdvhxo0biaef';
    $mysterykey[2] = 'cewszx142nph05mi9ulafbdvy36o8gq7trjk';
    $mysterykey[3] = '37hp28wjdqe5vnlzxofrsybu4im90k16agtc';
    $mysterykey[4] = 'n9a3jksl5ct0eqfzo7pvgyd4m2hiu18xrb6w';
    $mysterykey[5] = 'mq0nbk3zs529gu4tecli8j176vardxoypfwh';

    $x36 = array();
    for ($i = 5; $i >= 0; $i--) {
        $x36[$i] = 0;  
        $y = pow(36, $i);

        if ($value >= $y) {
            $z = floor($value/$y);
            $value = $value - ($z * $y);
            $x36[$i] = $z;
        }   
    }      

    $encoded = '';
    for ($i = 0; $i <= 5; $i++) {
        $encoded .= $mysterykey[$i][$x36[$i]];
    }

    return $encoded;
}

function x36_to_int($value) {
    $mysterykey[0] = 'awkbs81t3jyg20v4qhoxzcuenr65dfimlp97';
    $mysterykey[1] = 'ut17qclz96n3msky8jwp4r25gdvhxo0biaef';
    $mysterykey[2] = 'cewszx142nph05mi9ulafbdvy36o8gq7trjk';
    $mysterykey[3] = '37hp28wjdqe5vnlzxofrsybu4im90k16agtc';
    $mysterykey[4] = 'n9a3jksl5ct0eqfzo7pvgyd4m2hiu18xrb6w';
    $mysterykey[5] = 'mq0nbk3zs529gu4tecli8j176vardxoypfwh';

    $value36 = str_split($value);

    $x36 = array();
    for ($i = 0; $i <= 5; $i++) {
        $x36[$i] = strpos($mysterykey[$i], $value36[$i]);
    }

    $x = 0;
    for ($i = 5; $i >= 0; $i--) {
        $x += $x36[$i] * pow(36, $i);
    }      

    return $x;
}

我确信我已经忽略了一些东西,但它似乎正在发挥作用。

答案 3 :(得分:1)

大随机数和SHA-256哈希例如?您可以稍后缩短它以满足您的需求。

答案 4 :(得分:1)

如果网站足够大,我的意思是 - 如“我们需要每秒分配数百个新ID”(这会产生其他问题,例如耗尽36 ^ 6密钥空间)一年),你可以预先生成密钥并分配它们。

以下是伪代码示例 - 在如此大流量的站点中,您可能需要优化所使用的算法。

预生成很简单 - 只需从000000开始,一直到zzzzzz,将每一行插入表中并将其标记为未使用:

 ID     | used
==============
 000000   0   
 000001   0   
 ...
 zzzzzz   0   

当您收到新ID的请求时,请选择一个随机的ID并将其标记为已使用(危险!并发问题!):

SELECT ID FROM ids WHERE RAND() LIMIT 1
UPDATE ids SET used = 1, url=what_you_want_shortened WHERE ID = whatever_you_got_from_previous_query

你会遇到效率问题(例如WHERE RAND(),表格锁定等),但它是可行的。

答案 5 :(得分:0)

如果你不反对引入.NET dll,我已经创建了一个项目来完成这个。来源为on GitHub here,二进制文件位于IdGenerator NuGet Package

它为您提供有序序列和/或用户指定长度的随机值,全部在base-36中。即使有多个id生成器实例或在分布式环境中,ID也是唯一的。

var generator = new Base36IdGenerator(
                numTimestampCharacters: 11, 
                numServerCharacters: 4, 
                numRandomCharacters: 5, 
                reservedValue: "", 
                delimiter: "-", 
                delimiterPositions: new[] {15, 10, 5});

// This instance would give you a 20-character Id, with an
// 11-character timestamp, 4-character servername hash, 
// and 5 character random sequence. This gives you 1.6 million
// hash combinations for the server component, and 60 million
// possible combinations for the random sequence. Timestamp is
// microseconds since epoch:
Console.WriteLine(generator.NewId());
// 040VZC6SL01003BZ00R2

// Argument name specified for readability only:
Console.WriteLine(generator.NewId(delimited: true));
// 040VZ-C6SL0-1003B-Z00R2

当然,如果你对字符串不感兴趣而不是有序序列感兴趣,你可以使用GetRandomString方法:

// 6-characters give you a little over 2 billion possible 
// combinations (36 ^ 6). 7 characters is about 78 billion.
Console.WriteLine(generator.GetRandomString(length: 6));

其背后的基本原则是:

  • 从纪元(64位数字)
  • 获取微秒
  • 获取0到(36 ^所需长度)(最多13个)
  • 之间的随机数(64位)
  • 获取服务器名称哈希(简单Sha1)
  • 将每个组件转换为base-36 string
  • 向左移动0至所需长度

Base36编码器本身来自http://www.stum.de/2008/10/20/base36-encoderdecoder-in-c/

相关问题