电商网站秒杀和抢购的高并发技术实现和优化​

时间:2022-11-11 18:05:42

一、如何防止多个用户同时抢购同一商品,防止高并发同时订购同一商品?

最近双十一抢购系统应用频繁,销量火爆的同时,让人头疼是却是多用户高并发情况下出现的库存问题。

据调查,多个用户同时下单,导致查询和插入不同步,而查询和插入有时间差,导致高并发情况下的库存问题(我的项目大概就是这种情况。首先,for update找出商品信息表,并将其放入全局表-表数组中。当用户扣除余额成功后,更新商品信息表,减去用户订单的数量。数据库使用Mysql,查询商品信息表时被锁定。但是当商品信息表中的数据越来越多时,查询就存在时间差,导致高并发。商品信息表放入变量数组时,执行后的时间差内其他用户也在下单,导致库存问题)。

现在问一个问题。如何防止同一产品同时被多个用户抢购。也就是说,同一秒内只能有一个用户下单。那下面就说说我们实现CRMEB Pro版高并发高性能时的开发思路:首先想到的思路是排队,阻塞队列。

1、update table set num=num-1 where num>1不查直接更新,更新成功代表抢到了

2、把抢购系统分为两步,第一步是下单(即抢购),如果订单成功,立即减少数量并更新表格数据。第二步,付款。在后台写一个程序。半小时不付款,自动删除订单,然后增加数量。这样就可以避免并发。如果一步完成,那么短时间内也会出现并发问题。

3、数据库中可以加行锁

4、可以使用队列+锁表来解决

二、Web系统大规模并发 - 秒杀和抢购的技术实现和优化

电商的秒杀和抢购对我们来说并不是一件陌生的事情。但是,从技术的角度来看,这对Web系统是一个极大的考验。当一个Web系统在一秒钟内收到上万甚至更多的请求时,系统的优化和稳定性就显得非常重要。这次将关注秒杀和抢购的技术实现和优化。同时从技术层面揭开我们总是很难抢到火车票的原因。 ​

1.大规模并发带来的挑战

在过去的工作中,我曾经面对过每秒5w的高并发秒杀项目。在这个过程中,整个Web系统遇到了很多问题和挑战。Web系统如果不优化,很容易陷入异常状态。现在我们来讨论一下优化的思路和方法。

(1)请求接口的合理设计

一个秒杀或者抢购页面,通常分为2个部分,一个是静态的HTML等内容,另一个就是参与秒杀的Web后台请求接口。

通常通过CDN部署静态HTML等内容,所以压力普遍较低,核心瓶颈其实在后台请求接口上。这个后端接口必须能够支持高并发请求。同时,在最短的时间内尽快返回用户的请求结果也是非常重要的。为了尽快实现这一点,接口的后端存储将使用内存级操作。此时,如果继续指向MySQL之类的存储就不太合适,如果有这样复杂的业务需求,推荐异步写入。

电商网站秒杀和抢购的高并发技术实现和优化​


图片来自网络,侵权联系删除

当然,也有一些秒杀和抢购采用“滞后反馈”,也就是说,你此刻并不知道秒杀的结果,过一段时间你才能从页面上看到用户是否秒杀成功。但这种“偷懒”的行为,同时给用户的体验也不好,容易被用户认为是“暗箱操作”。​

(2)高并发的挑战:一定要“快”

我们通常用QPS(每秒请求数)来衡量一个Web系统的吞吐率,这对于解决每秒上万次的高并发场景至关重要。例如,我们假设处理一个业务请求的平均响应时间是100毫秒。同时系统中有20台Apache Web服务器,配置MaxClients为500个(表示Apache的最大连接数)。

那么,我们的Web系统的理论峰值QPS为(理想化的计算方式):

20*500/0.1 = 100000 (10万QPS)

这么一看我们的系统好像很强大,一秒钟可以处理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入口层,快速将客户的直接请求返回。

2.作弊的手段:进攻与防守

秒杀和抢购收到了“海量”的请求,实际上里面的水分是很大的。不少用户,为了“抢“到商品,会使用“刷票工具”等类型的辅助工具,帮助他们发送尽可能多的请求到服务器。还有一部分高级用户,制作强大的自动请求脚本。这种做法的理由也很简单,就是在参与秒杀和抢购的请求中,自己的请求数目占比越多,成功的概率越高。

这些都是属于“作弊的手段”,不过,有“进攻”就有“防守”,这是一场没有硝烟的战斗哈。

(1)同一个账号,一次性发出多个请求

部分用户通过浏览器的插件或者其他工具,在秒杀开始的时间里,以自己的账号,一次发送上百甚至更多的请求。实际上,这样的用户破坏了秒杀和抢购的公平性。

这种请求在某些没有做数据安全处理的系统里,也可能造成另外一种破坏,导致某些判断条件被绕过。例如一个简单的领取逻辑,先判断用户是否有参与记录,如果没有则领取成功,最后写入到参与记录中。这是个非常简单的逻辑,但是,在高并发的场景下,存在深深的漏洞。多个并发请求通过负载均衡服务器,分配到内网的多台Web服务器,它们首先向存储发送查询请求,然后,在某个请求成功写入参与记录的时间差内,其他的请求获取查询到的结果都是“没有参与记录”。这里,就存在逻辑判断被绕过的风险。

电商网站秒杀和抢购的高并发技术实现和优化​


图片来自网络,侵权联系删除

应对方案:

在程序入口处,一个账号只允许接受1个请求,其他请求过滤。不仅解决了同一个账号,发送N个请求的问题,还保证了后续的逻辑流程的安全。实现方案,可以通过Redis这种内存缓存服务,写入一个标志位(只允许1个请求写成功,结合watch的乐观锁的特性),成功写入的则可以继续参加。

电商网站秒杀和抢购的高并发技术实现和优化​

图片来自网络,侵权联系删除

或者,自己实现一个服务,将同一个账号的请求放入一个队列中,处理完一个,再处理下一个。​

(2)多个账号,一次性发送多个请求

很多公司的账号注册功能,在发展早期几乎是没有限制的,很容易就可以注册很多个账号。因此,也导致出现了一些特殊的工作室,通过编写自动注册脚本,积累了一大批“僵尸账号”,数量庞大,几万甚至几十万的账号不等,专门做各种刷的行为(这就是微博中的“僵尸粉“的来源)。举个例子,例如微博中有转发抽奖的活动,如果我们使用几万个“僵尸号”去混进去转发,这样就可以大大提升我们中奖的概率。

这种账号,使用在秒杀和抢购里,也是同一个道理。例如,iPhone官网的抢购,火车票黄牛党。

电商网站秒杀和抢购的高并发技术实现和优化​


图片来自网络,侵权联系删除

应对方案:

这种场景,可以通过检测指定机器IP请求频率就可以解决,如果发现某个IP请求频率很高,可以给它弹出一个验证码或者直接禁止它的请求:


(3)弹出验证码

最核心的追求,就是分辨出真实用户。因此,大家可能经常发现,网站弹出的验证码,有些是“鬼神乱舞”的样子,有时让我们根本无法看清。他们这样做的原因,其实也是为了让验证码的图片不被轻易识别,因为强大的“自动脚本”可以通过图片识别里面的字符,然后让脚本自动填写验证码。实际上,有一些非常创新的验证码,效果会比较好,例如给你一个简单问题让你回答,或者让你完成某些简单操作(例如百度贴吧的验证码)。

(4)直接禁止IP

实际上是有些粗暴的,因为有些真实用户的网络场景恰好是同一出口IP的,可能会有“误伤“。但是这一个做法简单高效,根据实际场景使用可以获得很好的效果。​

(5)多个账号,不同IP发送不同请求

所谓道高一尺,魔高一丈。有进攻,就会有防守,永不休止。这些“工作室”,发现你对单机IP请求频率有控制之后,他们也针对这种场景,想出了他们的“新进攻方案”,就是不断改变IP。​

电商网站秒杀和抢购的高并发技术实现和优化​


图片来自网络,侵权联系删除

有同学会好奇,这些随机IP服务怎么来的。有一些是某些机构自己占据一批独立IP,然后做成一个随机代理IP的服务,有偿提供给这些“工作室”使用。还有一些更为黑暗一点的,就是通过木马黑掉普通用户的电脑,这个木马也不破坏用户电脑的正常运作,只做一件事情,就是转发IP包,普通用户的电脑被变成了IP代理出口。通过这种做法,黑客就拿到了大量的独立IP,然后搭建为随机IP服务,就是为了挣钱。

应对方案:

说实话,这种场景下的请求,和真实用户的行为,已经基本相同了,想做分辨很困难。再做进一步的限制很容易“误伤“真实用户,这个时候,通常只能通过设置业务高门槛来限制这种请求了,或者通过账号行为的”数据挖掘“来提前清理掉它们。

僵尸账号也还是有一些共同特征的,例如账号很可能属于同一个号码段甚至是连号的,活跃度不高,等级低,资料不全等等。根据这些特点,适当设置参与门槛,例如限制参与秒杀的账号等级。通过这些业务手段,也是可以过滤掉一些僵尸号。

(6)火车票的抢购

看到这里,同学们是否明白你为什么抢不到火车票?如果你只是老老实实地去抢票,真的很难。通过多账号的方式,火车票的黄牛将很多车票的名额占据,部分强大的黄牛,在处理验证码方面,更是“技高一筹“。

高级的黄牛刷票时,在识别验证码的时候使用真实的人,中间搭建一个展示验证码图片的中转软件服务,真人浏览图片并填写下真实验证码,返回给中转软件。对于这种方式,验证码的保护限制作用被废除了,目前也没有很好的解决方案。

电商网站秒杀和抢购的高并发技术实现和优化​


图片来自网络,侵权联系删除

因为火车票是根据身份证实名制的,这里还有一个火车票的转让操作方式。大致的操作方式,是先用买家的身份证开启一个抢票工具,持续发送请求,黄牛账号选择退票,然后黄牛买家成功通过自己的身份证购票成功。当一列车厢没有票了的时候,是没有很多人盯着看的,况且黄牛们的抢票工具也很强大,即使让我们看见有退票,我们也不一定能抢得过他们哈。​

电商网站秒杀和抢购的高并发技术实现和优化​

图片来自网络,侵权联系删除

最终,黄牛顺利将火车票转移到买家的身份证下。

这种情况并没有很好的解决方案,唯一可以动心思的也许是对账号数据进行“数据挖掘”,这些黄牛账号也是有一些共同特征的,例如经常抢票和退票,节假日异常活跃等等。将它们分析出来,再做进一步处理和甄别。​

3.高并发下的数据安全

我们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,如果每次运行结果和单线程运行的结果是一样的,结果和预期相同,就是线程安全的)。如果是MySQL数据库,可以使用它自带的锁机制很好的解决问题,但是,在大规模并发的场景中,是不推荐使用MySQL的。

秒杀和抢购的场景中,还有另外一个问题,就是“超库存”,如果在这方面控制不慎,会产生发送过多的情况。我们也曾经听说过,某些电商搞抢购活动,买家成功拍下后,商家却不承认订单有效,拒绝发货。这里的问题,也许并不一定是商家奸诈,而是系统技术层面存在超发风险导致的。

(1)超过库存的原因假设在一个购买场景中,我们总共只有100件商品。最后时刻,我们已经消耗了99件物品,只剩下最后一件。此时系统发送了多个并发请求,都是读取99个产品剩余,然后都通过了这个剩余判断,最终导致单量超过库存。(与文章前面提到的场景相同)

电商网站秒杀和抢购的高并发技术实现和优化​

图片来自网络,侵权联系删除

在上面的这个图中,就导致了并发用户B也“抢购成功”,多让一个人获得了商品。这种场景,在高并发的情况下非常容易出现。

(2)悲观锁思路解决线程安全的思路很多,可以从“悲观锁”的方向开始讨论。
悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。

电商网站秒杀和抢购的高并发技术实现和优化​


图片来自网络,侵权联系删除

虽然上面的方案确实解决了线程安全的问题,但是别忘了我们的场景是“高并发”。也就是说,会有很多这样的修改请求,每一个都需要等待“锁”。一些线程可能永远也不会有机会获得这个“锁”,这样的请求会死在那里。同时会有很多这样的请求,会瞬间增加系统的平均响应时间。结果可用连接的数量将被耗尽,系统将陷入异常。

(3)FIFO队列思路让我们稍微修改一下上面的场景。我们直接将请求放入队列中,采用FIFO(先入先出)。这样,我们就不会导致一些请求永远获取不到锁。看到这里,是不是有点把多线程强行变成单线程的感觉?

电商网站秒杀和抢购的高并发技术实现和优化​

图片来自网络,侵权联系删除

然后,我们现在解决了锁的问题,所有请求都在“先进先出”队列中处理。那么新的问题来了。在高并发的场景下,队列内存很可能因为请求多而瞬间“爆仓”,然后系统会再次陷入异常状态。或者设计一个巨大的内存队列也是一个解决方案。但是,系统能够完成一个队列中请求的速度,是无法和疯狂涌入队列的数量相比的。也就是说,队列中的请求会越积越多,最后Web系统的平均响应时间会急剧下降,系统仍然会陷入异常。

(4)乐观锁思路

这时候就可以讨论“乐观锁定”的思路了。乐观锁采用比“悲观锁”更宽松的锁机制,大部分都是用版本号更新的。也就是说,该数据的所有请求都有资格被修改,但是将获得该数据的版本号。只有版本号符合的才能更新成功,其他的都返回失败。这样我们就不需要考虑队列问题,不过会增加CPU的计算开销。但总的来说,这是一个更好的解决方案。

电商网站秒杀和抢购的高并发技术实现和优化​

图片来自网络,侵权联系删除

有很多软件和服务都有“乐观锁”功能的支持,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。

总结:

本文讨论了商城项目高并发服务在面对大流量时的一些技术手段和应对要点。当然,实际的在线服务要比现在的复杂。这里只是一些建议。希望能保持敬畏之心,在高并发的道路上继续探索。做出更好的互联网商城应用。