1.兑换码要求
要求如下:
-
可读性好:兑换码是要给用户使用的,用户需要输入兑换码,因此可读性必须好。我们的要求:
-
长度不超过10个字符
-
只能是24个大写字母和8个数字:ABCDEFGHJKLMNPQRSTUVWXYZ23456789
-
-
数据量大:优惠活动比较频繁,必须有充足的兑换码,最好有10亿以上的量
-
唯一性:10亿兑换码都必须唯一,不能重复,否则会出现兑换混乱的情况
-
不可重兑:兑换码必须便于校验兑换状态,避免重复兑换
-
防止爆刷:兑换码的规律性不能很明显,不能轻易被人猜测到其它兑换码
-
高效:兑换码生成、验证的算法必须保证效率,避免对数据库带来较大的压力
2.Base32转码
把二进制数经过加密得到字符的算法就是Base32法,类似的还有Base64法。
举例:假如我们经过自增id计算出一个复杂数字,转为二进制,并每5位一组,结果如下:
01001 00010 01100 10010 01101 11000 01101 00010 11110 11010
此时,我们看看每一组的结果:
-
01001转10进制是9,查数组得字符为:K
-
00010转10进制是2,查数组得字符为:C
-
01100转10进制是12,查数组得字符为:N
-
10010转10进制是18,查数组得字符为:B
-
01101转10进制是13,查数组得字符为:P
-
11000转10进制是24,查数组得字符为:2
-
...
依此类推,最终那一串二进制数得到的结果就是KCNBP2PC84,刚好符合我们的需求。
综上,我们可以利用自增id作为兑换码,但是要利用Base32加密,转为我们要求的格式。此时就符合了我们的几个要求了:
-
可读性好:可以转为要求的字母和数字的格式,长度还不超过10个字符
-
数据量大:可以应对40亿以上的数据规模
-
唯一性:自增id,绝对唯一
3.重兑校验算法
-
基于数据库:我们在设计数据库时有一个字段就是标示兑换码状态,每次兑换时可以到数据库查询状态,避免重兑。
-
优点:简单
-
缺点:对数据库压力大
-
-
基于BitMap:兑换或没兑换就是两个状态,对应0和1,而兑换码使用的是自增id.我们如果每一个自增id对应一个bit位,用每一个bit位的状态表示兑换状态,是不是完美解决问题。而这种算法恰好就是BitMap的底层实现,而且Redis中的BitMap刚好能支持2^32个bit位。
-
优点:简答、高效、性能好
-
缺点:依赖于Redis
-
4.防刷校验算法
JWT分为三部分组成:
-
Header:记录算法
-
Payload:记录用户信息
-
Verify Signature:验签,用于验证整个token
JWT中的的Header和Payload采用的是Base64算法,与我们Base32类似,几乎算是明文传输,难道不怕其他人伪造、篡改token吗?
为了解决这个问题,JWT中才有了第三部分,验证签名。这个签名是有一个秘钥,结合Header、Payload,利用MD5或者RSA算法生成的。因此:
-
只要秘钥不泄露,其他人就无法伪造签名,也就无法伪造token。
-
有人篡改了token,验签时会根据header和payload再次计算签名。数据被篡改,计算的到的签名肯定不一致,就是无效token
因此,我们也可以模拟这种思路:
-
首先准备一个秘钥
-
然后利用秘钥对自增id做加密,生成签名
-
将签名、自增id利用Base32转码后生成兑换码
只要秘钥不泄露,就没有人能伪造兑换码。只要兑换码被篡改,就会导致验签不通过。
当然,这里我们不能采用MD5和RSA算法来生成签名,因为这些算法得到的签名都太长了,一般都是128位以上,超出了长度限制。
因此,这里我们必须采用一种特殊的签名算法。由于我们的兑换码核心是自增id,也就是数字,因此这里我们打算采用按位加权的签名算法:
-
将自增id(32位)每4位分为一组,共8组,都转为10进制
-
每一组给不同权重
-
把每一组数加权求和,得到的结果就是签名
举例:
最终的加权和就是:4*2 + 2*5 + 9*1 + 10*3 + 8*4 + 2*7 + 1*8 + 6*9 = 165
这里的权重数组就可以理解为加密的秘钥。
当然,为了避免秘钥被人猜测出规律,我们可以准备16组秘钥。在兑换码自增id前拼接一个4位的新鲜值,可以是随机的。这个值是多少,就取第几组秘钥。
这样就进一步增加了兑换码的复杂度。
最后,把加权和,也就是签名也转二进制,拼接到最前面,最终的兑换码就是这样: