笨法玩秒杀

赞助

如果你觉得我写得还行,并且愿意付费,那么我会更有动力写下去。

秒杀无异于一场自找的DDoS攻击,从这个角度来说:玩秒杀的电子商务网站,和那些不停喊着用力打我的受虐狂没有什么两样,因为他们都痛并快乐着。

在「中国数据库技术大会」上,淘宝分享了「秒杀场景下MySQL的低效」,详细分析了秒杀的技术难点及改进措施,简而言之,主要就是在高并发事务请求的情况下,数据库性能由于死锁检测等因素直线下降,在这种场景下,单纯的关闭死锁检测虽然可以提升一定的性能,但这顶多是治标而已,如何治本?淘宝给出来两个改进方法:

  • 请求排队:如果请求一股脑的涌入数据库,势必会由于争抢资源造成性能下降,通过排队,让请求从混沌到有序,从而避免数据库在协调大量请求时过载。
  • 请求合并:甲买了一个商品,乙也买了同一个商品,与其把甲乙当做当做单独的请求分别执行一次商品库存减一的操作,不如把他们合并后统一执行一次商品库存减二的操作,请求合并的越多,效率提升的就越大。

可惜的是淘宝的这些改进方法都是通过修改MySQL源代码在数据层实现的,对芸芸众生的我们而言,简直是一个无法逾越的技术门槛!那么是否可以在应用层实现呢?

请求排队

通过Redis实现队列是一件很简单的事情,使用LIST或者ZSET就可以搞定,如果没有优先级之类需求的话,通常LIST是一个更好的选择,因为它的时间复杂度更低,当然,如果处理队列的速度足够快,那么ZSET也不错。

BTW:关于Redis队列的介绍可以参考我以前写的「Redis消息通知系统的实现」。

把请求保存到队列里之后,可以通过Gearman实现Worker来消费队列,请求的生产和消费是异步的,所以不会出现并发拥堵,但是可能发生延迟,如果出现这种情况,可以通过增加Worker的数量可以加快消费队列的速度。

BTW:关于Gearman Worker的介绍可以参考我以前写的「管理Gearman」。

让我们从头捋捋:程序收到请求,然后把请求保存到Redis队列里,Gearman通过Worker处理队列里的请求,可是处理完之后如何通知程序呢?因为整个过程是异步的,所以除非程序支持某种形式的回调,否则很难通知。

最容易想到的解决办法是在程序里通过轮询来查询请求是否已经处理完成,但这无疑会增加数据库的负载,同时程序的实时性也会大打折扣。好在我们有其它的方法,比如说Redis提供了名为BLPOPBRPOP的方法,它尝试从一个LIST里取元素,如果LIST为空则会堵塞连接,利用这个特性我们可以实现一个简易的通知功能:程序把请求保存到Redis队列里,然后调用BLPOP或BRPOP方法等通知,因为此时LIST为空,所以会堵塞连接,与此同时Gearman的Work处理完队列里的请求后,往LIST里保存一个状态码,程序感知到这个状态码,并通过状态码判断出请求是成功还是失败。

BTW:类似的,利用Redis的BRPOPLPUSH方法,还能实现一些有趣的功能。

整个过程中有一些需要注意的地方,比如说因为BLPOP和BRPOP都属于堵塞性质的操作,所以一旦队列处理速度跟不上,程序就会堆积大量连接,这可能会引起很多连锁问题:一方面可能导致内存不足,以PHP为例,一个连接通常占用10M左右,堆积一千个连接的话,10G内存就没有了;另一方面大量的连接可能耗尽端口资源,具体取决于内核参数「net.ipv4.ip_local_port_range」。此时提高处理队列的速度是唯一的出路。

请求合并

把类似的请求合并起来是一件既简单又复杂的事情,介于本文的标题是笨法玩秒杀,我们就挑简单的说,当我们通过Gearman的Work去处理队列里的请求时,通常是弹出一个请求处理一个请求,下面我们做出一些调整,每次不再只从队列里弹出一个请求,取而代之,我们一次性从队列里取出多个请求,然后在程序里完成合并后再执行。当然这里有很多细节问题,由于篇幅关系就不多说了。

希望本文能起到一个抛砖引玉的作用,大家如果有什么好点子,不妨分享一二。

笨法玩秒杀》上有7条评论

  1. 我之前做的时候 ,没有引入redis,直接使用gearman分发任务把数据写入DB,后来再使用redis的blpop来通知用户,redis主要用作缓存了,呵呵

  2. 个人觉得本文和秒杀关系不大。redis队列+gearman分布式 处理后台业务常规方案。只是在扫描处理结果时候,不是简单一次次的轮询,而是利用Blpop,每次轮询都有效果,减少结果存储队列的鸭梨。

  3. Pingback引用通告: 第四部 » 博客推荐9:火丁笔记(原老王的技术手册)

  4. 关于用BLPOP,在并发情况下,通知的结果是不能保证顺序的,也就是A用户的BLPOP可能拿到的是B的通知结果,这个你怎么处理?还是说你给每个用户单独开一个LIST,但这样消耗会不会很大?

    • 通知里面可以带上一些参数来区分,
      用户id,订单id,状态。
      不用操心顺序问题,拿到通知根据通知的参数体,去处理就行了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注