一、兑换码的需求
兑换码不是简单的字符串它有很多需求:
-
可读性好:兑换码是要给用户使用的,用户需要输入兑换码,因此可读性必须好。我们的要求:
-
长度不超过10个字符
-
只能是24个大写字母和8个数字:ABCDEFGHJKLMNPQRSTUVWXYZ23456789(没有字母I和数字1,没有字母O和数字0)
-
-
数据量大:优惠活动比较频繁,必须有充足的兑换码,最好有10亿以上的量
-
唯一性:10亿兑换码都必须唯一,不能重复,否则会出现兑换混乱的情况
-
不可重兑:兑换码必须便于校验兑换状态,避免重复兑换
-
防止爆刷:兑换码的规律性不能很明显,不能轻易被人猜测到其它兑换码
-
高效:兑换码生成、验证的算法必须保证效率,避免对数据库带来较大的压力
二、算法分析
首先想到的就是UUID和雪花算法,但是他们并不合适我们的需求
0~31的角标刚好对应了我们的32个字符!而2的5次幂刚好就是32,因此5个二进制位的结果就是0~31,所以我们直接把数字转成二进制,每五个一组转10进制的结果是不是刚好对应一个角标。这样把二进制数加密得到的算法就是Base32算法。
为什么不能用雪花和UUID呢。因为我们要求字符不能超过10位,而每个字符对应5个bit位,因此二进制数不能超过50个bit位。UUID和Snowflake算法得到的结果,一个是128位,一个是64位,无法满足。
而自增id从1增加到Integer的最大值,可以达到40亿以上个数字,而占用的字节仅仅4个字节,也就是32个bit位。所以可以利用自增id作为兑换码,但是要利用Base32加密,转为我们要求的格式。
三、重兑校验算法
基于BitMap:兑换或没兑换就是两个状态,对应0和1,而兑换码使用的是自增id.我们如果每一个自增id对应一个bit位,用每一个bit位的状态表示兑换状态,是不是完美解决问题。而这种算法恰好就是BitMap的底层实现,而且Redis中的BitMap刚好能支持2^32个bit位。
四、防刷校验算法
一共50位,自增id占32位,使用密钥对自增id进行加密=签名,签名+自增id=50位的bit。
MD5和RSA算法来生成签名,因为这些算法得到的签名都太长了,一般都是128位以上,超出了长度限制。
-
将自增id(32位)每4位分为一组,共8组,都转为10进制
-
每一组给不同权重
-
把每一组数加权求和,得到的结果就是签名
加权累加得到的可以理解为加密的密钥。为了避免密钥被猜出来,我们再在32位自增长序列号前加上一个4bit位的新鲜值,4位对应了16组密钥,值是多少就取第几组的密钥,最后再把加权的和也就是签名转为二进制14bit位拼在前面。
总结兑换码生成算法:
-
利用Redis自增来生成序列号s,作为兑换码的核心
-
利用优惠券id的后4位做新鲜值f,得到加密密钥
-
利用密钥对序列号s加密,得到14位签名c
-
将f、c、s拼接,利用Base32编码,得到最终兑换码