秒杀系统实现分析

秒杀的特点

再讲怎么实现之前先说说秒杀有什么特点。

秒杀的场景比较特别,一般都会在一些集中的时间点对某些商品进行抢购。

我们先来考虑下常规的购买流程,浏览商品——加入购物车——下单——减库存——支付——支付完成。而多数人的操作其实都是浏览商品,加入购物车,下单购买的占少数。而秒杀的场景,其实大家已经省略掉了前面的操作,直接选择好了商品,就要直接下单、支付了。而且买的还都是同一个商品。这块问题就来了,非常多的人全都来操作这一部分集中的业务数据,对系统的冲击之大可想而知。

比如一般我们来处理库存的方式,需要对数据库加行级锁,然后减库存(同时需要确认库存数量,不能超售),然后释放。一般情况还好,因为大家都在购买不同的商品,所以锁加在不同的记录上面。但是在秒杀场景中,锁需要加载同一个商品上面,当大量请求过来,所有的请求都在等待给这个记录加锁,结果会多惨可想而知。

秒杀的优化

请求尽量拦截在系统上游

从客户端到负载服务,再到业务服务,再到数据库,我们要尽量能把请求拦截到前面就不要往后放。

总体来说的思路有限流、消峰、排队等。

客户端层

记得之前红包也发了个新年版的微信,就是把很多静态资源提前缓存到了客户端,到时候没有的请求,有的就不用请求了。优化了高峰秒杀的服务压力。另外对于商品秒杀,也可以把页面缓存,一些不关键业务静态化,比如商品评价、相关推荐等,减少服务器压力。

记得去年前年的春晚微信红包,摇一摇有机会抽到红包。但是大家都在摇,每个人摇都给你去系统抽一下,系统压力好大,如果你摇三次给你发一次请求呢,是不是系统的压力下降到了1/3?

再举个例子,12306,大家刷票,为什么平时使用的时候一点事没有,而春节、国庆抢票就很难。其实人家已经优化的很好了,但是还是会有很大压力,为什么,就是大家不停的在刷新。想想是不是春节的时候一直在刷票,不管是手动还是软件刷,人家一直查压力很大啊。于是页面上显示几秒才能重新查询一次(但是这样也限制不了外挂党),这样有效降低了服务器压力。

其实很多地方的查询功能也是类似,点了查询,会有个倒计时,几秒之后才能查,按钮置灰。这样可以防止不停的发送请求。尤其是12306这种,查询结果出的稍微一慢,用户还会觉得没查出来,就会不停的点击增加服务器负担。

上面举的两个例子,都是拦截到了客户端(应用、浏览器)层面,但是如果有人用外挂,或者直接写脚本发请求呢(想起月饼时事件)?正常用户也会有很多穿透客户端的,下面就需要在nginx这类服务器层面去做拦截了。(也可能lvs)

负载服务层

对于恶意用户,假如我们客户端限制的是3秒能发一次请求,我们在负载服务一层,限制超过2秒一次的请求,把这个uid给他封锁一定时间,对于短期秒杀场景直接全站封禁也可以。做的松一点就是3秒只放过1个请求,而不封锁帐号。

(这里有个小tip给大家,出现这种异常的情况,我们可以认为就是非法用户了,直接封了也无所谓,但是文案的提示尽量不要说你的帐号被封了什么的,给他一个和一般用户一样的没抢到什么的提示最好。不然写脚本的人会一点点摸索你的频率,伪装成正常用户。)

如果不考虑封锁帐号,也可以增加负载层面的缓存,对于一定时间内的请求,都返回第一次请求的结果,而不去请求后端服务数据。

在大量请求时为了让缓存的结果或者计数的结果尽量一致,可以按UID来映射到具体服务,保证请求得到的结果一致。(对于内存计数,希望同一UID落到一个服务上,缓存结果也是如此)

这里另说一句,千万不要觉得,我们一个nginx负载多少,到时候真出现了大量秒杀的情况,预估多少量,除以目前单nginx负载,就是我们需要的nginx数量。对于单机部署多个nginx或者多nginx协调的情况,都是可能影响性能的,平均响应时间也会变长,需要实际测试出拐点才是正确的做法。

业务服务层

对于秒杀,我举个我们这里实际遇到的例子。我们有红包的业务,对于抢红包的用户来说,就是个秒杀场景。比如发一次红包,可以20个人抢,那么透过数据库的就没有必要太多,一般会做多几个的冗余量(比如5个),这样其他的请求就没必要去与数据库交互了,直接快速返回没抢到就好了。透过去的这25个再进行数据库的行级锁进行减少库存等相关操作。对于上百万千万的请求来说,这25个请求就并不是很多了,数据库也可以轻松抗住。这里还要注意,要使用乐观锁减库存,进一步减少超卖可能。

加锁的时候检查其实会更好,多出来的5个就不会处理了。

为什么要多放一些呢,假如在进行减库存等相关操作出错了,不要重放,直接抛弃让后面的补上就好了。重放还是复杂了些,同时也符合fail fast规则。

这里具体的实现技术,使用缓存、队列都可以完成,后面说缓存,队列的话不管是用哪种中间件,比如mq、redis队列,都可以轻松搞定。当然对于关键大型的抢购业务,也可以放很多,比如1w商品,放2w入队列。不可能一半都失败吧。

业务上其实也可以配合一些优化,比如很多东西并不需要精细化的显示,就可以粗劣的显示一下,比如红包的剩余个数,可以只显示还有没有,而没必要显示还剩几个(当然也可以显示,但是业务上接受具体数值显示不准的情况)。

数据库

到数据库这一次的压力其实已经很小了,只需要保证数据的准确性就可以了。

对于大量商品同时秒杀的情况,可以按照商品id进行分表或者分库,提升数据库的性能。

多用缓存

在秒杀优化中,一定要充分的利用缓存这一工具。推荐的缓存工具使用redis。

比如在负载服务的时候,可以使用redis作为通用计数器。业务服务的地方,使用redis的队列,或者配合队列做计数器,把超过25个的请求全部快速返回没有抢到。

另外对于读的业务,尽量多的使用缓存,上面提到过,如果是商品页的话,完全可以把整个页面的内容都缓存起来,这里推荐页面的缓存可以在nginx上做。

对于缓存服务挂了的情况,重启一定要记得预热,不过在高并发的秒杀业务下,我们基本上是不允许服务挂了的情况,所以一定要做好服务保护,当过载过高的时候可以抛弃请求,不要让服务挂了。

异步处理

对于订单生成、物流状态等信息,完全可以交给队列后面的服务去处理,并不需要实时处理。把一些不着急的业务逻辑放到异步队列后面处理也是优化的核心。

尽量让http请求能够快速返回。我们的抢红包业务,由于服务与客户端维持了长连接,所以都快速返回了,然后抢到的和一小部分多放过去的量,是通过异步返回的结果,其他的请求都是http快速返回没抢到的结果。

用户体验

秒杀优化的过程中,不能丢失了正常用户的体验。

可以异步化处理一些业务,比如我们上面提到的抢红包业务,虽然抢到的结果是异步发送的,但是速度也不会太久,用户体验就不会太差。

对于商品的抢购,如果具体结果异步处理,可以先来个中间页面,提示用户的抢购结果,或者提示个抢购中。这块最早小米就做了,但是总是抢购中,然后没抢到,大家心里也不是很开心。但是总比啥都没有卡在那好。

最后说点别的

对于一般的企业,如果预估秒杀会比较激烈的话,一定要提到带宽,如果是云服务的话,建议新弄一组机器来做这块的业务,降低对正常业务的影响。

多做压测总是好的。

可以针对秒杀场景做些简化需求,没必要让他和一般商品销售需求一致,尽可能的砍掉更多的不必要功能,保证服务正常运行。

特殊情况可以增加验证机制,比如苹果使用了最烦人的,需要下单用户给苹果发短信,然后获取短信验证码,才能预定店里拿手机。不排除会被刷,但是谁叫人家厉害呢,你不买别人买。但是我们的验证服务尽量还是不要影响到用户的下单流程。

多做提前需求准备,比如建议用户提前把东西加入购物车或者增加一键购这样的功能。建议用户提前选择好收货地址等等。

做倒计时,秒杀一般都是从某一时间点准时开始的,做个倒计时,可以方式用户总是刷新看开枪没,一直重新访问页面也很消耗服务器性能呀。

很多网站初期对于用户的注册还不是很在意,结果出了很多僵尸用户,这些帐号在秒杀的时候被某些工作室用来刷接口。这个建议想办法把这部分用户区别出来。然后只要是这部分用户想要秒杀,需要验证手机号等复杂操作才能进行,防止工作室僵尸用户刷接口秒杀。

本文原创于赵伊凡BLOG转载请注明出处,超过3000字全原创手敲实在不容易。

到此秒杀就说的差不多了,当然秒杀还会涉及到很多东西,这也需要开发人员一步一步的成长,再去提升自己系统的稳定性。数据库扩容,分库、更多的缓存等优化都是需要慢慢进行的。

©原创文章,转载请注明来源: 赵伊凡's Blog
©本文链接地址: 秒杀系统实现分析

“秒杀系统实现分析”的3个回复

发表评论

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