尊重原创来源于:石头大V 今日头条前端
在网上购物,秒抢某个商品,好比说小米手机,这对咱们来讲都不陌生。这些看似很简单的东西从技术的角度来讲对于Web系统是一个巨大的考验,一个Web系统,在很短期内收到不少请求时,系统的优化和稳定相当重要,咱们就来详细解释一下这些问题。
redis
一、大规模并发带来的挑战数据库
好比说5w每秒的高并发秒杀功能,在这个过程当中,整个Web系统遇到了不少的问题和挑战。若是Web系统不作针对性的优化,会垂手可得地陷入到异常状态。一块儿来讨论下优化的思路和方法。后端
1.一、请求接口的合理设计浏览器
一个抢购页面,一般分为2个部分,一个是静态的HTML等内容,另外一个就是Web后台请求接口。一般静态HTML等内容,是经过CDN的部署,通常压力不大,核心瓶颈实际上在后台请求接口上。这个后端接口,必须可以支持高并发请求,同时必须尽量“快”,在最短的时间里返回用户的请求结果。为了实现尽量快这一点,接口的后端存储使用内存级别的操做会更好一点,仍然直接面向MySQL之类数据库的存储是不合适的,若是有这种复杂业务的需求,都建议采用异步写入。安全
1.二、高并发的挑战服务器
衡量一个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数),解决每秒数万次的高并发场景,这个指标很是关键。假设处理一个业务请求平均响应时间为100ms,同时系统内有20台Apache的Web服务器,配置MaxClients为500个(表示Apache的最大链接数目)。那么Web系统的理论峰值QPS为(理想化的计算方式):20*500/0.1 = 100000 (10万QPS) ,系统彷佛很强大,1秒钟能够处理完10万的请求,实际状况固然没有这么理想。在高并发的实际场景下,机器都处于高负载的状态,在这个时候平均响应时间会被大大增长。就Web服务器而言,Apache打开了越多的链接进程,CPU须要处理的上下文切换也越多,额外增长了CPU的消耗,而后就直接致使平均响应时间增长。所以上述的MaxClient数目,要根据CPU、内存等硬件因素综合考虑,绝对不是越多越好。能够经过Apache自带的abench来测试一下,取一个合适的值。而后,咱们选择内存操做级别的存储的Redis,在高并发的状态下,存储的响应时间相当重要,不考虑网络带宽和负载均衡问题。假设系统,在5w/s的高并发状态下,平均响应时间从100ms变为250ms(实际状况,甚至更多):20*500/0.25 = 40000 (4万QPS)因而系统剩下了4w的QPS,面对5w每秒的请求,中间相差了1w。 举个通俗例子说明,收费站1秒钟来5部车,每秒经过5部车,收费站运做正常。忽然这个收费站1秒钟只能经过4部车,车流量仍然依旧,结果一定出现大塞车。(5条车道突然变成4条车道的感受)同理某一个秒内,20*500个可用链接进程都在满负荷工做中,却仍然有1万个新来请求,没有链接进程可用,系统陷入到异常状态也是预期以内。其实在正常的非高并发的业务场景中,也有相似的状况出现,某个业务请求接口出现问题,响应时间极慢,将整个Web请求响应时间拉得很长,逐渐将Web服务器的可用链接数占满,影响其余正常的业务请求,无链接进程可用。更严重的是用户的行为,系统越是不可用,用户的点击越频繁,恶性循环最终致使“雪崩”(其中一台Web机器挂了,致使流量分散到其余正常工做的机器上,再致使正常的机器也挂,而后恶性循环),将整个Web系统拖垮。网络
1.三、重启与过载保护多线程
若是系统发生“雪崩”,贸然重启服务,是没法解决问题的。这种状况最好在入口层将流量拒绝,而后再将重启,若是是redis/memcache这种服务也挂了,重启的时候须要注意“预热”,而且极可能须要比较长的时间。秒杀和抢购的场景,流量每每是超乎系统的准备和想象的。这个时候过载保护是必要的。若是检测到系统满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,可是,这种作法是会被客户骂的,更合适的解决方案是将过载保护设置在CGI入口层,快速将客户的直接请求返回。并发
二、做弊的常见手段
秒杀和抢购收到了海量的请求,实际上水分是很大的。很多用户为了抢到商品,会使用刷票软件等类型的辅助工具,因此就会发送尽量多的请求到服务器。还有一部分用户,本身制做强大的自动请求脚本,这些都是属于“做弊的手段”,不过有“进攻”就有“防守”。
2.1 、同一个帐号,一次性发出多个请求
部分用户经过浏览器的插件或者其余工具,在秒杀开始的时间里,以本身的帐号一次发送上百甚至更多的请求。这样的用户破坏了秒杀和抢购的公平性,这种请求在某些没有作数据安全处理的系统里,也可能形成另一种破坏,致使某些判断条件被绕过。如一个简单的领取逻辑,先判断用户是否有参与记录,若是没有则领取成功,最后写入到参与记录中。这是个很是简单的逻辑,可是在高并发的场景下,存在深深的漏洞,多个并发请求经过负载均衡服务器,分配到内网的多台Web服务器,它们先向存储发送查询请求,而后在某个请求成功写入参与记录的时间差内,其余的请求获查询到的结果都是“没有参与记录”。这就存在逻辑判断被绕过的风险。
应对方案:在程序入口处,一个帐号只容许接受1个请求,其余请求过滤,不只解决了同一个帐号发送N个请求的问题,还保证了后续的逻辑流程的安全。
2.二、多个帐号,一次性发送多个请求
帐号注册功能在发展早期几乎是没有限制的,很容易就能够注册不少个帐号。所以也致使了出现了一些特殊的工做室,经过编写自动注册脚本,积累了一大批“僵尸帐号”,数量庞大,几万甚至几十万的帐号不等,专门作各类刷行为(这就是传说中的“僵尸粉“)。好比有转发抽奖的活动,若是使用几万个“僵尸号”去混进去转发,这样就能够大大提高中奖的几率,使用在秒杀和抢购也是同一个道理,好比iPhone官网的抢购,火车票黄牛党也是如此。
应对方案:这种场景能够经过检测指定机器IP请求频率就能够解决,若是发现某个IP请求频率很高,能够给它弹出一个验证码或者直接禁止它的请求:弹出验证码,目的就是分辨出真实用户。网站弹出的验证码,都是随机的样子,有时没法看清,这样作的缘由,其实也是为了让验证码的图片不被轻易识别,由于强大的“自动脚本”能够经过图片识别里面的字符,而后让脚本自动填写验证码。如今有一些新方法效果会比较好,如给你一个简单问题让你回答,或者让你完成某些简单操做(例如百度贴吧的验证码),这个作法简单高效,效果也很好。
2.三、多个帐号,不一样IP发送不一样请求
有进攻,就会有防守,这些“工做室”,发现你对单机IP请求频率有控制以后,他们也针对这种场景,想出了他们的“新进攻方案”,就是不断改变IP,这些随机IP服务怎么来的,有一些是某些机构本身占据一批独立IP,而后作成一个随机代理IP的服务,有偿提供给这些“工做室”使用。还有一些就是经过木马黑掉普通用户的电脑,这个木马也不破坏用户电脑的正常运做,只作一件事情,就是转发IP包。经过这种作法黑客就拿到了大量的独立IP,而后搭建为随机IP服务,赚了不少黑心钱。
应对方案: 这种状况一般只能经过设置业务门槛高来限制这种请求了,或者经过帐号行为的”数据挖掘“来提早清理掉它们。僵尸帐号也仍是有一些共同特征的,例如帐号极可能属于同一个号码段甚至是连号的,活跃度不高,等级低,资料不全等等。根据这些特色,适当设置参与门槛,例如限制参与秒杀的帐号等级。经过这些业务手段,也是能够过滤掉一些僵尸号。
2.四、火车票的抢购
看到这里,你是否明白为何抢不到火车票,若是你只是老老实实地去抢票,真的很难。经过多帐号的方式黄牛将不少车票的名额占据,部分牛逼的黄牛在处理验证码方面更是“技高一筹“,高级的黄牛刷票时,在识别验证码的时候使用真实的人,中间搭建一个展现验证码图片的中转软件服务,真人浏览图片并填写下真实验证码,返回给中转软件。对于这种方式,验证码的保护限制做用被废除了,目前也没有很好的解决方案。由于火车票是根据身份证明名制的,除比以外还有一个火车票的转让操做方式,就是先用买家的身份证开启一个抢票工具,持续发送请求,黄牛帐号选择退票,而后黄牛买家成功经过本身的身份证购票成功,由于黄牛们的抢票工具也很强大,即便让咱们看见有退票,咱们也抢不过他们,最终黄牛顺利将火车票转移到买家的身份证下。
什么是服务器的高并发?解决方案:并无很好的解决方案,惟一能够动心思的也许是对帐号数据进行“数据挖掘”,这些黄牛帐号也是有一些共同特征的,例如常常抢票和退票,节假日异常活跃等等。将它们分析出来,再作进一步处理和甄别。
三、高并发下的数据安全
多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,若是每次运行结果和单线程运行的结果是同样的,结果和预期相同,就是线程安全的)。若是是MySQL数据库,可使用它自带的锁机制很好的解决问题,可是在大规模并发的场景中,是不推荐使用MySQL的。秒杀和抢购的场景中,还有另一个问题,就是“超发”,若是在这方面控制不慎,会产生发送过多的状况,好比某些电商搞抢购活动,买家成功拍下后,商家却不认可订单有效,拒绝发货。问题也许并不必定是商家奸诈,而是系统技术层面存在超发风险致使的。
3.一、超发的缘由
假设某个抢购场景中,一共只有100个商品,在最后一刻,咱们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,而后都经过了这一个余量判断,最终致使超发。这就致使了并发用户B也“抢购成功”,多让一我的得到了商品。这种场景在高并发的状况下很是容易出现。
3.二、悲观锁思路
悲观锁也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改,遇到加锁的状态,就必须等待,虽然上述的方案的确解决了线程安全的问题,可是咱们的场景是“高并发”,也就是说会不少这样的修改请求,每一个请求都须要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时这种请求会不少,瞬间增大系统的平均响应时间,结果是可用链接数被耗尽,系统陷入异常。
3.三、FIFO队列思路
直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话就不会致使某些请求永远获取不到锁。看到这里是否是有点强行将多线程变成单线程的感受哈,如今解决了锁的问题,所有请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,由于请求不少,极可能一瞬间将队列内存“撑爆”,而后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,系统处理完一个队列内请求的速度根本没法和疯狂涌入队列中的数目相比。也就是说队列内的请求会越积累越多,最终Web系统平均响应时候仍是会大幅降低,系统仍是陷入异常。
3.四、乐观锁思路
乐观锁是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。这个数据全部请求都有资格去修改,但会得到一个该数据的版本号,只有版本号符合的才能更新成功,其余的返回抢购失败。这样的话咱们就不须要考虑队列的问题,不过它会增大CPU的计算开销。可是综合来讲,这是一个比较好的解决方案。有不少软件和服务都“乐观锁”功能的支持,经过这个功能能够保证数据的安全。
四、随着互联网的用户愈来愈多,高并发的场景也变得愈来愈多,电商秒杀和抢购是两个比较典型的高并发场景,虽然遇到的挑战多,可是咱们不怕,由于咱们一直在努力。