当前位置: 首页 > 后端技术 > Java

秒杀系统设计

时间:2023-04-01 14:09:16 Java

秒杀活动是指网络商家为了促销等目的而组织的网络闪购活动。这种活动具有瞬时并发大、库存小、业务逻辑简单的特点。设计秒杀系统需要考虑的因素很多,比如对现有服务的影响、网络带宽消耗、超卖等。本文将讨论秒杀系统各个环节可能出现的问题及解决方法。秒杀系统秒杀系统的核心难点是并发。如果不考虑并发问题,那么我们可以使用下图所示的简单系统结构来实现秒杀系统。用户只有两个简单的操作:刷新界面和秒杀按钮,服务端只有两个服务接口:返回秒杀界面和处理秒杀逻辑。假设本文有100个秒杀产品,参与秒杀的用户有100万。但是在高并发场景下,这个系统会出现很多问题。我们将在全文中对这些问题一一进行优化。大量用户同时刷新界面,会对服务器带宽造成很大压力;用户可以在秒杀按钮前后重复点击,造成大量不必要的请求;用户可以通过脚本抢购,抢购成功率非常高;服务器接受高并发请求,可能响应太慢或失败;数据库接受高并发请求,会导致连接池耗尽,响应慢;如果数据库更新设计不合理,可能会超卖;秒杀接口CDN秒杀启动前,用户会请求秒杀接口,有的用户甚至会不断刷新秒杀接口,100W用户可能会产生数千万的秒杀接口请求。秒杀界面往往包含大量的静态资源。如果这些接口请求全部通过服务器获取,会造成大量的带宽消耗,甚至导致服务器在秒杀启动前就崩溃了。对于网页等静态资源的并发访问,业界早就有成熟的解决方案:内容分发网络(CDN)。在秒杀启动前,我们可以将网页的静态资源预先存储在CDN节点中,用户在刷新界面时可以直接从CDN获取静态资源,从而减少刷新对服务器造成的压力秒杀界面。加入CDN服务后,大量用户同时访问和刷新秒杀界面不会给服务器带来太大压力。秒杀按钮优化我们知道,秒杀系统中经常会有一个秒杀按钮。如果不限制按钮,可能会出现以下问题:用户在秒杀开始前点击了按钮,导致很多无用的请求;用户在秒杀启动后多次点击按钮,导致多次重复请求;所以我们可以对按钮做一些限制:按钮在秒杀开始前不可用,用户点击一次秒杀按钮后按钮也进入不可用状态。这种方式不能限制通过脚本请求后台的情况,但是可以限制普通用户的多次无效点击,大大减少请求量。秒杀链接优化一般情况下,当用户点击秒杀按钮时,前端会请求一个固定的URL,在前端界面可以找到。对于不懂技术的普通用户来说,这不是问题。如果用户对Http协议稍有了解,可以在秒杀启动前获取URL,在秒杀启动前或启动后的毫秒内请求秒杀链接。给服务器造成很大压力,也会造成不公平:货物都被写脚本的人拿走了。为了避免这种现象,我们可以将URL做成动态的,即使是秒杀系统的开发者也无法知道秒杀开头的URL。具体实现方法是在获取秒杀URL的接口中返回一个服务器生成的随机数,在下单URL中传递该参数完成下单。虽然我上面提到了动态URL可以防止用户在秒杀启动前请求秒杀链接,但是用户仍然可以通过脚本在秒杀启动的那一刻请求秒杀链接,普通用户基本没有办法和秒杀竞争脚本。我们可以引入机器难以识别的验证码。用户请求闪杀链接前需要填写验证码识别结果。验证码错误的请求直接被拒绝。使用验证码不仅可以增加脚本秒杀的难度,还可以降低请求的QPS,因为请求在秒杀的那一刻不再进来,而是分散到填充的时间段验证码。过滤请求通过以上步骤,我们可以减少很多重复请求和脚本请求,并且可以保证一个人在秒杀活动中只会请求一次(脚本还是可以请求多次)。但是100万人参与了秒杀,每人请求一个秒杀链接,近100万个请求,服务器还是处理不了。仔细分析可以发现,秒杀的产品只有100个,最后成功的只有100个。那么我们的100W请求有必要发送到秒杀服务器吗?显然,我们不需要把所有的请求都发送到秒杀服务器,我们只需要保证发送到秒杀服务器的请求超过100个就可以保证秒杀的正常进行,所以我们可以在上面加一层过滤客户端和服务端,过滤层只需要保证100个以上的请求都能到达秒杀服务端即可。我们可以使用Nginx服务器搭建过滤层,即使一台Nginx服务器也无法抗拒100W的请求,我们假设每台Nginx服务器可以处理10W的请求,那么我们需要10台Nginx。那么如何使用它来保证至少可以向后端请求100个请求呢?我们可以简单的让每个Nginx服务器只通过前100个请求,后面的请求直接返回降级接口。通过Nginx过滤,我们可以将100W个请求过滤成1000个请求,大大减轻了服务器端的压力。如果Redis缓存通过了前面的过滤,请求量还是很大的。如果数据库无法处理这些请求,我们需要在数据库之上加一层Redis缓存。单个Redis可以处理数万QPS。如果预估请求的QPS大于几万,我们也可以采用Redis集群的方式来增加Redis的处理能力。当Redis存储和销售相同数量的商品时,秒杀服务需要在每次访问数据库之前从Redis中扣除库存。只有推演成功后,数据库才能继续更新。这样最终对数据库的请求数与要销售的商品数基本一致,可以大大减轻数据库的压力。Redis原子性我们知道Redis是不支持事务的,所以推算可能是负数。这种情况下,我们可以使用Lua脚本来保证推导操作的原子性,从而保证推导结果的正确性。异步更新数据库经过Redis判断后,更新数据库的请求都是必要的请求。这些请求必须由数据库来处理,但是如果数据库仍然无法处理这些请求怎么办?这时候可以考虑削峰填谷操作。削峰填谷的最佳实践是MQ。经过Redis扣库存判断后,我们已经确定了这个请求需要生成订单,我们可以通知订单服务生成订单并异步扣库存。我是狐神,欢迎大家关注我的微信公众号:wzm2zsd本文首发于微信公众号,版权所有,禁止转载!