【.com原稿】说到“秒杀”,恐怕大多数人都会想到“双11”、“促销”、“买买买”和其他火爆场面吧。图片来自Pexels每个人都涌向打折产品,在电子商务网站上造成了一片热闹景象。但是作为程序员,我们看到的是它背后的高并发性和可靠性。无论你处于软件开发的哪个阶段,都希望能够设计出属于自己的秒杀系统。今天我们就来看看秒杀系统的架构设计需要考虑哪些方面:秒杀场景的特点系统隔离的设计思路客户端设计代理层设计应用层设计数据库设计压力测试总结秒杀场景的特点秒杀场景是电商网站定期举办活动。本次活动有明确的起止时间,提前定义好参与互动的商品,参与秒杀的商品数量也有限制。同时提供秒杀入口,用户可以通过该入口进行抢购。总结一下秒杀场景的特点:当秒杀在固定时间启动时,大量用户会在秒杀过程中同时抢购同一款商品,网站流量会瞬间增加。库存有限,秒杀订单数量远大于库存数量,只有小部分用户能够秒杀成功。操作可靠,秒杀业务流程相对简单。一般是下单减少库存。库存是用户争夺的“资源”。实际消耗的“资源”不能超过计划出售的“资源”,即不能“超卖”。系统隔离设计思路分析秒杀的特点后,我们发现秒杀活动是有计划的,短时间内会爆发大量的请求。为了不影响现有业务系统的正常运行,我们需要将其与现有系统进行隔离。即使秒杀活动出现问题,也不会对现有系统造成影响。隔离的设计思路可以从三个维度考虑。业务隔离技术隔离数据库隔离业务隔离既然秒杀是一种活动,那么它肯定不同于常规业务。我们可以把它看成一个单独的项目。活动开始前,最好先设计一个“热场”。“热点”的形式多种多样,比如:分享活动拿优惠券、抢名额闪购等等。“热点”的形式不重要,重要的是通过它得到一些准备信息.例如:可能参与的用户数量、地域分布、感兴趣的产品等。为后续的技术架构提供数据支持。技术隔离之前已经准备好了技术隔离架构图,所以在技术上需要考虑以下几个方面:客户端,前端秒杀页面使用了一个特殊的页面,这些页面包括静态HTML和动态JS,都需要CacheonCDN。在接入层,增加了过滤器来应对尖峰请求。即使我们扩展更多的应用,使用更多的应用服务器,部署更多的负载均衡器,我们也会遇到无法支持海量请求的时候。所以这一层需要考虑的是如何做好限流。当超过系统的容忍度时,我们需要果断地阻止请求的涌入。在应用层,瞬时海量的请求就像请求的“高峰”,我们架构体系的目的就是“削峰”。需要通过服务集群和横向扩展,让“高峰期”的请求分布到不同的服务器上进行处理。同时利用缓存和队列技术减轻应用程序处理压力,通过异步请求实现最终一致性。由于是多线程操作,商品数量有限,为了解决超卖问题,需要考虑进程锁的问题。数据库隔离秒杀活动持续时间短,瞬时数据量大。为了不影响现有数据库的正常业务,可以新建一个库或表进行处理。秒杀结束后,这部分数据需要同步到主业务系统或查询表中。如果数据量特别大,达到几千万甚至上亿,建议使用分表或者分库。客户端设计在上面提到的三个隔离维度中,我们最关心的是技术维度。如果浏览器/客户端是用户接触“秒杀系统”的入口,那么在这一层提供缓存数据是非常有必要的。在设计之初,我们会为闪购产品生成特殊的产品页面和订单页面。这些页面主要是静态HTML,包括尽可能少的动态信息。从业务角度来说,用户早已熟悉这些商品的信息,关心的是如何在闪购期间快速下单。由于商品详情页和订单页都是静态生成的,所以需要定义一个URL,并在闪购开始前打开这个URL供用户访问。为了防止“程序员或圈内人”作弊,这里的地址可以通过时间戳和Hash算法生成,也就是说这个地址只有系统知道,会在快秒杀前由系统下发。有人说,如果浏览器/客户端存储的都是静态页面,那么“控制和启动订单”按钮和发送“订单请求”按钮也是静态的吗?答案是否定的,其实静态页面方便客户端容易缓存,下单的动作和下单时间的控制还是在服务端。只是通过一个JS文件发送给客户端,这部分JS会在秒杀前下载到客户端。因为业务逻辑很少,基本只包括时间、用户信息、产品信息等。因此,它对网络的要求并不高。同时,在网络设计上,我们也会将JS和HTML同时缓存在CDN上,让用户从离自己最近的CDN服务器获取这些信息。为了防止秒杀程序参与秒杀,在客户端也会设计一些问答或者滑条功能,以减轻此类机器人对服务端的压力。秒杀系统前端设计示意图代理层的设计说完秒杀系统的前端设计,请求自然就来到了代理层。由于用户请求量很大,我们需要使用负载均衡加服务器集群来面对这样前所未有的压力。代理层三大功能示意图本层可用于缓存、过滤和限流:缓存,以Nginx为例,可以缓存用户信息。假设用户信息的修改不是那么频繁,即使有类似的修改,也可以通过更新服务刷新。它比从服务器获取它要高效得多。过滤,由于缓存了用户信息,可以在这里过滤掉一些不符合条件的用户。注意,这里对用户信息的过滤和缓存只是一个例子。主要意思是可以将一些不经常变化的数据交由代理层进行缓存,提高响应效率。同时,它还可以根据风控系统返回的信息,过滤一些疑似机器人或恶意请求。例如:来自固定IP的高频请求。最重要的是,在这一层,可以识别来自秒杀系统的请求。如果是带秒杀系统的参数,则请求必须路由到秒杀系统的服务器集群。只有这样才能脱离正常的业务系统。限流,每个服务器集群能承受的压力是有限的。代理层可以根据服务器集群所能承受的最大压力设置流量阈值。阈值的设置可以动态调整。例如:集群服务器中有10台服务器,其中一台由于压力过大挂掉了。这时需要调整代理层的流量阈值,减少可以处理的请求流量,保护后端应用服务器。服务器恢复后,可以将阈值调整回原来的位置。可以通过Nginx+Lua的配合来完成。Lua从服务注册中心读取服务健康状态,动态调整流量。应用层设计“秒杀系统”什么是秒杀?它只不过是一种商品。对于系统来说,就是商品的库存。采购的商品一旦超过库存,就不能再销售。防止超卖和超库存,仍然可以卖给用户。这就是“超卖”,在系统设计中需要避免。为了承受大流量的访问,我们使用了可水平扩展的服务,但只有一个资源“库存”供它们消费。为了提高效率,这个库存信息会放在缓存中。以现在流行的Redis为例,用它来存储库存信息,多线程访问会造成资源争用。也就是说,分布式程序会争夺唯一的资源。为了解决这个问题,我们需要实现分布式锁。假设有多个应用响应用户的订单请求,它们会同时访问Redis中存储的库存信息,每接受一个用户请求,Redis库存中就会减去1个商品库存。当任何进程访问Redis中的库存资源时,其他进程无法访问,所以这里需要考虑锁的情况(乐观,悲观)。Redis缓存携带库存变量。如果长时间没有释放锁,需要考虑锁的过期时间,需要设置两个超时时间:资源本身的超时时间。一旦资源被使用了一段时间还没有释放,Redis会自动释放资源。由其他服务使用。服务获取资源的超时时间。一个服务一旦获得资源一段时间后,无论该服务是否处理完该资源,都需要释放该资源以供其他服务使用。订单处理流程中这里的“扣款服务”完成了最简单的库存扣款工作,不与其他项目服务打交道,更不用说访问数据库了。订单流程图背后的过程相对复杂。先看图,按图解释:首先,扣款服务是订单流程的入口,会先扣款商品的库存。另外,它会检查产品是否还有库存?由于订单对应的操作步骤较多,为了流程顺畅,使用一个队列来存储每个订单请求,等待订单处理服务完成具体业务。订单处理服务实现了一个多线程或水平可伸缩的服务阵列,这些服务不断地侦听队列中的消息。一旦发现有新的订单请求,则取出该订单进行后续处理。注意这里可以添加ZooKeeper之类的服务调度,帮助协调服务调度和任务分配。订单处理服务,订单处理完成后,将结果写入数据库。写入数据库是IO操作,耗时较长。因此,在写入数据库的同时,会先将结果写入缓存,方便用户第一时间查询是否下单成功。结果被写入数据库,这可能成功也可能不成功。为了保证数据的最终一致性,我们使用订单结果同步服务,在数据库中不断比对、缓存和保存订单结果信息。一旦发现不一致,就会进行重试操作。如果重试仍然不成功,则将信息重写到缓存中,让用户知道失败的原因。用户下单后,急着刷新页面查看下单结果。其实他就是读取缓存上的订单结果信息。虽然这些信息和最终的结果存在偏差,但是在秒杀的场景下,高性能是前提,结果的一致性可以在后期进行补偿。数据库设计说完了秒杀的处理流程,我们再来说说数据库设计应该注意的点。数据预估上面提到,秒杀场景需要注意隔离,这里的隔离包括“业务隔离”。也就是说,我们需要在闪购前使用商业手段,比如热点活动、问卷调查、历史数据分析等。使用它们来估计此次闪购可能需要存储的数据量。需要考虑的数据有两部分:业务数据和日志数据。前者显然用于业务系统。后者用于问题订单的分析和后续处理,也可用于秒杀完成后的复查。这些数据分表分库的存储需要具体情况具体分析。例如MySQL单表推荐存储容量为500W条记录(经验数字)。如果估算超过这个数据,建议做分表。如果服务的连接数较大,建议进行分库操作。数据隔离由于大量的数据操作是插入,有少量的修改操作。如果使用关系型数据存储,建议使用专用表存储,不建议使用业务系统当前使用的表。正如开头所说,数据隔离是必须的。秒杀系统一旦宕机,不会影响正常的业务系统。这种风险意识是必须要有的。除了ID,表的设计最好不要设置其他主键,以保证快速插入。由于数据合并存储在一个专门的表中,因此需要在秒杀活动完成后与现有数据进行合并。其实交易已经完成,合并的目的就是为了查询。这种合并需要根据具体情况具体分析。对于那些“只读”的数据,对于读写分离的公司,可以导入到读专用的数据库或者NoSQL数据库中。压力测试秒杀系统已经搭建好,肯定会上线,所以上线前压力测试必不可少。我们做压力测试的目的是检查系统崩溃的边缘在哪里?系统的极限在哪里?只有这样才能合理设置流量上限。为了保证系统的稳定性,需要丢弃多余的流量。压力测试方法合理的测试方法可以帮助我们深入了解系统。下面介绍两种压力测试方法:正压力测试负压力测试正压力测试。每一次秒杀活动都会有计划,会占用多少服务器资源,容忍多少请求。您可以继续对该请求量加压,直到系统接近崩溃或实际崩溃为止。简单的说,就是做加法。正压力测试示意图负压力测试。在系统正常运行的情况下,逐渐减少支撑系统的资源(服务器),看看系统什么时候不能支撑正常的业务请求。例如:在系统正常运行的情况下,逐渐减少服务器或微服务的数量,观察业务请求的状态。说白了,就是做减法。负压测试示意图压测步骤测试步骤有了测试方法的支持,我们来看看需要进行哪些测试步骤。下面的操作比较常规,大家也可以在其他系统的压力测试中做同样的操作,供大家参考。首先,确定测试目标。与性能测试不同,压力测试的目标是在系统接近崩溃时进行。例如:需要支持500W的访问量。第二,确定关键特征。压测其实是有重点的。根据2/8原则,系统中20%的功能使用最多。我们可以对这些核心功能进行压力测试。例如:下单、扣库存。聚焦核心服务第三??,确定负载。这与关键服务的思路是一致的。并非每个服务都有高负载。我们的测试其实是针对那些负载比较大的服务,或者说系统中某些服务的负载在一段时间内有波动的。这些是测试目标。第四,选择环境。建议搭建一个与生产环境完全一样的环境进行测试。第五,确定监控点,其实就是监控关注的参数,比如CPU负载,内存占用,系统吞吐量等等。第六,要产生负载,需要从生产环境中获取一些真实的数据作为负载数据源。这部分数据源根据目标系统的承载需求,由脚本驱动,对系统产生影响。建议使用之前闪购系统的数据或者实际生产系统的数据进行测试。第七,执行测试。这里主要是根据目标系统和关键部件,进行带载测试,返回监控点的数据。建议团队可以针对测试制定计划,模拟不同的网络环境,定期对硬件条件进行测试。第八,分析数据。以测试为目的,分析关键服务的压测数据,知道服务的上限在哪里。对一段时间内负载波动较大或负载较大的业务进行数据分析,获取业务转型方向。总结一下秒杀系统的特点,并发量大,资源有限,操作比较简单,访问热点数据。因此,我们需要将其与业务、技术、数据进行隔离,保证不影响现有系统。因此,架构设计需要从客户请求到数据库存储,再到上线前的压力测试,多层次考虑。一个简单的思维导图供大家按照以下顺序思考,客户端→代理层→应用层→数据库→压力测试:客户端90%静态HTML+10%动态JS;配合CDN做好缓存。接入层侧重于过滤和节流。应用层采用缓存+队列+分布式的方式处理订单。做好数据预估、隔离、合并工作。记得在上线前进行压力测试。作者:崔浩简介:十六年开发架构经验。曾在惠普武汉交付中心担任技术专家、需求分析师、项目经理,后在一家初创公司担任技术/产品经理。善于学习,乐于分享。目前专注于技术架构和研发管理。【原创稿件,合作网站转载请注明原作者和出处为.com】
