实际应用场景中经常会遇到用什么方式来生成userId,常用的方式是直接使用Mysql的自增主键作为userId,优点是简单方便,插入时无须考虑userId重复的问题,缺点是位数不一致,如果userId还需要暴露给用户或者第三方,容易让他人估计出用户总数量,同时伪造userId也变得更方便。所以需要一个更好的方式来产生userId来代替自增主键。
以下算法可以通过一个数字产生另一个位数相对固定的数字,同时还是可逆的。
$seeds = [
'7893401256',
'7296853041',
'9832107654',
'8952346701',
'6035892471',
'5293076418',
'8721643095',
'4532980167',
];
function contactID2userID(int $contactID, array $seeds): int {
$letters = count($seeds);
$kinds_ucode = 10;
// 替换最后 8 位数字
$rv = []; // 替换前的ID为数组
$nv = [];
$rv[0] = $contactID % $kinds_ucode;
$nv[0] = ($contactID - $rv[0]) / $kinds_ucode;
for ($i = 1; $i < $letters; $i++) {
$j = $i - 1;
$rv[$i] = $nv[$j] % $kinds_ucode;
$nv[$i] = ($nv[$j] - $rv[$i]) / $kinds_ucode;
}
$rv[$letters] = $nv[$letters - 1];//在第 9 位之后取 9,10,11,...
$iv = [];
$iv[0] = $rv[0];
for ($i = 1; $i < $letters; $i++) {
$iv[$i] = ($rv[$i] + $rv[0]) % $kinds_ucode;
}
$xv = []; // 替换后的 ID 排列成数组
for ($i = 0; $i < $letters; $i++) {
$xv[$i] = substr($seeds[$i], $iv[$i], 1); // 替换为 $seeds[$i]的数字字符串中的$idxth数字
}
// 第9位是数字+1的值(这样就不会变成0)
$xv[$letters] = $rv[$letters] + 1;
$result = 0;
for ($i = 0; $i <= $letters; $i++) {
$result += (10 ** $i) * $xv[$i];
}
return $result;
}
function userID2contactID(int $userID, array $seeds): int {
$letters = count($seeds);
$kinds_ucode = 10;
$xv = [];
$nv = [];
$xv[0] = $userID % $kinds_ucode;
$nv[0] = ($userID - $xv[0]) / $kinds_ucode;
for ($i = 1; $i < $letters; $i++) {
$j = $i - 1;
$xv[$i] = $nv[$j] % $kinds_ucode;
$nv[$i] = ($nv[$j] - $xv[$i]) / $kinds_ucode;
}
$xv[$letters] = $nv[$letters - 1]; // 在第 9 位之后取 9,10,11,...
$iv = [];
for ($i = 0; $i < $letters; $i++) {
$iv[$i] = strpos($seeds[$i], (string) $xv[$i]); // 用 $xv[$i] 在 $seed[$i] 中搜索
}
$rv = [];
$rv[0] = $iv[0];
for ($i = 1; $i < $letters; $i++) {
$rv[$i] = ($iv[$i] - $rv[0] + $kinds_ucode) % $kinds_ucode;
}
// 在第9位,数字值增加了1,所以恢复它
$rv[$letters] = $xv[$letters] - 1;
$result = 0;
for ($i = 0; $i <= $letters; $i++) {
$result += (10 ** $i) * $rv[$i];
}
return $result;
}
echo contactID2userID(1, $seeds); // 157209828
echo userID2contactID(157209828, $seeds); // 1
如何更好的获取自增ID?
方式一:利用Mysql的自增主键,但是需要先insert一条记录再生成userId后update回去,这样效率是很低的
方式二:利用Redis的INCRBY,可以达到预期效果,只需要将Redis里的自增值同时记录到DB中即可,方便Redis丢失时可以还原最大自增值。
方式三:在Redis Cluster集群模式下,由于集群内会存在多台Redis实例,那么我们可以考虑利用多个RedisKey的自增来提高自增的吞吐量,比如设置16个自增key,名字依次是incr_key_[1-16],那么这16个key算出来的hash slot必定不同,很可能就分布在多台实例上,那么计算自增值的方式
$suffix = mt_rand(1, 16);
$key = "incr_key_" . $suffix;
$uniqueID = $redis->incrby($key);
$contactID = $suffix + $uniqueID * 16;
$userID = contactID2userID($contactID);