100万用户,抢10万优惠券,如何设计?
文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :
免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
100万用户,抢10万优惠券,如何设计?
尼恩特别说明: 尼恩的文章,都会在 《技术自由圈》 公号 发布, 并且维护最新版本。 如果发现图片 不可见, 请去 《技术自由圈》 公号 查找
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的架构类/设计类的场景题:
1.如何设计高并发优惠券系统 ,请说出你的方案?
2.听说你会架构设计,请问一下如果让你来设计优惠券系统,说说你的架构设计方案。
3.100万用户,抢10万优惠券,如何设计?
最近有小伙伴在面试 字节,又遇到了优惠券架构问题。小伙伴支支吾吾的说了几句,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
本文目录
一、10Wqps抢券系统的功能分析
首先看看,电商系统(Electronic Commerce Systems)的促销手段:
- 优惠券
- 拼团
- 砍价
- 老带新
说到电商平台上,无人不知优惠券体系,它是一种常见的促销方式,所以说, 优惠券是电商常见的营销手段,具有灵活的特点。
优惠券 的功能:在规定的周期内,购买对应商品类型和额度商品时,下单结算时会减免一定金额。
优惠券既可以作为促销活动的载体,也是重要的引流入口。
一般来说,优惠券系统是 商城营销模块中一个重要组成部分,哪怕早在 商城单体应用时代,优惠券就是其中核心模块之一。
在电商系统中 优惠券的种类,主要有:
-
代金券
可以直接抵扣商品金额,不找零;任意金额可用(可能会导致零元单,看业务是否能接受);可以同非商品券叠加
-
满减券
可以抵扣商品金额,有使用门槛;门槛金额需大于优惠金额;可以同非商品券叠加
-
折扣券
可以抵扣商品金额,有使用门槛,需要设置封顶金额;可以同非商品券叠加
-
运费券
不可以抵扣商品金额;只能够抵扣运费;可以同非商品券叠加
在电商系统中 优惠券 涉及到两个核心功能:
(1)发放券
-
谁能领?
所有用户 or 指定的用户
-
领取上限
一个优惠券最多能领取多少张?
-
领取方式
用户主动领取 or 自动发放被动领取
(2)核销券
-
作用范围
商品、商户、类目
-
计算方式
是否互斥、是否达到门槛等
V1.0版本的抢券系统需求拆解
最为简单的一个抢券系统的功能,保护两个部分:
- tob部分:配置券,会涉及到券批次(券模板)创建,券模板的有效期以及券的库存信息
- toc部分:发券,会涉及到券记录的创建和管理(过期时间,状态)
因此,我们可以将需求先简单拆解为两部分:
同时,无论是券模板还是券记录,都需要开放查询接口,支持券模板/券记录的查询。
V1.0版本的 模块介绍
优惠券系统主要分为tob部分 运营端的运营配置管理模块和toc部分执行端的执行核心模块。
tob部分 优惠券运营配置管理模块提供给运营人员操作、管理券权益,回收数据分析。包括
- 模板库存管理、
- 外部券管理、
- 策略规则管理、
- 数据报表等功能。
toc部分 优惠券执行核心模块包括:
- 优惠券的兑换发放、
- 使用核销、
- 状态流转等功能,
- 提供统一的API接口,供上游各业务系统调用,参与到各产品功能环节。
同时在运行中有完善的运行监控和有效的策略管控能力,提供保障。
优惠券 模板管理 的设计
优惠券信息的定义是基于对应模板的,一张优惠券一般有这些要素:
- 券的名称描述等信息
- 券的类型
- 应该在什么条件下能使用
- 使用时优惠减免多少。
这些要素被抽象为券模板,在不同模板中定义了券的不同元素。
模板主要包含了两部分信息:模板配置和运营管理。
模板配置主要定义了模板使用的业务配置,比如模板类型、基础信息、发放信息、激活条件、使用条件、展示信息、兑换码管理、外部券管理等。
运营管理是对模板的使用、运行进行的监控统计分析,包含监控告警、数据统计、策略管控。
抢券系统的业务流程设计
优惠券系统提供统一的 API 接口和 MQ 消息,提供配券、发券、查券、用券的能力,接入到上游各业务系统中。
优惠券的使用规则设计
1.叠加规则
-
一般情况下,商品券我们分为商家自有券和平台发放券,都为商品券,平台的自有券是可以和商家的自行发放的券进行叠加的,但是商家券与 商家券不能叠加,平台券和平台券不能叠加,所以有
商家券+平台券
-
商品券和运费券是可以叠加的,那么我们可以知道,叠加规则可以
商家券+平台券+运费券
-
有时候,双十一等大促期间,电商平台还会出一些神券,这些券也是可以和上边的券叠加的,给到用户更多的优惠
商家券+平台券+运费券+神券
2.命中规则
对于用户的优惠券来说,可能有很多,但是最优的搭配只能有一个,所以在选中最优的优惠券时,我们需要有一些排序条件
命中规则考虑的因素比较多
-
金额
金额是用户最关心的一个点,到底能有多大优惠,一定是体现在金额上,所以我们在搭配优惠券时,一定要把金额最大的,排在第一位
-
时间
优惠券都有有效时间,有可能有两张券,他们的面额、使用条件等,都一样,那么过期时间就作为一个排序条件,最先过期的优先
-
类型
可能我们的多张优惠券计算出来的金额都一样,时间也都一样,那么券的类型就很重要了。
一般来说满减券要优先,折扣券次之,代金券最后。
因为满减券有门槛,有最大值,但是折扣券可能封顶的优惠更大,而代金券又没有门槛。
也就是说,满减券是限制最多的,优先使用掉
-
商品
有些券有商品的适用范围,有些券是全品类的,那么我们优先使用限制商品的券
-
渠道
有些券限制了APP还是小程序使用,有些是全渠道通用的券,那么优先使用限制渠道的券
优惠券的生命周期设计
优惠券各状态说明
券状态 | 说明 |
---|---|
待激活 | 发放给用户时券的初始状态,需用户完成任务或达到一定条件后自动触发激活券,激活后才能使用。 |
待使用 | 发放给用户时券的初始状态;由用户自己激活的券;可直接在各场景下使用。 |
已使用 | 用户已使用的券。 |
已过期 | 发放给用户,直到过期都未使用的券。 |
锁定 | 被锁定的券,在订单流转过程中会出现。比如已下单未付款时,使用的券被锁定。 |
已回收 | 被回收的券,用户不可使用。比如活动资格被取消后,回收已发放的券。 |
V2.0版本的抢券系统需求演进
V1.0版本 优惠券最早和商城耦合在一个系统中
随着商城的不断发展,营销活动力度加大,优惠券使用场景增多,优惠券系统逐渐开始“力不从心”,暴露了很多问题:
- 海量优惠券的发放,达到优惠券单库、单表存储瓶颈。
- 与商城系统的高耦合,直接影响了商城整站接口性能。
- 优惠券的迭代更新受限于商城的版本安排。
- 针对多品类优惠券,技术层面没有沉淀通用优惠券能力。
为了解决以上问题,V2.0优惠券系统进行了系统独立,提供通用的优惠券服务,独立后的系统架构如下:
V2.0优惠券系统覆盖了优惠券的4个核心要点:创、发、用、计。
- “创” 指优惠券的创建,包含各种券规则和使用门槛的配置。
- “发” 指优惠券的发放,优惠券系统提供了多种发放优惠券的方式,满足针对不同人群的主动发放和被动发放。
- “用” 指优惠券的使用,包括正向购买商品及反向退款后的优惠券回退。
- “计” 指优惠券的统计,包括优惠券的发放数量、使用数量、使用商品等数据汇总。
除了提供常见的优惠券促销玩法外,优惠券系统还以优惠券的形式作为其他一些活动或资产的载体,比如手机类商品的保值换新、内购福利、与外部广告商合作发放优惠券等。
以下为V2.0版本的抢券系统的展示:
优惠券的依赖方和调用方
依赖方
-
商品
好多优惠券都是限制哪些商品可用,商品服务作为一个基础服务,是优惠券系统需要依赖的
-
商户
有些券是商家券,限制了哪个商家可用,比如lining可用,nike不可用,我们要识别到是哪个商户,所以需要依赖商户系统
-
职能
在权限控制和用户的有效性判断上,需要依赖职能系统,做校验相关的处理
-
审批流
在创建优惠券的时候,我们需要有流程化的管控,需要对接审批流
调用方
-
APP、小程序
C端作为流量的入口,需要展示优惠券的信息数据,承接了优惠券展示的所有能力
从领券开始,到提交订单时的查询可以使用的优惠券,到提交订单后的核销优惠券,最后在我的账户里边查询个人优惠券信息,完成了一整个C端的生命周期处理,属于最关键的调用方。
-
订单
订单系统,在提交订单时,需要向优惠券系统来校验,券的状态等数据,如果订单金额符合满额返券的金额。那么还需要调用优惠券系统来向用户发券
-
营销工具
一些营销工具多种多样,比如新人礼包等,需要调用优惠券系统来获取券的信息
二、抢券系统的非功能需求
如何从零搭建10万级 QPS 大流量、高并发优惠券系统?
大促期间,多个业务方都有发放优惠券的需求,且对发券的 QPS 量级有明确的需求。
所有的优惠券发放、核销、查询都需要一个高并发架构 来承载。
因此,需要设计、开发一个能够支持十万级 QPS 的券系统,并且对优惠券完整的生命周期进行维护。
NFR难题1:如何实现10Wqps的领券能力?
以多点APP为例,来看看如何实现10Wqps的领券能力?
优惠券并不是每个人都可以领取的,有很多限制条件,比如”有些优惠券只有新人可以领“,”有些优惠券一天只能领一张“,”有些券需要完成任务或达到一定门槛才可以领“,等等。
我们简单看下优惠券领取的流程图:
优惠券的领取,逻辑相对简单些,需要关注券模板、领取记录,用户标签等几个维度校验。
如果满足条件就可以给用户发放优惠券。
券模板校验:
- 优惠券模板本身设置领取时间,只有在区间时间内才有机会领取优惠券,当然是否能最终领取还取决于其他条件。
- 券库存。正如商品一样,没有库存的商品是无法下单售卖
- 当日发放限制。为了防止用户对券哄抢,同时为了延长活动的黏性效果,会限量每天发放优惠券数量。
优惠券不是随便乱发的,毕竟要计入公司的支出成本,一旦涉及到钱的问题便是个严肃的问题。
定位好用户群体,什么样的优惠券发给什么类型的用户才能获得最大收益。所以我们在创建优惠券时,除了设置适用的商品范围,还要限制优惠券的用户类型。人尽其用,物尽其才。
适用商品范围一般在页面展示或下单时才用到。而用户类型则相反,需要前置控制,也就是说在用户领取时校验,一旦用户领取成功就属于用户个人财产,只要没有过期都可以使用。
用户领取记录校验:
- 根据券模板的领取条件限制,以及用户的领取记录,判断是否还可以领取
用户标签校验:
- 有些券限制只有新人才可以领取。如:新人专享活动
- 券也可以跟用户等级挂钩,只有达到设置的等级才可以领取
- 黑名单用户。用风控挂钩,识别风险刷券用户
- 自定义用户。可以给用户打一些特定标签。并针对该类型的用户发放。
技术挑战:如何实现10Wqps的领券能力?
- 风控如何接入,领券接口的10Wqps,对风控接口性能有较高要求
- 券缓存如何设计。一般会按变化的频率做拆分。券模板本身内容可以封装一个缓存模型。其中的券库存由于经常变化,需要单独剥离处理,采用缓存+数据库。但如何保证两者的数据一致性需要我们特别关注
- 领取记录同样采用缓存+数据库
- 用户标签。由于不用的优惠券会限制发放给不用的用户人群,所以我们会根据券模板设置的用户标,采用策略模式,调用外部服务,实时查询用户是否满足领取条件。
如何实现10Wqps的领券能力? 稍后详细展开,当然,也可以来尼恩的 技术自由圈社群交流。
NFR难题2:如何实现10Wqps的算券能力?
以 多点APP为例,来看看如何实现10Wqps的算券能力?
用户对勾选的商品(也可能是购物车批量商品下单)创建订单时,会计算有哪些优惠券可以使用。
如上图所示,页面输出信息很简单,只显示可以当前可用且按优惠力度排序的优惠券,但如何计算出满足当前订单的优惠券,后台涉及哪些复杂业务逻辑,又会遇到哪些技术难点挑战,下面来我们来分析下。
整体的架构思想还是采用过滤机制,首先查出用户下所有的优惠券,然后根据优惠的各种标记位做相应的策略处理。
渠道过滤:
上面的图是多点APP的券包截图,该券限制使用条件必须是”多点配送(到家可用)“,如果是”门店自提“则订单无法使用该优惠券。
所以我们可以清楚判断,优惠券请求接口势必要传入配送方式参数。
下单时计算可用的优惠券,首要一条就是判断订单中的商品是否在优惠券的商品范围里。
常见的商品作用范围有哪些:
- 单品:创建单品优惠券,限制只有该商品才可以使用
- 部分商品:可以与活动绑定,也可以独立设置,只有指定的这些商品才可以适用优惠券
- 所有商品:全场券,所有售卖的商品都可使用。
- 店铺商品:店铺优惠券,限制只有购买本店铺的商品才可以使用,当然要满足金额门槛
- 商品分类:类目券,根据商品的类目属性做适用条件
- 商品品牌:按品牌决定是否可以使用优惠券
- 商品标签:为指定商品打上自定义标签,然后优惠券模板中配置
- 指定商品/排除特殊商品:针对特殊商品做给运营人员的灵活配置。
- 渠道商品:根据商品进货或者销售渠道定义优惠券范围
- 区域商品:根据商品销售区域划分,此类优惠券社区电商用的较多。
- 订单范围就是订单金额满减、满赠、包邮等条件下可使用的优惠券。
上图优惠券就是商家券,除了”商家券“的标记外,还会设置商家的id,也就是下单的商品中属于这家的商品才会累计校验金额门槛。
如何实现10Wqps的算券能力? 稍后详细展开,当然,也可以来尼恩的 技术自由圈社群交流。
NFR难题3:如何实现10Wqps的核销能力?
以多点APP为例,来看看如何实现10Wqps的核销能力?
优惠券核销的流程,主要如下:
如何实现10Wqps的核销能力? 稍后详细展开,也可以来尼恩的 技术自由圈社群交流。
NFR难题4:如何实现10Wqps的抢券事务?
TCC,Try-Confirm-Cancel,目前分布式事务主流解决方案。
- 阶段一:Try
对资源进行冻结,预留业务资源
创建订单时,将优惠券状态改为 “冻结”
- 阶段二:Confirm
确认执行业务操作,做真正提交,将第一步 Try 中冻结的资源,真正扣减
订单支付成功,将优惠券状态改为 “已使用”
- 阶段三:Cancel
取消执行业务操作,取消 Try 阶段预留的业务资源
支付失败/超时或订单关闭情况,将优惠券状态改为 “未使用”
如何实现10Wqps的抢券事务?稍后详细展开,也可以来尼恩的 技术自由圈社群交流。
三、抢券系统的微服务架构
抢券的微服务划分
模版模块:提供给运营人员,运营人员根据要求构建优惠券模版
分发模块:面向用户,提供查看、领取、结算、核销优惠券等功能。需要依赖模版微服务与结算微服务
结算模块:结算优惠券,对优惠券规则进行计算
1 模版微服务
运营人员通过设定条件创建优惠券模版,之后生成对应数量优惠券,最后用户才可以去领取优惠券
核心功能:运营人员设定好条件(名称、logo、数量等)创建优惠券模版,后台异步创建对应数量优惠券。创建优惠券过程比较耗时,http接口不返回不是一种很好的体验,所以使用异步操作
使用Redis保存优惠券码,提高效率
查询优惠券信息功能,用于提供给其他微服务调用
2 分发微服务
面向用户,提供查看优惠券、领取优惠券、核销优惠券与结算优惠券功能
查看优惠券
根据不同信息查询对应优惠券模版信息,需要依赖模版微服务
领取优惠券
通过验证,即优惠券模版是可领取的,且成功获取到优惠券码,就将优惠券写入MySQL与Redis
核销优惠券
标记优惠券状态为已使用,更新MySQL与Redis数据
3 结算微服务
调用结算微服务计算优惠券规则
四、抢券系统的表设计
与系统架构对应的,我们需要建立对应的 MySQL 数据存储表。
五: 抢券系统的10Wqps高并发架构亮点
1 服务器架构(应用服务器、数据库、中间件...)
业务从发展的初期到逐渐成熟,服务器架构也是从相对单一到集群,再到分布式服务。
一个可以支持高并发的服务少不了好的服务器架构,需要有均衡负载,数据库需要主从集群,NoSQL缓存需要主从集群,静态文件需要上传CDN,这些都是能让业务程序流畅运行的强大后盾
2 MQ缓冲队列(削峰填谷、系统解耦)
大量需要处理的各种请求涌入到服务端,无论是nosql还是sql都支撑不住,那么就要考虑如何解决这些请求,让他们不会搞垮我们的服务端应用甚至搞垮我们的数据库
由于券模板/券记录都需要展示过期状态,并且根据不同的状态进行业务逻辑处理,因此有必要引入延迟消息队列来对券模板/券状态进行处理。
RocketMQ 支持延时消息,因此我们选用 RocketMQ 作为消息队列。
3 高并发存储
由于券模板、券记录这些都是需要持久化的数据,同时还需要支持条件查询,
所以我们选用通用的结构化存储 MySQL 作为存储中间件。
但是需要做数据分片,数据分库
4 网关(令牌桶)
解决前端到服务端的不确定流量以及瞬时的超大流量,防止服务器血崩
5 缓存(静态缓存、动态缓存)
- 由于发券时需要券模板信息,大流量情况下,不可能每次都从 MySQL 获取券模板信息,因此考虑引入缓存
- 同理,券的库存管理,或者叫库存扣减,也是一个高频、实时的操作,因此也考虑放入缓存中
主流的缓存 Redis 可以满足我们的需求,因此我们选用 Redis 作为缓存中间件。
当然,缓存不单单只是Redis,还需要应用内部的三级缓存等等
6 应用内的代码设计(多线程、锁等等)
7 异常流量的抵御(缓存攻击、数据库攻击...)
布隆过滤器
8 并发测试
高并发相关的业务,需要进行并发的测试,通过大量的数据分析评估出整个架构可以支撑的并发量。
测试高并发可以使用第三方服务器或者自己测试服务器,利用测试工具进行并发请求测试,分析测试数据得到可以支撑并发数量的评估,这个可以作为一个预警参考,俗话说知己自彼百战不殆
其实高并发系统涉及到的知识点还有很多很多,网络抖动?灾备?动态伸缩?等等等等等等...
接下来,我们首先来看看,10Wpqs 抢券系统的核心逻辑的高并发设计
六:10Wpqs 抢券 的核心流程的高并发设计
1 结合redis 高并发缓存,完成领券逻辑设计
领券流程分为三部分:参数校验、幂等校验、库存扣减。
幂等操作用于保证发券请求不正确的情况下,业务方通过重试、补偿的方式再次请求,可以最终只发出一张券,防止资金损失。
2 大流量 10W qps 场景下的问题及解决方案
实现了系统的基本功能后,我们来讨论一下,如果在大流量、高并发的场景下,系统可能会遇到的一些问题及解决方案。
存储瓶颈及解决方案
瓶颈:
在系统架构中,我们使用了 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 单机的压力。
容量预估:
基于上述思路,在要满足发券 12w QPS 的需求下,我们预估一下存储资源。
a. MySQL 资源
在实际测试中,单次发券对 MySQL 有一次非事务性写入,MySQL 的单机的写入瓶颈为 4000,据此可以计算我们需要的 MySQL 主库资源为:
120000/4000 = 30
b. Redis 资源
假设 12w 的发券 QPS,均为同一券模板,单分片的写入瓶颈为 2w,则需要的最少 Redis 分片为:
120000/20000 = 6
3 redis 热点库存问题及解决方案
问题
大流量发券场景下,如果我们使用的券模板为一个,那么每次扣减库存时,访问到的 Redis 必然是特定的一个分片,因此,一定会达到这个分片的写入瓶颈,更严重的,可能会导致整个 Redis 集群不可用。
解决方案
热点库存的问题,业界有通用的方案:即分而治之,扣减的库存 key 不要集中在某一个分片上。
如何保证这一个券模板的 key 不集中在某一个分片上呢,我们拆 key(拆库存)即可。
如图:
在业务逻辑中,我们在建券模板的时候,就将这种热点券模板做库存拆分,后续扣减库存时,也扣减相应的子库存即可。
建券
4:数据分片场景下的库存扣减方案设计
这里还剩下一个问题,即:扣减子库存,每次都是从 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 超时的概率大概在万分之 2、3。因此,这部分发券请求是必然失败的。
解决方案
为了提高这部分请求的成功率,我们有两种方案。
-
一是从 Redis 获取券模板失败时,内部进行重试;
-
二是将券模板信息缓存到实例的本地内存中,即引入二级缓存。
内部重试可以提高一部分请求的成功率,但无法从根本上解决 Redis 存在超时的问题,同时重试的次数也和接口响应的时长成正比。
二级缓存的引入,可以从根本上避免 Redis 超时造成的发券请求失败。
因此我们选用二级缓存方案:
当然,引入了本地缓存,我们还需要在每个服务实例中启动一个定时任务来将最新的券模板信息刷入到本地缓存和 Redis 中,
将模板信息刷入 Redis 中时,要加分布式锁,防止多个实例同时写 Redis 给 Redis 造成不必要的压力。
引入了本地缓存的还有一个问题,要保证本地缓存与redis 之间的数据一致性。
库存扣减,属于读少写多的场景,其实不适合使用二级缓存。
最好的方案,请参见尼恩的Redis分段锁方案。
5 参考秒杀Redis分段锁,设计分库库存扣减方案
秒杀Redis分段锁 , 参见下面的链接。
七:10Wpqs 抢券的缓存优化
本节的方案来自于: 达达集团 千万级QPS-优惠券系统架构设计
优惠券系统C端使用Redis承接高流量,对Redis的深度了解和学习是优化实战的重要一部分。
下面会分别介绍常见的实战优化经验,选取的常用的优化点:大Key、热Key、过期策略。
1 消灭缓存大Key问题
Redis主线程是单线程模型。大Key表面理解为单个存储Key的Value比较大,参考值:String类型长度大于8000;对于List、Set、Hash、Zset的元素数量大于8000。该数据仅供参考,实际业务不同也可有一定调整。
如果一次操作比较大,会导致主线程处理时间变长,单点阻塞;另外大Key的删除或是过期,也会导致节点阻塞,极端情况会导致主从出现问题,Redis无法响应,所以在使用层面根据业务应拆尽拆。
如果是复杂度比较低的应用,定位大Key是比较简单的事情,通常开发人员根据经验就能定位到,但是优惠券系统有200多缓存Key场景,靠经验值很难定位,针对大Key问题下面拿具体案例进行说明,如何定位、如何解决。
案例背景(优惠券适用门店缓存优化)
1.高峰期响应时长高,所有接口都出现问题。
2.某分片新建连接数飙升、流出量高,并且问题分片每天都变化。
3.定位场景复杂:200多个接口无法确定某个业务;缓存Key和接口之间存在多对多关系。
4.Redis没有慢日志,定位问题困难。
5.大Key存在但是不一定是问题Key;热Key也存在但也不一定是问题,如何找到影响性能具体的Key。
排查过程:
1.根据方法耗时最终可以定位到具体集群,但是不能定位到某个缓存Key,因为Redis是单点阻塞,所以表现为该集群所有缓存都变慢。
2.寻找该集群涉及数据源代码中重复调用过程优化,优化后无效。
3.降级+限流无效。
4.使用主从切换方式定位到问题Key,发现分片流出量从主分片到了从分片。定位到券适用门店缓存,此缓存是CouponId为Key,Value是门店ID,接近上万门店,用来判断券和门店是否适用。
解决方案:
将缓存结构Key是CouponId,Value是适用门店结构,改造为缓存Key是CouponId+门店的维度,B端进行一次写入,C端只判断有没有来做到用户已领取的券在不在该门店展示。
结果:总的每秒流出量由几G/s直接降到几十M/s,单分片由几百M/s降低为几M/s。
总结如何发现大Key:
1.业务经验,假设问题Key。
2.客户端代理,收集大Key信息。
3.查看Redis记录的慢日志。
4.在从集群Scan大Key,以免影响生产环境稳定。
5.读写分离笨办法,对怀疑的Key一个一个进行主从分片切换,观察节点的流出量,这个方法虽然不能快速定位,但是一定能帮助定位到问题。
总结解决大Key:
1.根据业务场景解决,散列足够均匀,足够小。个人认为根据场景解决为首选。
2.对于大Key,要想办法进行业务拆解,无法业务拆分的考虑使用技术手段路由拆分。
3.精简缓存Key的长度,使用缩写替代,例如coupon_info_base改为c_i_b。
4.对字段属性进行精简,使用单字母表示,例如couponName对应存储字母a。
5.数据优先使用整数,比字符串省空间。
6.对缓存数据进行冷热分离,比较热的字段放入一个Key,减少请求的网络流出量。
7.在清理大Key数据时,特别注意不要直接删除,也会造成单点阻塞,使用Scan进行清理。4.0以上引入了unlink,异步删除。
2 解决缓存热Key问题
Redis单节点可以承受10万+QPS,如果突然有几十万请求访问某一固定Key,那会达到网卡上限,导致节点无法响应。当然这里的量级也是参考值,和实际场景的Key大小和机器性能许多因素都有关系。常见的突出业务场景是秒杀,其他的业务场景也会存在请求量大的热Key,热Key的发现和处理与大Key有些共通之处,也有不一样之处。
热Key发现:
1.业务经验,假设问题Key。
2.抽样抓取请求,这里分为服务端和客户端两个维度抓取,客户端便向集群维度抓取,服务端便向单节点维度抓取。例如:尝试抓10s内的请求然后按请求量对Key排序。
3.Redis自带命令,Redis 4+版本后提供热点Key发现。
4 .查看Redis记录的慢日志。
5.读写分离笨办法,对怀疑的Key一个一个进行主从分片切换,观察节点QPS变化,这个和大Key发现思路一致。
解决热Key:
1.业务优化减少请求量,例如秒杀,网上有各种降流量措施。
2.缓存隔离,减少热Key对其他业务影响。
3.散列多份,放许多份,读取数据时进行随机读取一份,这个方法比较通用。
4.对于热Key前置缓存到应用服务器上,尽量是占用空间小并且不怎么发生变的数据进行前置,不然Gc也是件比较麻烦的事情。
注意: 实际生产环境会比较复杂,会存在一些场景比较难以定位。例如:不是大Key也不是热Key,但是会是阻塞节点的主要流量Key。
定位方法可以参考读写分离方法,观察节点出流量以及QPS变化。
八:10Wpqs 抢券系统的高并发DB设计
1 优惠券分库分表
随着优惠券发放量越来越大,单表已经达到瓶颈。为了支撑业务的发展,综合考虑,对用户优惠券数据进行分库分表。
关键字:技术选型、分库分表因子
分库分表有成熟的开源方案,这里不做过多介绍。
参考之前项目经验,采用了公司中间件团队提供的自研框架。
原理是引入自研的MyBatis的插件,根据自定义的路由策略计算不同的库表后缀,定位至相应的库表。
用户优惠券与用户id关联,并且用户id是贯穿整个系统的重要字段,因此使用用户id作为分库分表的路由因子。
这样可以保证同一个用户路由至相同的库表,既有利于数据的聚合,也方便用户数据的查询。
假设共分N个库M个表,分库分表的路由策略为:
库后缀databaseSuffix = hash(userId) / M %N
表后缀tableSuffix = hash(userId) % M
2 DB的读写分离操作
以 vivo 亿级优惠券系统为例,介绍一下优惠券系统 DB的读写分离操作
除了上述所说的分库分表外,在此基础上还做了读写分离操作。
主库负责执行数据更新请求,然后将数据变更实时同步到所有从库,用从库来分担查询请求,解决数据库写入影响查询的问题。
主从同步存在延迟,正常情况下延迟不超过1ms,优惠券的领取或状态变更存在一个耗时的过程,主从延迟对于用户来说无感知。
3 用户维度优惠券字段冗余
查询用户相关的优惠券数据是优惠券最频繁的查询操作之一,用户优惠券数据做了分库分表,在查询时无法关联券规则表进行查询,为了减少IO次数,用户优惠券表中冗余了部分券规则的字段。
优惠券规则表字段较多,冗余的字段不能很多,要在性能和字段数之间做好平衡。
4 优惠券V1.0-》V2.0 独立的DB迁移方案
以 vivo 亿级优惠券系统为例,介绍一下优惠券系统独立迁移方案
如何将优惠券从商城系统迁移出来,并兼容已对接的业务方和历史数据,也是一大技术挑战。
系统迁移有两种方案:停机迁移和不停机迁移。
我们采用的是不停机迁移方案:
- 迁移前,运营停止与优惠券相关的后台操作,避免产生优惠券静态数据。
静态数据:优惠券后台生成的数据,与用户无关。
动态数据:与用户有关的优惠券数据,含用户领取的券、券和订单的关系数据等。
- 配置当前数据库开关为单写,即优惠券数据写入商城库(旧库)。
- 优惠券系统上线,通过脚本迁移静态数据。迁完后,验证静态数据迁移准确性。
- 配置当前数据库开关为双写,即线上数据同时写入商城库和优惠券新库。此时服务提供的数据源依旧是商城库。
- 迁移动态数据。迁完后,验证动态数据迁移准确性。
- 切换数据源,服务提供的数据源切换到新库。验证服务是否正确,出现问题时,切换回商城数据源。
- 关闭双写,优惠券系统迁移完成。
迁移后优惠券系统请求拓扑图如下:
九:10Wqps场景的微服务治理
系统开发完成后,还需要通过一系列操作保障系统的可靠运行。
-
超时设置。优惠券系统是一个 RPC 服务,因此我们需要设置合理的 RPC 超时时间,保证系统不会因为上游系统的故障而被拖垮。例如发券的接口,我们内部执行时间不超过 100ms,因此接口超时我们可以设置为 500ms,如果有异常请求,在 500ms 后,就会被拒绝,从而保障我们服务稳定的运行。
-
监控与报警。对于一些核心接口的监控、稳定性、重要数据,以及系统 CPU、内存等的监控,我们会在 Grafana 上建立对应的可视化图表,在春节活动期间,实时观测 Grafana 仪表盘,以保证能够最快观测到系统异常。同时,对于一些异常情况,我们还有完善的报警机制,从而能够第一时间感知到系统的异常。
-
限流。优惠券系统是一个底层服务,实际业务场景下会被多个上游服务所调用,因此,合理的对这些上游服务进行限流,也是保证优惠券系统本身稳定性必不可少的一环。
-
资源隔离。因为我们服务都是部署在 docker 集群中的,因此为了保证服务的高可用,服务部署的集群资源尽量分布在不同的物理区域上,以避免由集群导致的服务不可用。
-
依赖外部接口隔离熔断
优惠券内部依赖了第三方的系统,为了防止因为依赖方服务不可用,产生连锁效应,最终导致优惠券服务雪崩的事情发生,优惠券对依赖外部接口做了隔离和熔断。
十: 优惠券系统高并发压测设计
做完了上述一系列的工作后,是时候检验我们服务在生产环境中的表现了。
当然,新服务上线前,首先需要对服务进行压测。这里总结一下压测可能需要注意的一些问题及压测结论。
注意事项
- 首先是压测思路,由于我们一开始无法确定 docker 的瓶颈、存储组件的瓶颈等。
- 所以我们的压测思路一般是:
- 找到单实例瓶颈
- 找到 MySQL 一主的写瓶颈、读瓶颈
- 找到 Redis 单分片写瓶颈、读瓶颈
得到了上述数据后,我们就可以粗略估算所需要的资源数,进行服务整体的压测了。
- 压测资源也很重要,提前申请到足量的压测资源,才能合理制定压测计划。
- 压测过程中,要注意服务和资源的监控,对不符合预期的部分要深入思考,优化代码。
- 适时记录压测数据,才能更好的复盘。
- 实际的使用资源,一般是压测数据的 1.5 倍,我们需要保证线上有部分资源冗余以应对突发的流量增长。
十一:优惠券系统高可用架构设计
优惠券业务量级的高速发展,对后台系统架构的可用性要求越来越高。在保障微信优惠券业务体验的前提下,优惠券后台系统进行了一系列高可用方面的优化设计。
要保障优惠券系统架构的高可用性,可以从多个方面进行优化和设计。以下是一些详细说明:
-
系统架构设计:
- 信息流、业务流、资金流分离:优惠券系统由信息流、业务流、资金流三部分组成,这三部分在组织架构上应由不同的后台团队完成,以提高系统的模块化和可维护性。
- 微服务架构:采用微服务架构,将优惠券系统拆分成多个独立的服务,每个服务负责特定的功能,这样可以提高系统的可扩展性和容错性。
- 无状态设计:确保优惠券系统的服务是无状态的,即服务不需要保存用户的会话信息,这样可以提高系统的并发处理能力和可靠性。
-
高可用性策略:
- 冗余部署:将系统的关键组件部署在多台服务器上,通过搭建主备或者集群的架构来实现冗余。当主服务器出现故障时,备用服务器能够自动接管,保证系统的可用性。
- 负载均衡:通过将流量分发到多台服务器上,均衡系统的请求负载,提高系统的可用性和扩展性。负载均衡可以通过硬件(如负载均衡器)或者软件(如Nginx、HAProxy)实现。
- 服务容器化:使用容器技术(如Docker、Kubernetes)将应用程序与其依赖项打包为容器,实现快速部署、弹性扩展和自动化管理。容器化可以提高系统的可移植性、弹性和可伸缩性,从而增加系统的高可用性。
-
数据保障:
- 数据备份与恢复:定期对关键数据进行备份,并确保备份的数据可用性。这样,在发生数据丢失或损坏时,可以快速恢复数据,减少系统停机时间。
- 分布式缓存:使用多级缓存技术(如Redis、Memcached等),将数据分别存储在内存缓存、本地缓存和分布式缓存中,以提高访问速度和降低数据库压力。
- 数据一致性:使用分布式锁技术来保护优惠券的领取操作,确保每个用户只能领取一次优惠券。同时,为了保证数据一致性,可以采用消息队列等技术实现请求的异步处理和结果的返回。
-
监控与告警:
- 系统监控:对优惠券系统的关键指标(如请求量、响应时间、错误率等)进行实时监控,以便及时发现和解决潜在问题。
- 告警机制:设置合理的告警阈值,当系统出现异常情况时,及时发送告警通知给相关人员,以便快速响应和处理。
-
安全性保障:
- 访问控制:实施严格的访问控制策略,确保只有授权的用户才能访问优惠券系统。
- 数据加密:对敏感数据进行加密存储和传输,以防止数据泄露和篡改。
- 安全审计:定期对优惠券系统进行安全审计和漏洞扫描,及时修复发现的安全漏洞和隐患。
通过以上措施的综合应用,可以大大提高优惠券系统架构的高可用性,确保系统在高峰时段能够稳定、高效地运行。
十二: 10Wqps高并发系统的架构思考
从零搭建一个大流量、高并发的优惠券系统,首先应该充分理解业务需求,然后对需求进行拆解,根据拆解后的需求,合理选用各种中间件;
本文主要是要建设一套优惠券系统,因此会使用各类存储组件和消息队列,来完成优惠券的存储、查询、过期操作;
在系统开发实现过程中,对核心的发券、券过期实现流程进行了阐述,并针对大流量、高并发场景下可能遇到的存储瓶颈、热点库存、券模板缓存获取超时的问题提出了对应的解决方案。
其中,我们使用了分治的思想,对存储中间件进行水平扩容以解决存储瓶颈;采取库存拆分子库存思路解决热点库存问题;引入本地缓存解决券模板从 Redis 获取超时的问题。
最终保证了优惠券系统在大流量高并发的情景下稳定可用;
除开服务本身,我们还从服务超时设置、监控报警、限流、资源隔离等方面对服务进行了治理,保障服务的高可用;
压测是一个新服务不可避免的一个环节,通过压测我们能够对服务的整体情况有个明确的了解,并且压测期间暴露的问题也会是线上可能遇到的,通过压测,我们能够对新服务的整体情况做到心里有数,对服务上线正式投产就更有信心了。
参考文献
《3万长文 秒杀圣经 : 16大绝招,完成10Wqps秒杀架构 》
《百亿级存储架构:ElasticSearch+HBase 海量存储架构与实现 》
《阿里二面:千万级、亿级数据,如何性能优化?教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
....... 更多架构文章,正在添加中
说在最后:有问题找老架构取经
如何做性能优化?以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。
最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。
很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。
遇到职业难题,找老架构取经, 可以省去太多的折腾,省去太多的弯路。
尼恩指导了大量的小伙伴上岸,前段时间,刚指导一个40岁+被裁小伙伴,拿到了一个年薪100W的offer。
狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。
技术自由的实现路径:
实现你的 架构自由:
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
… 更多架构文章,正在添加中
实现你的 响应式 自由:
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》
实现你的 spring cloud 自由:
《Spring cloud Alibaba 学习圣经》 PDF
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
实现你的 linux 自由:
实现你的 网络 自由:
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》
实现你的 分布式锁 自由:
实现你的 王者组件 自由:
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》