如何从零搭建 10 万级 QPS 大流量、高并发优惠券系统

360影视 2025-01-20 17:27 3

摘要:在春节活动期间,众多业务方纷纷提出了发放优惠券的需求,且对优惠券的发放速度(QPS量级)有着明确的要求。为了应对这一挑战,我们亟需构建一个全新的系统,用以承载优惠券的发放、核销以及查询等全流程操作。因此,设计和开发一个能够稳定支持十万级QPS的券系统,并对优惠

在春节活动期间,众多业务方纷纷提出了发放优惠券的需求,且对优惠券的发放速度(QPS量级)有着明确的要求。为了应对这一挑战,我们亟需构建一个全新的系统,用以承载优惠券的发放、核销以及查询等全流程操作。因此,设计和开发一个能够稳定支持十万级QPS的券系统,并对优惠券的整个生命周期进行细致管理,成为了我们当前的首要任务。

1、在面试过程中,我们需要分层次地理解问题,并逐步与面试官沟通,构建出切实可行的解决方案。关键在于展现我们的思考能力和逐步推进的过程,而非仅仅给出一个经过长时间思考的完备方案。毕竟,如果面试官无法记住我们的方案,那么一切努力都将付诸东流。

2、对于问题的拆解,我们可以从两个方面入手:一是功能需求,即构建一个能够发放优惠券的系统,并对优惠券的整个生命周期进行维护;二是性能需求,即系统需要能够稳定支持十万级的QPS。

2.1 需求拆解与技术选型

2.1.1 需求拆解细化

在配置优惠券时,我们需要考虑券批次的创建(即券模板的设计),包括券模板的有效期以及券的库存信息等关键要素。

在发放优惠券时,则涉及到券记录的创建与管理,如设置券的过期时间、记录券的状态等。此外,优惠券需要与交易系统紧密配合,当订单失败时,需要确保优惠券能够回滚至可用状态。因此,我们需要对优惠券的生命周期进行精细化管理,包括发券、券激活、券使用以及券回退等各个环节。

综上所述,我们可以将需求简化为两大核心部分:券模板与券记录。同时,无论是券模板还是券记录,都需要提供便捷的查询接口,以满足业务方对券模板及券记录的查询需求。

2.1.2 系统选型及中间件

确定了基本需求后,我们根据需求进一步分析了可能会用到的中间件以及系统整体的组织方式。

存储方面,由于券模板、券记录等需要持久化存储,并支持条件查询,因此我们选择了通用的结构化存储Mysql作为存储中间件。

缓存方面,考虑到发券时需要频繁获取券模板信息,以及库存管理(库存扣减)是一个高频、实时的操作,因此我们引入了缓存机制。Redis作为主流的缓存中间件,能够满足我们的需求,因此我们选择了Redis作为缓存中间件。

消息队列方面,由于券模板/券记录需要展示过期状态,并根据不同状态进行业务逻辑处理,因此我们引入了延迟消息队列。RocketMQ支持延时消息,因此我们选择了RocketMQ作为消息队列。

系统框架方面,发券系统作为下游服务,需要被上游服务调用。公司内部服务之间采用RPC服务调用,且系统开发语言为JAVA,因此我们选择了dubbo作为RPC框架进行代码编写。我们采用dubbo+springBoot+MySQL+Redis+RocketMQ来实现发券系统,RPC服务部署在公司的docker容器中。至此,一套完整的微服务框架已初具雏形。

2.2 系统开发与实践

2.2.1 系统整体架构

从需求拆解部分我们对要开发的系统有了大致了解,下面给出整体系统架构,包含了一些具体的功能。

2.2.2 数据结构ER图

与系统架构相对应,我们需要建立对应的MySQL数据存储表。

2.2.3 核心逻辑实现

2.2.3.1 发券:

发券流程包括参数校验、幂等校验和库存扣减三部分。幂等操作用于确保在发券请求不正确的情况下,业务方通过重试、补偿等方式再次请求时,能够最终只发出一张券,防止资金损失。

2.2.3.2 券过期:

券过期是一个状态推进的过程,我们使用RocketMQ来实现。由于RocketMQ支持的延时消息有最大限制,而卡券的有效期不固定,可能会超过这个限制,因此我们将卡券过期消息进行循环处理,直到卡券过期。

2.3 重难点解决方案——大流量、高并发场景的挑战与应对策略

此刻,便是展现真才实学之时。面对各种极端场景的考验,你需要挺身而出,提供切实可行的解决方案。这要求你不仅要敏锐地发现问题,精准识别其中的重难点,更要能够运筹帷幄,给出恰如其分的应对策略。

在系统的基本功能得以实现之后,我们接下来深入探讨一下,在大流量、高并发场景下,系统可能会遭遇的种种挑战,以及我们精心准备的应对策略。

问题1:发券记录的存储挑战

面对海量的发券数据,如何高效存储成为了一个关键问题。MySQL作为常用的关系型数据库,其存储能力是否足以支撑?在数据量庞大的情况下,是否需要考虑分库分表的策略来优化存储性能?这些都是我们需要深入思考和解决的问题。

问题2:券记录的查询与Redis的应用

在券记录的查询方面,Redis作为高性能的缓存系统,是否能够满足我们的需求?为了充分发挥其优势,我们需要进行怎样的配置?此外,在承担库存扣减任务时,Redis是否能够独当一面,解决我们面临的问题?这些都是Redis应用过程中需要仔细考量的问题。

在我们的系统架构中,MySQL和Redis共同承担着存储的重任。然而,任何单个服务器的I/O能力都是有限的。通过实际测试,我们得出了以下数据:单个MySQL服务器的每秒写入能力大约在4000 QPS左右,一旦超过这个阈值,MySQL的I/O时延将急剧上升,影响系统性能。同时,当MySQL单表的记录数达到千万级别时,查询效率会显著下降;若记录数过亿,数据查询将变得异常困难。而对于Redis而言,单个分片的写入瓶颈大约在2万左右,读瓶颈则高达10万。这些数据为我们优化系统架构提供了重要的参考依据。

读写分离:在查询券模板、查询券记录等场景下,我们可以将MySQL进行读写分离,让查询流量走MySQL的读库,从而减轻写库的查询压力。分治:对于存储瓶颈问题,业界常用的方案是分而治之,即流量分散、存储分散。具体来说,就是分库分表。水平扩容:发券归根结底是要对用户的领券记录进行持久化存储。对于MySQL的I/O瓶颈问题,我们可以在不同服务器上部署MySQL的不同分片,对MySQL进行水平扩容。这样一来,写请求就会分布在不同的MySQL主机上,从而大幅提升整体的吞吐量。以user_id后四位为分片键,对用户领取的记录表进行水平拆分,以支持用户维度的领券记录查询。对于Redis的存储瓶颈问题,我们同样需要进行水平扩容,以减轻单机压力。在给用户发券的过程中,我们将发券数记录在Redis中。大流量情况下,我们需要对Redis进行水平扩容。

基于上述思路,在满足发券12w QPS的需求下,我们对存储资源进行了预估。

MySQL资源:在实际测试中,单次发券对MySQL有一次非事务性写入。MySQL单机的写入瓶颈为4000。据此,我们可以计算出所需的MySQL主库资源为:120000/4000=30。

Redis资源:假设12w的发券QPS均为同一券模板,Redis单分片的写入瓶颈为2w,则所需的最少Redis分片为:120000/20000=6。

大流量发券场景下,如果我们使用的券模板为一个,那么每次扣减库存时都会访问到特定的Redis分片,因此一定会达到这个分片的写入瓶颈,甚至可能导致整个Redis集群不可用。

对于热点库存问题,业界有通用的方案:即扣减的库存key不要集中在某一个分片上。如何保证这一点呢?我们可以拆key(拆库存)即可。在建券模板时,就将这种热点券模板的库存进行拆分,后续扣减库存时扣减相应的子库存即可。

在建券时,我们需要对券模板进行配置。在库存扣减时,为了确保每次请求都随机不重复地轮询子库存,我们可以先生成对应分片总数的随机不重复数组。这样每次扣减子库存的请求就会分布到不同的Redis分片上,既缓解了Redis单分片压力,又能支持更高QPS的扣减请求。当某个券模板的子库存耗尽时,我们可以跳过这个子库存分片,以优化系统在库存即将耗尽情况下的响应速度。

6.3.1.4 库存扣减

对于库存扣减相关的处理逻辑,建议搜索描述相关的项目,其有相对完整的逻辑。其思路一般如下:

1、请求合并,一次扣减。扣减失败,请求回退,逐个处理。

2、基于redis处理,redis扣减完成后,数据同步回Mysql,持久化存储。

3、如果进一步提升效率,方案是减少热点。进行数据拆迁。就是同一个库存拆成多个字段存储,每个字段分配一部分库存。固定的时间进行库存的重新整合。对于库存的操作,要用数据的加减,而不是数据的覆盖。

下面是基于redis热点数据,分片处理的策略:

这里尚存一个问题亟待解决:若扣减子库存始终从1开始,那么Redis对应分片的压力实则并未得到丝毫缓解。因此,我们亟需实现的是:每次请求时,能够随机且不重复地轮询子库存。以下为本项目所采纳的一项具体策略:

Redis子库存的key设计巧妙,其最后一位即为分片的编号,例如:xxx_stock_key1、xxx_stock_key2……在扣减子库存的过程中,我们首先会生成一个与分片总数相对应的随机且不重复的数组。譬如,第一次可能是[1,2,3],而第二次则可能是[3,1,2]。如此一来,每次扣减子库存的请求都将被分散至不同的Redis分片上,这不仅能够缓解Redis单分片的压力,同时也能够支撑起更高QPS的扣减请求。

然而,这一策略在库存接近耗尽时却会面临一个挑战:众多分片的子库存轮询将变得毫无意义。为了应对这一挑战,我们可以在每次请求时记录下子库存的剩余量。当某一券模板的子库存完全耗尽后,随机且不重复的轮询操作将直接跳过这个子库存分片。这一举措能够显著优化系统在库存即将耗尽时的响应速度。

在业界,针对Redis热点key的处理,除了分key策略外,还有一种key备份的思路。即将相同的key,通过某种策略备份至不同的Redis分片上,从而将热点打散。然而,这一思路更适用于读多写少的场景,并不适合应对发券这种大流量写的场景。因此,在面对具体的业务场景时,我们需要根据业务需求,精心挑选最合适的方案来解决问题。

问题

在高QPS、高并发的严峻场景下,即便是微小的0.01%接口成功率提升,其实际效益也是相当可观的。此刻,让我们再次审视整个发券流程:查询券模板信息(Redis)→ 校验 → 幂等性检查(MySQL)→ 发券(MySQL)。在这一连串的步骤中,查询券模板信息对Redis的强依赖尤为显著。然而,在实际监控中,我们发现Redis超时的概率大约在万分之二至万分之三之间。这无疑意味着,这部分发券请求注定会以失败告终。

为了显著提升这部分请求的成功率,我们精心设计了两种策略。其一,当从Redis获取券模板信息失败时,我们在内部进行重试;其二,我们将券模板信息缓存至服务实例的本地内存中,即引入二级缓存机制。内部重试策略确实能够提升一部分请求的成功率,但遗憾的是,它并不能从根本上解决Redis超时的问题。同时,重试次数与接口响应时长之间呈现出正相关的关系。相比之下,二级缓存的引入则能够从根本上规避Redis超时所导致的发券请求失败问题。因此,我们毅然选择了二级缓存方案。

当然,在引入本地缓存的同时,我们还需要在每个服务实例中部署一个定时任务,以确保最新的券模板信息能够实时刷入本地缓存和Redis中。在将模板信息刷入Redis时,我们会巧妙地加上分布式锁,以防止多个实例同时写入Redis,从而给Redis带来不必要的压力。

对于二级缓存方案,本质上是业务的降级策略。就是在当前系统无法满足需求的情况下的备选方案。需要有开关逻辑,在业务量可以承载的情况,关闭二级缓存的方案。

在系统开发圆满收官之后,还需实施一系列精细操作,以确保系统的稳健运行无虞。

超时机制的精妙设定。鉴于优惠券系统作为RPC服务的特性,我们精心设置了合理的RPC超时时长,旨在防止上游系统的潜在故障对我们的系统造成连锁反应。以发券接口为例,其内部执行效率极高,不超过100ms,因此我们将接口超时时间审慎设定为500ms。这一机制意味着,任何异常请求若在500ms内未能完成,将被果断拒绝,从而有力保障了我们服务的持续稳定运行。监控与报警的双重保险。针对核心接口的监控、稳定性评估、关键数据追踪,以及系统CPU、内存等资源的监控,我们已在Grafana平台上构建了直观的可视化图表。特别是在春节活动高峰期,我们实时紧盯Grafana仪表盘,确保能够迅速捕捉到任何系统异常信号。同时,我们还配备了完善的报警机制,一旦有异常情况发生,能够立即触发警报,让我们第一时间洞悉系统状态。限流的智慧策略。作为底层服务的优惠券系统,在实际业务场景中需应对多个上游服务的调用需求。因此,我们对这些上游服务实施了合理的限流措施,这无疑是保障优惠券系统自身稳定性的关键一环。资源隔离的深思熟虑。鉴于我们的服务均部署在docker集群之中,为了确保服务的高可用性,我们精心规划了服务部署的集群资源分布,尽量将其分散在不同的物理区域,从而有效避免了因集群故障而导致的服务不可用风险。

在完成上述一系列周密准备后,是时候检验我们的服务在生产环境中的真实表现了。当然,在新服务正式上线之前,对其进行严格的压测是必不可少的环节。以下是对压测过程中可能需要注意的问题及压测结论的精炼总结。

压测注意事项

首先是压测思路的明确,由于初期我们无法准确判断docker、存储组件等潜在的瓶颈所在,因此我们的压测策略通常遵循以下步骤:

精准定位单实例瓶颈深入剖析MySQL主库的写、读性能瓶颈细致探寻Redis单分片的写、读性能极限

在获取上述关键数据后,我们便能大致估算出所需资源数量,进而全面展开服务整体的压测工作。

压测资源的充足准备至关重要,只有提前申请到足够的压测资源,才能科学合理地制定压测计划。压测过程中的监控与反思同样不可或缺,我们应密切关注服务和资源的监控数据,对任何不符合预期的部分进行深入剖析,并着手优化代码。适时记录压测数据是复盘提升的关键,只有详尽记录每一次压测的数据结果,我们才能更好地总结经验教训。实际使用资源的适度冗余也是明智之举,我们通常会按照压测数据的1.5倍来配置线上资源,以确保有足够的冗余资源来应对突发的流量增长。

在面临高达13万QPS的发券请求时,该系统展现出了卓越的性能,请求成功率稳稳地维持在99.9%以上,系统监控数据亦显示一切正常。特别是在春节红包雨活动期间,该优惠券系统成功承载了两次红包雨的全部流量冲击,全程表现平稳,未出现任何异常状况,圆满地完成了优惠券的发放任务。

关于系统的业务深度思考

当前的系统虽已能够高效支持高并发的发券功能,但在优惠券业务的深度探索上仍有待加强。未来,我们需要紧密结合业务需求,尝试引入批量发券(如券包形式)、批量核销等更多元化的功能。发券系统作为业务中台的基础支撑,其潜力巨大,可适配各类应用场景,我们应当持续探索,以支持更多样化的业务需求。

在从零搭建这样一个能应对大流量、高并发的优惠券系统时,首要任务是深入理解业务需求,进而对需求进行细致拆解。基于拆解后的需求,我们应合理选用各类中间件以构建系统。本文聚焦于优惠券系统的建设,因此广泛运用了各类存储组件和消息队列,以实现优惠券的高效存储、快速查询以及精准过期处理。在系统开发过程中,我们详细阐述了核心的发券流程和券过期处理流程,并针对大流量、高并发场景下可能出现的存储瓶颈、热点库存问题以及券模板缓存获取超时等挑战,提出了切实可行的解决方案。具体而言,我们采用了分治策略,对存储中间件进行水平扩容,有效缓解了存储瓶颈;通过库存拆分为子库存的方法,成功解决了热点库存问题;同时,引入了本地缓存机制,有效避免了券模板从Redis获取超时的情况。这些措施共同确保了优惠券系统在大流量、高并发的复杂场景下依然能够稳定、可靠地运行。

此外,我们还从服务超时设置、监控报警、限流策略以及资源隔离等多个维度对服务进行了全面治理,进一步提升了服务的高可用性。

压测作为新服务上线前不可或缺的一环,其重要性不言而喻。通过压测,我们能够全面、准确地了解服务的整体性能表现,同时,压测过程中暴露的问题也往往是线上运行时可能遭遇的挑战。因此,经过压测的洗礼,我们对新服务的整体情况有了更加清晰的把握,对服务正式上线并投入生产环境运行充满了信心。

来源:散文随风想

相关推荐