PHP解决抢购、抽奖等阻塞式⾼并发库存防控超量的思路⽅法
如今在电商⾏业⾥,秒杀抢购活动已经是商家常⽤促销⼿段。但是库存数量有限,⽽同时下单⼈数超过了库存量,就会导致商品超卖甚⾄库存变负数的问题。
⼜⽐如:抢购⽕车票、论坛抢楼、抽奖乃⾄爆红微博评论等也会引发阻塞式⾼并发问题。如果不做任何措施可能在⾼瞬间造成服务器瘫痪,如何解决这个问题呢?
这⾥提出个⼈认为⽐较可⾏的⼏个思路⽅法:
⽅案⼀:使⽤消息队列来实现
可以基于例如MemcacheQ等这样的消息队列,具体的实现⽅案这么表述吧
⽐如有100张票可供⽤户抢,那么就可以把这100张票放到缓存中,读写时不要加锁。当并发量⼤的时候,可能有500⼈左右抢票成功,这样对于500后⾯的请求可以直接转到活动结束的静态页⾯。进去的500个⼈中有400个⼈是不可能获得商品的。所以可以根据进⼊队列的先后顺序只能前100个⼈购买成功。后⾯400个⼈就直接转到活动结束页⾯。当然进去500个⼈只是举个例⼦,⾄于多少可以⾃⼰调整。⽽活动结束页⾯⼀定要⽤静态页⾯,不要⽤数据库。这样就减轻了数据库的压⼒。
⽅案⼆:当有多台服务器时,可以采⽤分流的形式实现
假设有m张票, 有n台产品服务器接收请求,有x个请求路由服务器随机转发
直接给每台产品服务器分配 m/n张票
每台产品服务器内存做计数器,⽐如允许m/n*(1+0.1)个⼈进来。
当内存计数器已满:
后⾯进的⼈, 直接跳到到转到活动结束的静态页⾯,
通知路由服务器,不在路由到这台服务器(这个值得商讨)。
所有产品服务器进来的m/n*(1+0.1)个⼈再全部转发到⼀台付款服务器上,进⼊付款环节,看谁⼿快了,这时候⼈少,加锁什么的就简单的。
⽅案三、如果是单服务器,可以使⽤Memcache锁来实现
product_key 为票的key
product_lock_key 为票锁key
当product_key存在于memcached中时,所有⽤户都可以进⼊下单流程。
当进⼊⽀付流程时,⾸先往memcached存放add(product_lock_key, “1″),
如果返回成功,进⼊⽀付流程。
如果不成,则说明已经有⼈进⼊⽀付流程,则线程等待N秒,递归执⾏add操作。
⽅案四、借助⽂件排他锁
在处理下单请求的时候,⽤flock锁定⼀个⽂件,如果锁定失败说明有其他订单正在处理,此时要么等待要么直接提⽰⽤户"服务器繁忙"
本⽂要说的是第4种⽅案,⼤致代码如下
阻塞(等待)模式:
<?php
$fp = fopen("", "w+");
if(flock($fp,LOCK_EX))
{
//..处理订单
flock($fp,LOCK_UN);
}
fclose($fp);
?>
⾮阻塞模式:
<?php
$fp = fopen("", "w+");
if(flock($fp,LOCK_EX | LOCK_NB))
{
//..处理订单
flock($fp,LOCK_UN);
}
else
{
echo "系统繁忙,请稍后再试";
}
fclose($fp);
?>
可以参考:
www.csdn/article/2014-11-28/2822858
电商的秒杀和抢购,对我们来说,都不是⼀个陌⽣的东西。然⽽,从技术的⾓度来说,这对于Web系
统是⼀个巨⼤的考验。当⼀个Web系统,在⼀秒钟内收到数以万计甚⾄更多请求时,系统的优化和稳定⾄关重要。这次我们会关注秒杀和抢购的技术实现和优化,同时,从技术层⾯揭开,为什么我们总是不容易抢到⽕车票的原因?
⼀、⼤规模并发带来的挑战
在过去的⼯作中,我曾经⾯对过5w每秒的⾼并发秒杀功能,在这个过程中,整个Web系统遇到了很多的问题和挑战。如果Web系统不做针对性的优化,会轻⽽易举地陷⼊到异常状态。我们现在⼀起来讨论下,优化的思路和⽅法哈。
1. 请求接⼝的合理设计
⼀个秒杀或者抢购页⾯,通常分为2个部分,⼀个是静态的HTML等内容,另⼀个就是参与秒杀的Web后台请求接⼝。
通常静态HTML等内容,是通过CDN的部署,⼀般压⼒不⼤,核⼼瓶颈实际上在后台请求接⼝上。这个后端接⼝,必须能够⽀持⾼并发请求,同时,⾮常重要的⼀点,必须尽可能“快”,在最短的时间⾥返回⽤户的请求结果。为了实现尽可能快这⼀点,接⼝的后端存储使⽤内存级别的操作会更好⼀点。仍然直接⾯向MySQL之类的存储是不合适的,如果有这种复杂业务的需求,都建议采⽤异步写⼊。
当然,也有⼀些秒杀和抢购采⽤“滞后反馈”,就是说秒杀当下不知道结果,⼀段时间后才可以从页⾯中看到⽤户是否秒杀成功。但是,这种属于“偷懒”⾏为,同时给⽤户的体验也不好,容易被⽤户认为是“暗箱操作”。
2. ⾼并发的挑战:⼀定要“快”
我们通常衡量⼀个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数),解决每秒数万次的⾼并发场景,这个指标⾮常关键。举个例⼦,我们假设处理⼀个业务请求平均响应时间为100ms,同时,系统内有20台Apache的Web服务器,配置MaxClients为500个(表⽰Apache的最⼤连接数⽬)。
那么,我们的Web系统的理论峰值QPS为(理想化的计算⽅式):
20*500/0.1 = 100000 (10万QPS)
咦?我们的系统似乎很强⼤,1秒钟可以处理完10万的请求,5w/s的秒杀似乎是“纸⽼虎”哈。实际情况,当然没有这么理想。在⾼并发的实际场景下,机器都处于⾼负载的状态,在这个时候平均响应时间会被⼤⼤增加。
就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系统拖垮。
3. 重启与过载保护
如果系统发⽣“雪崩”,贸然重启服务,是⽆法解决问题的。最常见的现象是,启动起来后,⽴刻挂掉。这个时候,最好在⼊⼝层将流量拒绝,然后再将重启。如果是redis/memcache这种服务也挂了,重启的时候需要注意“预热”,并且很可能需要⽐较长的时间。
秒杀和抢购的场景,流量往往是超乎我们系统的准备和想象的。这个时候,过载保护是必要的。如果检测到系统满负载状态,拒绝请求也是⼀种保护措施。在前端设置过滤是最简单的⽅式,但是,这种做法是被⽤户“千夫所指”的⾏为。更合适⼀点的是,将过载保护设置在CGI⼊⼝层,快速将客户的直接请求返回。
⼆、作弊的⼿段:进攻与防守
秒杀和抢购收到了“海量”的请求,实际上⾥⾯的⽔分是很⼤的。不少⽤户,为了“抢“到商品,会使⽤“刷票⼯具”等类型的辅助⼯具,帮助他们发送尽可能多的请求到服务器。还有⼀部分⾼级⽤户,制作强⼤的⾃动请求脚本。这种做法的理由也很简单,就是在参与秒杀和抢购的请求中,⾃⼰的请求数⽬占⽐越多,成功的概率越⾼。
这些都是属于“作弊的⼿段”,不过,有“进攻”就有“防守”,这是⼀场没有硝烟的战⽃哈。
1. 同⼀个账号,⼀次性发出多个请求
部分⽤户通过浏览器的插件或者其他⼯具,在秒杀开始的时间⾥,以⾃⼰的账号,⼀次发送上百甚⾄更多的请求。实际上,这样的⽤户破坏了秒杀和抢购的公平性。
这种请求在某些没有做数据安全处理的系统⾥,也可能造成另外⼀种破坏,导致某些判断条件被绕过。例如⼀个简单的领取逻辑,先判断⽤户是否有参与记录,如果没有则领取成功,最后写⼊到参与记录中。这是个⾮常简单的逻辑,但是,在⾼并发的场景下,存在深深的漏洞。多个并发请求通过负载均衡服务器,分配到内⽹的多台Web服务器,它们⾸先向存储发送查询请求,然后,在某个请求成功写⼊参与记录的时间差内,其他的请求获查询到的结果都是“没有参与记录”。这⾥,就存在逻辑判断被绕过的风险。
应对⽅案:
在程序⼊⼝处,⼀个账号只允许接受1个请求,其他请求过滤。不仅解决了同⼀个账号,发送N个请求的问题,还保证了后续的逻辑流程的安全。实现⽅案,可以通过Redis这种内存缓存服务,写⼊⼀个标志位(只允许1个请求写成功,结合watch的乐观锁的特性),成功写⼊的则可以继续参加。
或者,⾃⼰实现⼀个服务,将同⼀个账号的请求放⼊⼀个队列中,处理完⼀个,再处理下⼀个。
2. 多个账号,⼀次性发送多个请求
很多公司的账号注册功能,在发展早期⼏乎是没有限制的,很容易就可以注册很多个账号。因此,也导致了出现了⼀些特殊的⼯作室,通过编写⾃动注册脚本,积累了⼀⼤批“僵⼫账号”,数量庞⼤,⼏万甚⾄⼏⼗万的账号不等,专门做各种刷的⾏为(这就是微博中的“僵⼫粉“的来源)。举个例⼦,例如微博中有转发抽奖的活动,如果我们使⽤⼏万个“僵⼫号”去混进去转发,这样就可以⼤⼤提升我们中奖的概率。
这种账号,使⽤在秒杀和抢购⾥,也是同⼀个道理。例如,iPhone官⽹的抢购,⽕车票黄⽜党。
应对⽅案:
这种场景,可以通过检测指定机器IP请求频率就可以解决,如果发现某个IP请求频率很⾼,可以给它弹出⼀个验证码或者直接禁⽌它的请求:
1. 弹出验证码,最核⼼的追求,就是分辨出真实⽤户。因此,⼤家可能经常发现,⽹站弹出的验证码,有些是“⿁神乱舞”的样⼦,有时让我们根本⽆法看
清。他们这样做的原因,其实也是为了让验证码的图⽚不被轻易识别,因为强⼤的“⾃动脚本”可以通过图⽚识别⾥⾯的字符,然后让脚本⾃动填写验证码。实际上,有⼀些⾮常创新的验证码,效果会⽐较好,例如给你⼀个简单问题让你回答,或者让你完成某些简单操作(例如百度贴吧的验证码)。
2. 直接禁⽌IP,实际上是有些粗暴的,因为有些真实⽤户的⽹络场景恰好是同⼀出⼝IP的,可能会有“误伤“。但是这⼀个做法简单⾼效,根据实际场景使
⽤可以获得很好的效果。
3. 多个账号,不同IP发送不同请求
所谓道⾼⼀尺,魔⾼⼀丈。有进攻,就会有防守,永不休⽌。这些“⼯作室”,发现你对单机IP请求频率有控制之后,他们也针对这种场景,想出了他们的“新进攻⽅案”,就是不断改变IP。
有同学会好奇,这些随机IP服务怎么来的。有⼀些是某些机构⾃⼰占据⼀批独⽴IP,然后做成⼀个随机代理IP的服务,有偿提供给这些“⼯作室”使⽤。还有⼀些更为⿊暗⼀点的,就是通过⽊马⿊掉普通⽤户的电脑,这个⽊马也不破坏⽤户电脑的正常运作,只做⼀件事情,就是转发IP包,普通⽤户的电脑
被变成了IP代理出⼝。通过这种做法,⿊客就拿到了⼤量的独⽴IP,然后搭建为随机IP服务,就是为了挣钱。
应对⽅案:
php的工作流程说实话,这种场景下的请求,和真实⽤户的⾏为,已经基本相同了,想做分辨很困难。再做进⼀步的限制很容易“误伤“真实⽤户,这个时候,通常只能通过设置业务门槛⾼来限制这种请求了,或者通过账号⾏为的”数据挖掘“来提前清理掉它们。
僵⼫账号也还是有⼀些共同特征的,例如账号很可能属于同⼀个号码段甚⾄是连号的,活跃度不⾼,等级低,资料不全等等。根据这些特点,适当设置参与门槛,例如限制参与秒杀的账号等级。通过这些业务⼿段,也是可以过滤掉⼀些僵⼫号。
4. ⽕车票的抢购
看到这⾥,同学们是否明⽩你为什么抢不到⽕车票?如果你只是⽼⽼实实地去抢票,真的很难。通过多账号的⽅式,⽕车票的黄⽜将很多车票的名额占据,部分强⼤的黄⽜,在处理验证码⽅⾯,更是“技⾼⼀筹“。
⾼级的黄⽜刷票时,在识别验证码的时候使⽤真实的⼈,中间搭建⼀个展⽰验证码图⽚的中转软件服
务,真⼈浏览图⽚并填写下真实验证码,返回给中转软件。对于这种⽅式,验证码的保护限制作⽤被废除了,⽬前也没有很好的解决⽅案。
因为⽕车票是根据⾝份证实名制的,这⾥还有⼀个⽕车票的转让操作⽅式。⼤致的操作⽅式,是先⽤买家的⾝份证开启⼀个抢票⼯具,持续发送请求,黄⽜账号选择退票,然后黄⽜买家成功通过⾃⼰的⾝份证购票成功。当⼀列车厢没有票了的时候,是没有很多⼈盯着看的,况且黄⽜们的抢票⼯具也很强⼤,即使让我们看见有退票,我们也不⼀定能抢得过他们哈。
最终,黄⽜顺利将⽕车票转移到买家的⾝份证下。
解决⽅案:
并没有很好的解决⽅案,唯⼀可以动⼼思的也许是对账号数据进⾏“数据挖掘”,这些黄⽜账号也是有⼀些共同特征的,例如经常抢票和退票,节假⽇异常活跃等等。将它们分析出来,再做进⼀步处理和甄别。
三、⾼并发下的数据安全
我们知道在多线程写⼊同⼀个⽂件的时候,会存现“线程安全”的问题(多个线程同时运⾏同⼀段代码,如果每次运⾏结果和单线程运⾏的结果是⼀样的,结果和预期相同,就是线程安全的)。如果是MyS
QL数据库,可以使⽤它⾃带的锁机制很好的解决问题,但是,在⼤规模并发的场景中,是不推荐使⽤MySQL的。秒杀和抢购的场景中,还有另外⼀个问题,就是“超发”,如果在这⽅⾯控制不慎,会产⽣发送过多的情况。我们也曾经听说过,某些电商搞抢购活动,买家成功拍下后,商家却不承认订单有效,拒绝发货。这⾥的问题,也许并不⼀定是商家奸诈,⽽是系统技术层⾯存在超发风险导致的。
1. 超发的原因
假设某个抢购场景中,我们⼀共只有100个商品,在最后⼀刻,我们已经消耗了99个商品,仅剩最后⼀个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,然后都通过了这⼀个余量判断,最终导致超发。(同⽂章前⾯说的场景)
在上⾯的这个图中,就导致了并发⽤户B也“抢购成功”,多让⼀个⼈获得了商品。这种场景,在⾼并发的情况下⾮常容易出现。
2. 悲观锁思路
解决线程安全的思路很多,可以从“悲观锁”的⽅向开始讨论。
悲观锁,也就是在修改数据的时候,采⽤锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。
虽然上述的⽅案的确解决了线程安全的问题,但是,别忘记,我们的场景是“⾼并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那⾥。同时,这种请求会很多,瞬间增⼤系统的平均响应时间,结果是可⽤连接数被耗尽,系统陷⼊异常。
3. FIFO队列思路
那好,那么我们稍微修改⼀下上⾯的场景,我们直接将请求放⼊队列中的,采⽤FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。看到这⾥,是不是有点强⾏将多线程变成单线程的感觉哈。
然后,我们现在解决了锁的问题,全部请求采⽤“先进先出”的队列⽅式来处理。那么新的问题来了,⾼并发的场景下,因为请求很多,很可能⼀瞬间将队列内
存“撑爆”,然后系统⼜陷⼊到了异常状态。或者设计⼀个极⼤的内存队列,也是⼀种⽅案,但是,系统处理完⼀个队列内请求的速度根本⽆法和疯狂涌⼊队列中的数⽬相⽐。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候还是会⼤幅下降,系统还是陷⼊异常。
4. 乐观锁思路
这个时候,我们就可以讨论⼀下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采⽤更为宽松的加锁机制,⼤都是采⽤带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得⼀个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增⼤CPU的计算开销。但是,综合来说,这是⼀个⽐较好的解决⽅案。
有很多软件和服务都“乐观锁”功能的⽀持,例如Redis中的watch就是其中之⼀。通过这个实现,我们保证了数据的安全。
四、⼩结
互联⽹正在⾼速发展,使⽤互联⽹服务的⽤户越多,⾼并发的场景也变得越来越多。电商秒杀和抢购,是两个⽐较典型的互联⽹⾼并发场景。虽然我们解决问题的具体技术⽅案可能千差万别,但是遇到的挑战却是相似的,因此解决问题的思路也异曲同⼯。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论