基于随机子串的解决方案并不好,因为输出会发生冲突。它可能会过早发生(运气不好),并且最终会在生成的值列表变大时发生。碰撞概率甚至不必那么大(参见生日攻击)。
对这个问题有好处的是递增 ID 与其对应的 ID 之间的伪随机排列,它将显示在 URL 中。这种技术保证了碰撞是不可能的,同时仍然生成与输入空间一样小的输出空间。
执行
我建议这个 C# 版本的Feistel 密码,它具有 32 位块、3 轮和一个受伪随机生成器启发的轮函数。
private static double RoundFunction(uint input)
{
// Must be a function in the mathematical sense (x=y implies f(x)=f(y))
// but it doesn't have to be reversible.
// Must return a value between 0 and 1
return ((1369 * input + 150889) % 714025) / 714025.0;
}
private static uint PermuteId(uint id)
{
uint l1=(id>>16)&65535;
uint r1=id&65535;
uint l2, r2;
for (int i = 0; i < 3; i++)
{
l2 = r1;
r2 = l1 ^ (uint)(RoundFunction(r1) * 65535);
l1 = l2;
r1 = r2;
}
return ((r1 << 16) + l1);
}
要在 base62 字符串中表示置换后的 ID:
private static string GenerateCode(uint id)
{
return ToBase62(PermuteId(id));
}
该Base62
函数与前面的答案相同,除了它是取uint
而不是int
(否则必须重写这些函数以处理负值)。
自定义算法
RoundFunction
是算法的秘诀。您可以将其更改为非公开版本,可能包括密钥。Feistel 网络有两个非常好的特性:
请注意,在圆形表达式中放置一些过于琐碎的东西会破坏伪随机效应,尽管它仍然会在每个PermuteId
输出的唯一性方面起作用。此外,在数学意义上不是函数的表达式将与算法不兼容,因此例如任何涉及random()
的内容都是不允许的。
可逆性
在其当前形式中,该PermuteId
函数是它自己的逆函数,这意味着:
PermuteId(PermuteId(id))==id
uint
因此,给定程序生成的短字符串,如果您使用函数将其转换回FromBase62
,并将其作为输入提供给PermuteId()
,则将返回相应的初始 ID。如果您没有数据库来存储 [internal-ID / shortstring] 关系,那就太酷了:它们实际上不需要存储!
制作更短的字符串
上述函数的范围是 32 位,也就是从 0 到 的大约 40 亿个值2^32-1
。要以 base62 表示该范围,需要 6 个字符。
只有 5 个字符,我们希望能表示最多的62^5
值,也就是略低于 10 亿。如果输出字符串限制为 5 个字符,则应按如下方式调整代码:
找到N
这样的,它N
是偶数并且2^N
尽可能高但低于62^5
. 那是 28,所以我们适合的实际输出范围62^5
将是2^28
或大约 2.68 亿个值。
在 中,PermuteId
使用28/2=14
位值代替 16 位,同时注意不要忽略输入的单个位(必须小于 2^28)。l1
r1
将结果乘以RoundFunction
16383 而不是 65535,以保持在 14 位范围内。
在 的末尾PermuteId
,重新组合r1
并l1
形成一个14+14=28
位值而不是 32。
相同的方法可以应用于 4 个字符,输出范围为2^22
,或大约 400 万个值。
它是什么样子的
在上面的版本中,前 10 个生成的以 id=1 开头的字符串是:
cZ6ahF
3t5mM
xGNPN
dxwUdS
ej9SyV
cmbVG3
科尔克
bfCPOX
JDr8Q
eg7iuA
如果我在 round 函数中做一个微不足道的改变,那就变成:
埃0LLY
ddy0ak
dDw3wm
bVuNbg
bKGX22
c0s5GZ
dfNMSp
齐SqE
cxKH4b
dNqMDA