使用auto_increment生成短唯一ID的PHP实现?

7
我想生成一个短的、唯一的ID,而不必检查冲突。我目前做的是这样的,但我生成的ID是随机的,在循环中检查冲突很麻烦,如果记录数显著增长,将会变得非常昂贵。通常担心冲突不是问题,但我想生成的唯一ID是一个短的唯一字符串,5-8个字符,字母数字混合,就像tinyurl一样。编辑:我想从5个字符开始,如果有6000万条记录,那么就转到6个字符,以此类推。
为此,我考虑使用一个对用户隐藏的自动递增值,并向他们呈现一个从该值生成唯一字符串的MD5或其他方法。生成的字符串不应该呈现线性,因此简单地将自动递增ID转换为base 36 [0-9A-Z]有点过于简单,但类似于该函数的功能是我正在考虑的方向。
编辑:安全性不是问题,因为这不会用于保护信息。它只是一个到较长字符串的快捷方式。 谢谢。感谢您的建议,对延迟表示歉意。牙医...
8个回答

6
你需要一个正确构造的东西,即排列函数:这是一种将一个整数(你的顺序计数器)映射到另一个整数的一对一可逆映射函数。以下是一些示例(任何这些组合也应该有效):
  • 反转一些位(例如使用异或运算符^在PHP中)
  • 交换位的位置(($i&0xc)>> 2 |($i&0x3)<< 2),或仅颠倒所有位的顺序
  • 添加一个常量值,取模你的最大范围(如果你将其与上述方法结合使用,必须是二的因子)
例如:此函数将将0、1、2、3、5等数字转换为13、4、12、7、15等数字,适用于最高达15的数字:
$i=($input+97) & 0xf;
$result=((($i&0x1) << 3) + (($i&0xe) >> 1)) ^ 0x5;

编辑

更简单的方法是使用线性同余生成器(LCG,通常用于生成随机数),其定义为以下形式的公式:

X_n+1 = (a * X_n + c) mod m

对于a、c和m的良好值,X_0、X_1..X_m-1的序列将恰好包含0到m-1之间的所有数字。现在,您可以从线性递增索引开始,并使用LCG序列中的下一个值作为您的“秘密”密钥。 EDIT2 实施: 您可以设计自己的LCG参数,但如果出错,它将无法覆盖整个范围(因此会有重复项),因此我将在此处使用一组已发布和尝试过的参数(这篇论文中)
a = 16807, c = 0, m = 2147483647

这将为您提供2 ** 31的范围。使用pack()可以将结果整数作为字符串获取,base64_encode()使其成为可读字符串(每字节6位,最多6个有效字符),因此这可能是您的函数:
substr(base64_encode(pack("l", (16807 * $index) % 2147483647)), 0, 6)

我喜欢这个想法。你有代码吗?1-60000000映射到一个5字符的字母数字字符串? - Daren Schwenke
抱歉,这个时间我不太相信我的数学,所以这是一个31位的。对于30位的,你会在base64_encode后恰好有5个(有效的,即没有填充'=')字符,但我找不到适用于那个的参数设置。 - Wim
我认为这个可以用于30位的: a = 357913942,c = 1,m = 1073741823(如果我没错的话,它符合维基百科文章中的标准,以便具有完整周期:m = 2 ** 31-1 = 3 ** 2 * 7 * 11 * 31 * 151 * 331,c = 1,因此它显然与m互质,而a-1 = 3 * 7 * 11 * 31 * 151 * 331,可被m的所有质因数整除...) - Wim
我读的都是数学,数学,数学.. :) 这个周末我会生成一些测试用例,很多测试用例。谢谢! - Daren Schwenke

1

你可以生成当前日期/随机数的MD5哈希值,并将其截断为所需长度(5-8个字符),然后将其存储为ID字段。

如果你要将这些信息存储在数据库中,你不需要使用for循环来进行冲突检查,而是可以使用select语句 - 类似于

SELECT count(1) c FROM Table WHERE id = :id

其中 :id 将是新生成的 id。如果 c 大于 0,则表示它已经存在。

编辑

这可能不是最好的方法。但我会尝试一下,所以我猜你需要的是将数字转换为唯一的短字符串的方法,并且不是按顺序排列的。

我想,正如你所说,base64 编码已经将数字转换为短字符串了。为了避免序列问题,您可以在自动生成的 id 和某个“随机”值(唯一映射)之间建立一些映射。然后,您可以对此唯一值进行 base64 编码。

您可以按以下方式生成此映射。创建一个临时表,存储从 1 到 10,000,000 的值。以随机顺序排序并将其存储到 Map 表中。

INSERT INTO MappingTable (mappedId) SELECT values FROM TemporaryTable ORDER BY RAND()

MappingTable应该有2个字段:id(您的自动生成的ID将针对此进行查找)和mappedId(这是您将为其生成base64编码的内容)。

当您接近1000万时,可以再次运行上述代码,并使用10,000,001-20,000,000之类的临时表中的值更改值。


循环开始生成另一个唯一标识符,如果失败的话。我知道这听起来像是过早优化,但我希望有更好的方法。 - Daren Schwenke

1

你可以使用按位异或来混淆一些比特:

select thefield ^ 377 from thetable;

+-----+---------+
| a   | a ^ 377 |
+-----+---------+
| 154 |     483 |
| 152 |     481 |
|  69 |     316 |
|  35 |     346 |
|  72 |     305 |
| 139 |     498 |
|  96 |     281 |
|  31 |     358 |
|  11 |     370 |
| 127 |     262 |
+-----+---------+


0

我认为这永远不会真正安全,因为你只需要找到短唯一字符串背后的加密方法就可以劫持一个ID。在您的设置中循环检查冲突是否真的有那么棘手吗?


现在还不会,但在五个字符和五千万个链接左右,大约80%的情况会发生冲突。 - Daren Schwenke

0
一个递增数字的MD5应该是可以的,但我担心如果你将MD5(通常为128位)截断为5-8个字符,你几乎肯定会损害它作为唯一签名的能力...
完全正确。特别是当你达到80%的碰撞概率时,截断的MD5将与任何随机数一样好,以保证独特性本身,即毫无价值。
但既然你已经在使用数据库,为什么不直接使用UNIQUE INDEX?这样,MySQL自己就可以以更有效的方式完成唯一性检查(而不是使用循环)。只需尝试使用生成的MD5密钥进行插入,如果失败,请重试...

是的,但我仍然需要重新计算随机数并重试插入。这就是我现在所做的。我正在寻找一种自动确保第一次尝试唯一的方法。基于36的方法可以在第一次尝试时做到这一点,但第一个链接将是00000,第二个链接将是00001,依此类推。 - Daren Schwenke
当我有足够的声望时,当然可以... ;) - Wim
对于建议使用 SQL 唯一索引的建议给予 +1,但对于继续建议使用 MD5 给予 -1。如果要使用没有 100% 唯一值的伪随机数,那么您可以使用 PHP 的 uniqid 作为唯一键(如果不是主键),或者使用自动增量与外部选项混合(如 base64encode)来“掩盖” ID。 - Kevin Peno
当然,这两种方法本身都不能保证唯一性。你真的需要一个在有限范围内的排列组合,可以看看我的其他答案。 - Wim
1
是的,你说得对。我本不应该把这个作为答案发布,但我无法在原始评论上发表评论。还剩13分要去...;-) - Wim
我喜欢你的另一个答案,但是我在评论这个答案;)(抱歉跳下来,我编辑了一下)。你肯定正在逐渐掌握评论技巧 :P - Kevin Peno

0

如果您无法使用自动递增字段,并且想要一个绝对唯一的值,请使用UUID。如果您决定使用除自动递增之外的任何其他内容,则不检查冲突是愚蠢的。


-1

一个递增数字的MD5应该是可以的,但我担心如果你将MD5(通常为128位)截断为5-8个字符,你几乎肯定会破坏它作为唯一签名的能力...


MD5(或任何哈希函数)永远不能作为唯一签名。因此,您的截断担忧有点无关紧要。 - Kevin Peno
我不确定那怎么否定了我的回答。人们经常会做一些愚蠢的事情,这并不能使哈希“神奇地”变成“唯一”的。 - Kevin Peno
那么,因为碰撞甚至是可能的,所以它作为签名就没用了? - dicroce

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接