让咱们聊聊秒杀这东西

万事皆有因

这段彷佛都成我写blog标准开头。言归正转,公司之前业务涉及到秒杀,而且是白天从10点起到晚上10点每小时一次(TT每天心惊肉跳的),周六还有个大礼包活动(重量级,常常会出一些你意想不到的事情,例如不活跃的用户忽然间活跃了,量级飙升TT)。同时,最近随着创业的兴起,仍是有不少人关注秒杀这技术怎么作。虽然不少NB的大厂(小米,淘宝,JD等)已经讲过这东西了,可是我仍是想讲讲这件事情。下面我就说说一个小厂是如何作秒杀的。前端

 

小厂有多小,小厂有多大

后端只有2个研发工程师和2个前端工程师,当时尚未全职的运维,不过服务器的数量有40多台(仍是挺多的)。用户量呢,下载和注册都在千万级别了,活跃也在百万级别。好了,小厂很小,可是小厂也很大。算法

 

初出茅庐

不少人感受,敢用初出茅庐这标题,应该很牛吧,然而并无。而且是意想不到的惨,惨不忍睹。第一个版本的秒杀系统,彻底是依赖MySQL的事务,不言而喻,你们都会知道有多惨。我直接告诉你们结果就能够了:数据库

  1. 整个系统在秒杀期间基本上停摆了,500和超时异常的多。后端

  2. 准备秒杀的产品数量是100,最后卖出去了400份。缓存

咱们来分析下为何会这样:服务器

  1. MySQL自己能承载连接数量有限,在秒杀的时候大量的连接处在事务状态,且绝大部分事务是须要回滚的,这就形成了很大的IO压力和计算压力前端工程师

  2. 那为何会超卖呢,由于最开始使用的主从结构,读写是分离的,主库压力那么大,从库同步跟不上,形成了卖出去的产品在毫秒级内再查询结果看起来就是没卖出去。简而言之就是就是技术不熟悉致使设计失误。运维

 

初窥门径

出第一次事故的时候,说句内心话,对一个刚毕业1年的工程师仍是挺蒙,而后就各类猜测。不过好在当时淘宝的一我的的blog上提了MySQL句事务的问题,算是找到方向了。而后就这样,秒杀活动就先暂停了一个星期,这个星期中我和同事都作了什么呢?性能

  1. 搭建了一个测试环境,模拟了下秒杀的状况,观察了MySQL的事务和主从的总体状况测试

  2. 修改秒杀流程

我先说下初版的流程:

  1. 从用户数据库查询用户积分是否充足,从规则数据库中查询用户是否符合条件

  2. 从数据库中读出一个产品的ID

  3. 而后事务性的将产品ID和用户ID关联,减小用户积分和更新用户规则数据,更新产品ID的状态

那么问题就明显了,读产品ID的时候是没有事务的,这必然会存在问题的。那么咱们是如何修改的呢?将读取产品ID这件事放入了整个事务中。那么整个流程就变成了:

  1. 从用户数据库查询用户积分是否充足,从规则数据库中查询用户是否符合条件

  2. 事务性的读出符合条件的产品,并马上更新状态,接着完成用户ID和产品ID的关联及减小积分等工做

那这样还有问题吗?依然有,最后仍是超卖了,你们会问为何?这里面咱们犯了另外一个错误,使用代码判断产品的状态而非存储过程,这样即使是在数据库事务内,但没有能够触发数据库事务回滚的条件,因此还会错误的将卖出的产品再次更新为卖出的状态。经历两次惨痛的教训,咱们才逐步的走上正轨,一个地方不会跌倒三次。

 

登堂入室

咱们已经发现了不少问题,最后该怎么解决,咱们决定先解决正确性,再解决速度的问题,咱们使用了一段时间的存储过程加关键ID作成惟一主键的方式,整个秒杀流程的第二部分,就是个完整的存储过程(往事不堪回首,每天被用户骂很是慢)。这个时候惟一能作的就是补充理论知识,发奋图强了。

在这个第二个版本的设计中,咱们开始采用Redis,咱们测试了Redis的pubsub机制,最开始想使用Redis的pubsub进行排队(如今想一想有点幼稚,可是老天帮了我一把,当时鬼使神差的就感受这机制不靠谱)。可是最终的方案嗯,使用了正向队列。何为正向队列?咱们将产品的ID在秒杀开始前,所有读入指定的队列中,秒杀流程就变成了:

  1. 判读Redis队列是否为0,为0结束

  2. 判读用户是否符合规则,是否有足够多的积分

  3. 从队列pop出一个产品ID,若是pop不出来就结束

  4. 开事务,改变产品ID的状态,关联用户ID和产品ID,更新规则和积分

这个时候基本上完全解决了超卖和性能的问题了,可是还会有用户在骂,为何?由于还不够快。

 

渐入佳境

咱们发现为何会慢,由于数据库的事务,回滚虽然少了,可是仍是处理不过来,1s也就那100多个事务能完成,剩下的各类跟不上。此时此刻,咱们直接采购了当时算是比较强劲的数据库服务器,事务量一下提升到了1000tps。可是这远远跟不上用户的增加速度(TT没业务也哭,有业务也哭)。

咱们既然已经发现了排队理论这么有用,咱们决定使用RabbitMQ,延迟处理队列。通过此次改造,咱们秒杀的流程就变成了:

  1. 判断Redis队列是否为0,为0结束

  2. 判读用户是否符合规则,是否有足够积分

  3. 从队列pop出一个产品ID,若是pop不出来就结束

  4. 将用户ID和产品ID放入RabbitMQ中,后面的消费者慢慢的吞下去

这时候用户在速度上算是基本满意了,不过却带来了新的问题。判断用户是否符合规则的时候,因为消费者慢慢的消化而数据库没有实时的更新,致使一个用户能够秒杀多个商品,不少用户就不满意了(TT用户是上帝)。

 

略有小成

咱们再次拿出了强大的Redis,咱们将Redis看成缓存。咱们把秒杀的业务逻辑直接变成了这样:

  1. 先判断Redis的队列是否为0,为0结束

  2. 判断Redis中用户的信息是否符合规则,积分是否符合规则

  3. 从队列pop出一个产品ID,若是pop不出来就马上结束

  4. 马上更新Redis中用户的缓存信息和积分信息,再放入RabbitMQ,让消费者消费

这样看起起来彷佛没什么问题了,可是仍是存在问题的,就是pop出产品ID到更新Redis用户信息的一瞬间仍是能让部分用户钻空子的,毕竟Redis没有MySQL那种强事务机制。

 

心照不宣

在这个阶段,咱们用Erlang的mnesia写了一个Redis特定功能替代品,但使用了段时间很快放弃了,由于咱们找到了更好的解决方式。让RabbitMQ的消费者使用一致性的hash,那么特定的用户必定会落到特定的消费者身上,消费者作去重判断。这样减小了,咱们本身维护基础软件的成本(2个后端工程师TT,别瞎折腾)。

 

为所欲为

当咱们的用户量逐步上升,系统依然出现吃紧和性能跟不上的阶段。

这个时候,咱们大量使用一致性Hash和随机算法,其中过程就变成了。

  1. 将秒杀的产品ID分红多个队列放在Redis集群上,而后将一个产品总数量放在一个Redis上(这个Redis是瓶颈,可是基本上20W的TPS满满的达到了)

  2. 为用户随机一个数字,在必定范围内,直接告诉秒杀失败(纯看运气,纯丢给应用服务器去玩了)

  3. 检查用户规则和用户积分,还有产品总数量,总数量为0,直接结束。

  4. 为用户随机一个产品ID队列,尝试pop,pop不出数据,直接结束(仍是看运气)

  5. 更新用户Redis的缓存和产品总数量的缓存(decr),而后交给RabbitMQ和消费者慢慢处理。

这个时候,基本上30wTPS,随便玩。

 

返璞归真

说了这么多废话,总结下吧。对于秒杀这种业务,优先保稳定和正确,最后才能保服务量。不稳定没得玩,不正确,极可能一单亏死。技术上,我我的认为小厂也能作看似很NB的秒杀只要用好如下几个相关技术:

  1. 削峰,不论是随机丢弃,仍是多层筛选,尽量减小进入核心业务的用户数

  2. 排队,在秒杀场景下,排队不仅仅能够减小系统压力,还能保证正确性

  3. 分区,使用分区能够下降一个节点当机带来总体性的损害或者雪崩性的系统不可用

  4. 最终一致,不少时候,不必定要强一致性,只要能保证最后数据的正确,哪怕是手工修复,都能带来大规模的性能提高

 

转自TechTalk https://www.ttalk.im/topics/18

相关文章
相关标签/搜索