一、兑换码的需求

兑换码不是简单的字符串它有很多需求:

java 兑换码随机生成 兑换码生成算法_序列号

  • 可读性好:兑换码是要给用户使用的,用户需要输入兑换码,因此可读性必须好。我们的要求:
  • 长度不超过10个字符
  • 只能是24个大写字母和8个数字:ABCDEFGHJKLMNPQRSTUVWXYZ23456789(没有字母I和数字1,没有字母O和数字0)
  • 数据量大:优惠活动比较频繁,必须有充足的兑换码,最好有10亿以上的量
  • 唯一性:10亿兑换码都必须唯一,不能重复,否则会出现兑换混乱的情况
  • 不可重兑:兑换码必须便于校验兑换状态,避免重复兑换
  • 防止爆刷:兑换码的规律性不能很明显,不能轻易被人猜测到其它兑换码
  • 高效:兑换码生成、验证的算法必须保证效率,避免对数据库带来较大的压力

二、算法分析

首先想到的就是UUID和雪花算法,但是他们并不合适我们的需求

java 兑换码随机生成 兑换码生成算法_自增_02

 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位拼在前面。

java 兑换码随机生成 兑换码生成算法_序列号_03

总结兑换码生成算法:

  1. 利用Redis自增来生成序列号s,作为兑换码的核心
  2. 利用优惠券id的后4位做新鲜值f,得到加密密钥
  3. 利用密钥对序列号s加密,得到14位签名c
  4. 将f、c、s拼接,利用Base32编码,得到最终兑换码