字节10万级QPS高并发优惠券系统实践
《字节10万级QPS高并发优惠券系统实践》共 0.4 万字,通篇围绕 Redis、MySQL、需求、缓存 展开,归在「推理、部署与算力」这一层。
适合要把模型跑起来,还得跑得起的人
目录骨架(8 节)
- 需求背景
- 需求拆解及技术选型
- 系统开发与实践
- 系统压测及实际表现
- 系统的业务思考
- 需求拆解
- 系统选型及中间件
- 系统设计实现
分享内容
- 需求背景
- 需求拆解及技术选型
- 系统开发与实践
- 系统压测及实际表现
- 系统的业务思考
- 总结
需求背景
春节活动中,多个业务方都有发放优惠券的需求,且对发券的 QPS 量级有明确的需求。所有的优惠券发放、核销、查询都需要一个新系统来承载。因此,我们需要设计、开发一个能够支持十万级 QPS 的券系统,并且对优惠券完整的生命周期进行维护。
需求拆解及技术选型
需求拆解
- 要配置券,会涉及到券批次(券模板)创建,券模板的有效期以及券的库存信息
- 要发券,会涉及到券记录的创建和管理(过期时间,状态)
因此,我们可以将需求先简单拆解为两部分:

同时,无论是券模板还是券记录,都需要开放查询接口,支持券模板 / 券记录的查询。
系统选型及中间件
确定了基本的需求,我们根据需求,进一步分析可能会用到的中间件,以及系统整体的组织方式。
存储
由于券模板、券记录这些都是需要持久化的数据,同时还需要支持条件查询,所以我们选用通用的结构化存储 MySQL 作为存储中间件。
缓存
- 由于发券时需要券模板信息,大流量情况下,不可能每次都从 MySQL 获取券模板信息,因此考虑引入缓存
- 同理,券的库存管理,或者叫库存扣减,也是一个高频、实时的操作,因此也考虑放入缓存中
主流的缓存 Redis 可以满足我们的需求,因此我们选用 Redis 作为缓存中间件。
消息队列
由于券模板 / 券记录都需要展示过期状态,并且根据不同的状态进行业务逻辑处理,因此有必要引入延迟消息队列来对券模板 / 券状态进行处理。RocketMQ 支持延时消息,因此我们选用 RocketMQ 作为消息队列。
系统框架
发券系统作为下游服务,是需要被上游服务所调用的。公司内部服务之间,采用的都是 RPC 服务调用,系统开发语言使用的是 golang,因此我们使用 golang 服务的 RPC 框架 kitex 进行代码编写。
我们采用 kitex+MySQL+Redis+RocketMQ 来实现发券系统,RPC 服务部署在公司的 docker 容器中。
系统开发与实践
系统设计实现
系统整体架构
从需求拆解部分我们对大致要开发的系统有了一个了解,下面给出整体的一个系统架构,包含了一些具体的功能。

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

核心逻辑实现
发券:
- 发券流程分为三部分:参数校验、幂等校验、库存扣减。

幂等操作用于保证发券请求不正确的情况下,业务方通过重试、补偿的方式再次请求,可以最终只发出一张券,防止资金损失。
券过期:
券过期是一个状态推进的过程,这里我们使用 RocketMQ 来实现。

- 由于 RocketMQ 支持的延时消息有最大限制,而卡券的有效期不固定,有可能会超过限制,所以我们将卡券过期消息循环处理,直到卡券过期。
大流量、高并发场景下的问题及解决方案
实现了系统的基本功能后,我们来讨论一下,如果在大流量、高并发的场景下,系统可能会遇到的一些问题及解决方案。
存储瓶颈及解决方案
瓶颈:
在系统架构中,我们使用了 MySQL、Redis 作为存储组件。我们知道,单个服务器的 I/O 能力终是有限的,在实际测试过程中,能够得到如下的数据:
- 单个 MySQL 的每秒写入在 4000 QPS 左右,超过这个数字,MySQL 的 I/O 时延会剧量增长。
- MySQL 单表记录到达了千万级别,查询效率会大大降低,如果过亿的话,数据查询会成为一个问题。
- Redis 单分片的写入瓶颈在 2w 左右,读瓶颈在 10w 左右
解决方案:
- 读写分离。在查询券模板、查询券记录等场景下,我们可以将 MySQL 进行读写分离,让这部分查询流量走 MySQL 的读库,从而减轻 MySQL 写库的查询压力。
- 分治。在软件设计中,有一种分治的思想,对于存储瓶颈的问题,业界常用的方案就是分而治之:流量分散、存储分散,即:分库分表。
- 发券,归根结底是要对用户的领券记录做持久化存储。对于 MySQL 本身 I/O 瓶颈来说,我们可以在不同服务器上部署 MySQL 的不同分片,对 MySQL 做水平扩容,这样一来,写请求就会分布在不同的 MySQL 主机上,这样就能够大幅提升 MySQL 整体的吞吐量。
- 给用户发了券,那么用户肯定需要查询自己获得的券。基于这个逻辑,我们以 user_id 后四位为分片键,对用户领取的记录表做水平拆分,以支持用户维度的领券记录的查询。
- 每种券都有对应的数量,在给用户发券的过程中,我们是将发券数记录在 Redis 中的,大流量的情况下,我们也需要对 Redis 做水平扩容,减轻 Redis 单机的压力。
