工作小记——小秒杀活动
公司紧急要求搞一个促销活动:
活动到时放出banner,点进去未登录的需先登录,已领取过直接显示已领取。未领取过的话,点“送祝福”,随机弹出一条祝福语,再点“领红包”,则发一个红包。总共1900个,领完弹框“已领完”
需求还是比较简单的,但是这个总共1900是个隐患啊,需求轻描淡写的一笔,架构可要小心。就算需求方说不会大肆宣传,小范围宣传,到时候直接出banner,看到的人才点。但是架构还是要做高并发的考虑。万一到时大家都来抢,把服务器弄挂了,就是大事啊。
因为时间紧,所以作了一个简单的方案。
1.单独页面:活动应打开新的单独页面,不在主页进行跳转。如果一直没收到服务器响应,用户也不能进行普通的投资浏览操作,要后退或关掉再开,体验不好
2.文本存在前端:所有祝福语直接存在前端,随机选取的操作由前端完成,不往后台发送请求
3.前端判断登录:前端可以通过cookie判断是否登录,不用请求后台。未登录直接跳转至登录页面
4.延后判断领取状态:进入活动页后,不先判断是否领取过。每一次进入页面,都要发请求给后台检查登录状态,平白无故多出一倍请求,且无意义。等用户点领取再判断并返回结果就可以了
5.先插流水,后发放:没必要在请求的时候,同步把所有的事情做完。先插流水,之后跑job根据流水表进行发放。延迟时间看业务方的接受度。
6.用缓存控制
7.用累加计数:一般来说,从0开始累加到1900,和从1900累减到0效果是一样的。但是累加的方式,总数不用一开始写死,整个活动过程中还能动态调整总数。而累减的方式,一开始就要初始化总数。
第五点着重聊一下:
负责开发的同事第一反应是:来新的请求时,先count(1)一下,如果<1900则插入一条流水,返回领取成功,如果>=1900则返回已领完。之后再跑job扫描流水表进行红包的发放。
流程图如下:
一般单线程情况下是没有问题,可是并发的话,比如两个线程大家都读到count(1)为1899,判断下都<1900,都去insert流水表,则最终1901条数据,超卖了。
开发的同事提出了第二版方案:先插入,再count(1),如果>1900,则返回失败,等发放的时候,根据插入的时间排序,选择1900条
流程图如下:
这个确实不会超卖了,但是有两个问题:
1.会误报信息。1899时,两个线程同时插入表后,再count(1)=1901,就都返回“已领完”。其实一个成功,一个失败。发放的时候不会少发,但是会错误提示
2.如果不加其他判断,这样操作,会流水表中插入远大于1900条的数据,除1900条以为都是废数据。
当然,可以在达到1900之后在redis中放一个标志位,之后如果标志位置为是,则不再insert。数据库操作远比redis操作慢,不如将领取数放在redis中。
考虑流程如下:
这个方案有几点比较好:
1.利用redis为单线程模型,实现分布式原子增减。并且在增减后获得最准确的结果,不存在并发情况
2.直接Insert,将检查是否已领取与插入流水合成一件事情做。不是先检查再插入,会存在并发问题
3.计数统计访问redis某键值,比count(1)性能好
由于时间非常紧急,且活动较小,暂时做了这些优化。如果时间充裕点,且活动更大点,还可以作如下改进:
1.动静分离:将静态资源如css,js等均推送到cdn上,由cdn来扛秒杀前,页面刷新的访问流量
2.领取流水用redis集合:可以往redis集合中添加元素(用户id),添加成功则说明未领取过,添加失败则说明已领取过,会比数据库插入一条流水性能好
3.设置已领完标志:如果担心redis集合中元素太多,则可以在到达指定条数后,置某个标志位,之后请求先查询该标志位,为true时直接返回“已领完”
4.发送一条消息入MQ:如果担心redis突然宕机,领取的用户id集合丢失的话,则加一个往MQ里扔一条消息的操作。异步取消息入库。redis和mq记了两份数据,同时宕机的概率非常小
结合,2,3,4,流程图如下:
5.用lua脚本:根据流程图,可以发现前面连续操作都是redis操作,只有最后是MQ。可以将多个redis操作编写成lua脚本,并预存在redis中,用evalsha命令一步执行。性能可以进一步提升