不重复 N 位兑换码生成方案 - 基于线性同余算法

不重复 N 位兑换码生成方案 - 基于线性同余算法

事情许多,博客断更,又到年末,八百里加急。想到去年做的一个方案,很小但是很有趣有趣。

需求如下: 做一个活动,用户参与,便可获得个6位随机数兑换码,每隔7天抽40个兑换码,发一份会员福利。 活动规划:一期预估是五十万量级用户参与。验证效果不错,活动会被推全,成为一个长期营销活动。

技术拆解:需求很常规,但是其中一点比较有趣,如何“存储或维护”100w+量级,不可重复的兑换码呢。


方案

1. 数据库存储

常规一点的方案有,找个数据库,预先把兑换码刷进去,用户参与活动时取一个出来。但是这个方案有一个问题,需要确定数量的兑换码,10w 或者 50w,或者经常去补库存。并不适合这种可能需要无限兑换码的场景。

2. 不重复 N 位兑换码生成器

需求思考

首先 6 位数字兑换码,可以用100w 内的任意数字,前面补 0 得到。因为 6位数字最多有 999999 个不重复的数字,那么 pm 接受 999999 个兑换码以内不重复。

然后就想到一个思路,有没有一种方式,可以把一个有序的数组,打乱成为一个无序的数组。举个例子:1 对应 1993,那么它的兑换码就是 001993;2 对应 122200,那么它的兑换码就是 122200. 即便是看起来无序,只要有一一对应的关系就行。那么只需要维护一个计数器,就可以转换为一个“随机数”,生成相应的兑换码。这样非常省存储,还好维护。

从数学上想是可行的,那有没有这样的算法呢?有一个算法可以参考:

线性同余方法

线性同余方法(LCG)是个产生伪随机数的方法。它是根据递归公式:N{j+1} = (A * N{j} + B) % M。 其中 A,B,M是产生器设定的常数。LCG的周期最大为 M,但大部分情况都会少于M。要令LCG达到最大周期,应符合以下条件:1. B,M互质;2. M的所有质因数都能整除A-1; 3. M是4的倍数, A-1也是;4. A,B,N{0}都比M小;5. A,B是正整数。

落地方案

这个需求里,我们并不放需要创造真正的随机数,要的是这种数字一一对应的方式。

分析了线性同余方法后,我发现用公式:N = (A * C + B) % M, 便可以达到这样的目的;C 作为计数器,N作为兑换码的结果。M 取 1000000,A、B 符合线性同余方法的要求。例子中 A 随便取了一个大一点的质数 49823,M 取 1,跑了 100w 数据,没有出现重复。

编码实现 DEMO

上线一年多,运行良好,没有再去主动维护,PHP 代码如下:

// counter 是 Redis 维护的计数器
$counter = {counter}; 
$num = (49823 * $counter + 1) % 1000000;
// token 补0 后多做了一层反转
$token = strrev(sprintf("%06d", $num));

参考资料: