互联网公司推出新产品的时候,经常使用邀请码作为一种运营手段,在一定的时间内,只有具有邀请资格的用户,才能参与到早期产品的体验中去。
这么做,一方面为了体现产品体验的稀缺性,鼓励用户抢购,造成供不应求的舆论影响。这点小米做的尤为突出,效果也是相当不错的。另一方面,先让一部分比较有热情的早期种子用户进来,也能够充分的对产品的易用性和体验流程进行早期的验证,方便手机用户的反馈之后迭代修改。·
我们首先来看看根据需求邀请码服务需要提供出哪些接口:
- 单个或批量的生成邀请码,要求六位,数字与字符的混合,供运营和用户使用;
- 给用户激活邀请码;
- 校验用户是否已经激活邀请码;
- 批量给已注册用户绑定几个可用的邀请码,方便二次传播;
- 查询用户名下已经绑定的邀请码和其被激活的情况,验证二次传播的效果
根据需求,现有的技术实现方案有很多,核心来讲的话主要有两点:
- 邀请码生成算法与唯一性保证
- 邀请码高效查询与激活
另外还有一些其他的点,会在文中慢慢道来。
邀请码的生成方式
产品的要求是六位,同时字母和数字混合的方式,一共会发放10000个左右。一开始的思路是使用纯算法的方式实现,通过生成唯一的ID,增加一些共性的信息,再根据信息摘要的方式,映射成字母与数字的混合,不过试了一下,算法实现的成本比较高,而且还有借助额外的唯一ID生成的工具,所以有些得不偿失。
另外由于不能是纯数字,所以十六进制映射的方案也无法进行。而且也不能是一长串的字符,这样md5的方案也无法实施了。
所以在这样的前提之下,决定使用简单算法+redis来做整个方案。
首先是邀请码的生成,由于生成的过程一般是后台或者手动触发,所以对性能的要求并不太高,也就是说,允许一定程度的碰撞。所谓的碰撞,就是算法算出来的邀请码,和已经生成的邀请码重复了。所以我们来算一下概率:
一共是六位的邀请码,每一位的组成是大小写字母+阿拉伯数字,也就是26+26+10=62种可能性。所以六位也就是一共能够产生出62^6=56,800,235,584
即560亿个邀请码,产品的要求是一共放出去一万个,所以即使现在已经有一万个邀请码被生成了,那么生成一个邀请码和现有邀请码重复的概率只是10000/560亿
, 非常低的概率,如果真的中了大奖,那么就进行二次生成,再次碰撞的概率,恐怕就更低了。
同时为了保证邀请码看起来更真实,再增加一个连续两位不能重复的验证,同时每次生成结束之后,使用redis验证是否唯一,即可实现整个邀请码的生成过程。
数据结构的设计
上文说到为了保证邀请码的唯一性,必须使用redis,redis中的集合数据结构就提供了很好支持。每次生成完成后,只需要调用isMember validatorSet
方法,即可根据返回判断是否已经生成了邀请码。
同时使用集合存储邀请码,也能够满足统计的需求,只需要定时执行smembers validatorSet
,即可统计出所有生成出来的邀请码。
另外,由于存在用户激活邀请码,和查询是否已经激活邀请码的寻求,所以必须对此设计专门的数据结构。查询需求是高频需求,在用户量比较大的情况下,会有比较大的访问压力。同时为了扩展可能存储的用户其他信息,所以使用散列的数据类型进行存储。
每次查询用户是否已经激活,只需要执行hget user:$guid validator
即可获取其中的邀请码信息,从而判断出用户是否已经激活。
而用户激活的过程,涉及的数据结构稍多,每次激活的过程,都需要分成几步:
第一步是查询这个邀请码是否是合法有效的邀请码,需要使用ismember validatorSet
进行判断;
如果合法有效,第二步是设置用户的激活状态,即上文中所用到的这个散列hset user:$guid validator {$validator}
;
第三步则是设置邀请码本身的状态,即这个邀请码被哪个用户用了,方便之后查询 set validator:{$validator}:status {'userName':ted,'avatar':'http://test.com'}
由于第二步和第三步必须保证数据的幂等性,因此需要使用redis的事务来进行执行,保证两者能够同时成功的被执行:
$redis->Multi()
->set validator:{$validator}:status {'userName':ted,'avatar':'http://test.com'}
->hset user:$guid validator {$validator}
->exec()
但是除了使用事务,还需要规避的就是两个用户同时取到了同一个邀请码,在同一时间进行了激活的操作,在高并发的情况下,会有原子性的问题。所以必须引入redis中的watch命令,监控邀请码的status这个key,保证一旦它被变更了,那么当前的事务就不再执行,从而避免给用户带来错误的反馈:
redis->watch validator:{$validator}:status
由此,就完成了用户邀请码的激活过程。
除此之外,还有个小功能,那就是给一批种子用户绑定几个邀请码,并在邀请码被激活之后,展示出相应的状态。刚刚说到了用户为维度的那个哈希表结构,是时候给它再加一个field了,这个field就叫boundValidators,专门用来存储用户名下被绑定的邀请码:
hset user:$guid boundValidators $validator1|$validator2...
那么查询的时候只需要进行两次查询:
// 查询用户名下绑定的邀请码
hset user:$guid boundValidators
// 查询邀请码是否以及被谁激活了
mget $boundValidators
为了降低网络延时,完全可以通过一个pipeline实现,从而使得网络交互从两次变成一次。
接口性能验证
由此已经大功告成,简单的使用wrk测试一下性能,服务器相比之前的差一些,一共是4核16G内存。使用wrk -t12 -c100 -d10s http:ip:port/queryActivate
,
海外内网、同步的redis调用:
QPS在5000左右,完全满足了本次邀请码的需求,事实上,如果能超过这个量,估计睡觉都要笑醒了:)