优惠券系统架构设计与实践

article/2025/10/29 6:40:32

技术琐话 2022-01-20 12:03

以下文章来源于vivo互联网技术 ,作者Yan Chao

vivo互联网技术.

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

一、业务背景

优惠券是电商常见的营销手段,具有灵活的特点,既可以作为促销活动的载体,也是重要的引流入口。优惠券系统是vivo商城营销模块中一个重要组成部分,早在15年vivo商城还是单体应用时,优惠券就是其中核心模块之一。随着商城的发展及用户量的提升,优惠券做了服务拆分,成立了独立的优惠券系统,提供通用的优惠券服务。目前,优惠券系统覆盖了优惠券的4个核心要点:创、发、用、计。

  • “创”指优惠券的创建,包含各种券规则和使用门槛的配置。

  • “发”指优惠券的发放,优惠券系统提供了多种发放优惠券的方式,满足针对不同人群的主动发放和被动发放。

  • “用”指优惠券的使用,包括正向购买商品及反向退款后的优惠券回退。

  • “计”指优惠券的统计,包括优惠券的发放数量、使用数量、使用商品等数据汇总。

vivo商城优惠券系统除了提供常见的优惠券促销玩法外,还以优惠券的形式作为其他一些活动或资产的载体,比如手机类商品的保值换新、内购福利、与外部广告商合作发放优惠券等。

以下为vivo商城优惠券部分场景的展示:

二、系统架构及变迁

优惠券最早和商城耦合在一个系统中。随着vivo商城的不断发展,营销活动力度加大,优惠券使用场景增多,优惠券系统逐渐开始“力不从心”,暴露了很多问题:

  • 海量优惠券的发放,达到优惠券单库、单表存储瓶颈。

  • 与商城系统的高耦合,直接影响了商城整站接口性能。

  • 优惠券的迭代更新受限于商城的版本安排。

  • 针对多品类优惠券,技术层面没有沉淀通用优惠券能力。

为了解决以上问题,19年优惠券系统进行了系统独立,提供通用的优惠券服务,独立后的系统架构如下:

优惠券系统独立迁移方案

如何将优惠券从商城系统迁移出来,并兼容已对接的业务方和历史数据,也是一大技术挑战。系统迁移有两种方案:停机迁移和不停机迁移。

我们采用的是不停机迁移方案:

  • 迁移前,运营停止与优惠券相关的后台操作,避免产生优惠券静态数据。

静态数据:优惠券后台生成的数据,与用户无关。

动态数据:与用户有关的优惠券数据,含用户领取的券、券和订单的关系数据等。

  • 配置当前数据库开关为单写,即优惠券数据写入商城库(旧库)。

  • 优惠券系统上线,通过脚本迁移静态数据。迁完后,验证静态数据迁移准确性。

  • 配置当前数据库开关为双写,即线上数据同时写入商城库和优惠券新库。此时服务提供的数据源依旧是商城库。

  • 迁移动态数据。迁完后,验证动态数据迁移准确性。

  • 切换数据源,服务提供的数据源切换到新库。验证服务是否正确,出现问题时,切换回商城数据源。

  • 关闭双写,优惠券系统迁移完成。

迁移后优惠券系统请求拓扑图如下:

三、系统设计

3.1 优惠券分库分表

随着优惠券发放量越来越大,单表已经达到瓶颈。为了支撑业务的发展,综合考虑,对用户优惠券数据进行分库分表。

关键字:技术选型、分库分表因子

分库分表有成熟的开源方案,这里不做过多介绍。参考之前项目经验,采用了公司中间件团队提供的自研框架。原理是引入自研的MyBatis的插件,根据自定义的路由策略计算不同的库表后缀,定位至相应的库表。

用户优惠券与用户id关联,并且用户id是贯穿整个系统的重要字段,因此使用用户id作为分库分表的路由因子。这样可以保证同一个用户路由至相同的库表,既有利于数据的聚合,也方便用户数据的查询。

假设共分N个库M个表,分库分表的路由策略为:

库后缀databaseSuffix = hash(userId) / M %N

表后缀tableSuffix = hash(userId) % M

3.2 优惠券发放方式设计

为满足各种不同场景的发券需求,优惠券系统提供三种发券方式:统一领券接口后台定向发券券码兑换发放

3.2.1 统一领券接口

保证领券校验的准确性

领券时,需要严格校验优惠券的各种属性是否满足:比如领取对象、各种限制条件等。其中,比较关键的是库存和领取数量的校验。因为在高并发的情况下,需保证数量校验的准确性,不然很容易造成用户超领。

存在这样的场景:A用户连续发起两次领取券C的请求,券C限制每个用户领取一张。第一次请求通过了领券数量的校验,在用户优惠券未落库的情况下,如果不做限制,第二次请求也会通过领券数量的校验。这样A用户会成功领取两张券C,造成超领。

为了解决这个问题,优惠券采用的是分布式锁方案,分布式锁的实现依赖于Redis。在校验用户领券数量前先尝试获取分布式锁,优惠券发放成功后释放锁,保证用户领取同一张券时不会出现超领。上面这种场景,用户第一次请求成功获取分布式锁后,直至第一次请求成功释放已获取的分布式锁或超时释放,不然用户第二次请求会获取分布式锁失败,这样保证A用户只会成功领取一张。

库存扣减

领券要进行库存扣减,常见库存扣减方案有两种:

方案一:数据库扣减。

扣减库存时,直接更新数据库中库存字段。

该方案的优点是简单便捷,查验库存时直接查库即可获取到实时库存。且有数据库事务保证,不用考虑数据丢失和不一致的问题。

缺点也很明显,主要有两点:

1)库存是数据库中的单个字段,在更新库存时,所有的请求需要等待行锁。一旦并发量大了,就会有很多请求阻塞在这里,导致请求超时,进而系统雪崩。

2)频繁请求数据库,比较耗时,且会大量占用数据库连接资源。

方案二:基于redis实现库存扣减操作。

将库存放到缓存中,利用redis的incrby特性来扣减库存。

该方案的优点是突破数据库的瓶颈,速度快,性能高。

缺点是系统流程会比较复杂,而且需要考虑缓存丢失或宕机数据恢复的问题,容易造成库存数据不一致。

从优惠券系统当前及可预见未来的流量峰值、系统维护性、实用性上综合考虑,优惠券系统采用了方案一的改进方案。改进方案是将单库存字段分散成多库存字段,分散数据库的行锁,减少并发量大的情况数据库的行锁瓶颈。

库存数更新后,会将库存平均分配成M份,初始化更新到库存记录表中。用户领券,随机选取库存记录表中已分配的某一库存字段(共M个)进行更新,更新成功即为库存扣减成功。同时,定时任务会定期同步已领取的库存数。相比方案一,该方案突破了数据库单行锁的瓶颈限制,且实现简单,不用考虑数据丢失和不一致的问题。

一键领取多张券

在对接的业务方的领券场景中,存在用户一键领取多张券的情形。因此统一领券接口需要支持用户一键领券,除了领取同一券模板的多张,也支持领取不同券模板的多张。一般来说,一键领取多张券指领取不同券模板的多张。在实现过程中,需要注意以下几点:

1)如何保证性能

领取多张券,如果每张券分别进行校验、库存扣减、入库,那么接口性能的瓶颈卡在券的数量上,数量越多,性能直线下降。那么在券数量多的情况下,怎么保证高性能呢?主要采取两个措施:

a. 批量操作

从发券流程来看,瓶颈在于券的入库。领券是实时的(异步的话,不能实时将券发到用户账户下,影响到用户的体验还有券的转化率),券越多,入库时与数据库的IO次数越多,性能越差。批量入库可以保证与数据库的IO的次数只有一次,不受券的数量影响。如上所述,用户优惠券数据做了分库分表,同一用户的优惠券资产保存在同一库表中,因此同一用户可实现批量入库。

b. 限制单次领券数量

设置阀值,超出数量后,直接返回,保证系统在安全范围内。

2)保证高并发情况下,用户不会超领

假如用户在商城发起请求,一键领取A/B/C/D四张券,同时活动系统给用户发放券A,这两个领券请求是同时的。其中,券A限制了每个用户只能领取一张。按照前述采用分布式锁保证校验的准确性,两次请求的分布式锁的key分别为:

用户id+A_id+B_id+C_id+D_id

用户id+A_id

这种情况下,两次请求的分布式锁并没有发挥作用,因为锁key是不同,数量校验依旧存在错误的可能性。为避免批量领券过程中用户超领现象的发生,在批量领券过程中,对分布锁的获取进行了改造。上例一键领取A/B/C/D四张券,需要批量获取4个分布式锁,锁key为:

用户id+A_id

用户id+B_id

用户id+C_id

用户id+D_id

获取其中任何一个锁失败,即表明此时该用户正在领取其中某一张券,需要自旋等待(在超时时间内)。获取所有的分布式锁成功,才可以进行下一步。

接口幂等性

统一领券接口需保证幂等性(幂等性:用户对于同一操作发起的一次请求或者多次请求的结果是一致的)。在网络超时、异常情况下,领券结果没有及时返回,业务方会进行领券重试。如果接口不保证幂等性,会造成超发。幂等性的实现有多种方案,优惠券系统利用数据库的唯一索引来保证幂等。

领券最早是不支持幂等性的,表设计没有考虑幂等性。

那么第一个需要考虑的问题:在哪个表来添加唯一索引呢?

无非两种方案:现有的表或者新建表。

  • 采用现有的表,不需要增加表的关联。但如上所述,因为做了分库分表,大量的表需要添加唯一字段,并且需要兼容历史数据,需要保证历史数据新增字段的唯一性。

  • 采用新建表这种方式,不需要兼容历史数据,但缺陷也很明显,增加了一层表的关联,对性能和现有逻辑都有很大影响。综合考虑,我们选取了在现有表添加唯一字段这种方式,这样更利于保证性能和后续的维护性。

第二个考虑的问题:怎么兼容历史数据和业务方?历史数据增加了唯一字段,需要填入唯一值,不然无法添加唯一索引。我们采用脚本刷数据的方式,构造唯一值并刷新到每一行历史数据中。优惠券已对接的业务方没有传入唯一编码,针对这种情况,优惠券侧生成唯一编码作为替代,保证兼容性。

3.2.2 定向发券

定向发券用于运营在后台针对特定人群进行发券。定向发券可以弥补用户主动领券,人群覆盖不精准、覆盖面不广的问题。通过定向发券,可以精准覆盖特定人群,提高下单转化率。在大促期间,大范围人群的定向发券还可以承载活动push和降价促销双重任务。

定向发券主要在于人群的圈选和发券流程的设计,整体流程如下:

定向发券不同于用户主动领券,定向发券的量通常会很大(亿级)。为了支撑大批量的定向发券,定向发券做了一些优化:

1)去除事务。事务逻辑过重,对于定向发券来说没必要。发券失败,记录失败的券,保证失败可以重试。

2)轻量化校验。定向发券限制了券类型,通过限制配置的方式规避需严格校验属性的配置。不同于用户主动领券校验逻辑的冗长,定向发券的校验非常轻量,大大提升发券性能。

3)批量插入。批量券插入减少数据库IO次数,消除数据库瓶颈,提升发券速度。定向发券是针对不同的用户,用户优惠券做了分库分表,为了实现批量插入,需要在内存中先计算出不同用户对应的库表后缀,数据归集后再批量插入,最多插入M次,M为库表总个数。

4)核心参数可动态配置。比如单次发券数量,单次读库数量,发给消息中心的消息体包含的用户数量等,可以控制定向发券的峰值速度和平均速度。

3.2.3 券码兑换

站外营销券的发放方式与其他券不同,通过券码进行兑换。券码由后台导出,通过短信或者活动的方式发放到用户,用户根据券码兑换后获取相应的券。券码的组成有一定的规则,在规则的基础上要保证安全性,这种安全性主要是券码校验的准确性,防止已兑换券码的再次兑换和无效券码的恶意兑换。

3.3 精细化营销能力设计

通过标签组合配置的方式,优惠券提供精细化营销的能力,以实现优惠券的千人千面。标签可分为准实时和实时,值得注意的是,一些实时的标签的处理需要前提条件,比如地区属性需要用户授权。

优惠券的精准触达:

3.4 券和商品之间的关系

优惠券的使用需要和商品关联,可关联所有商品,也可以关联部分商品。为了灵活性地满足运营对于券关联商品的配置,优惠券系统有两种关联方式:

a. 黑名单。

可用商品 = 全部商品 - 黑名单商品。

黑名单适用于券的可使用商品范围比较广这种情况,全部商品排除掉黑名单商品就是券的可使用范围。

b. 白名单。

可用商品 = 白名单商品。

白名单适用于券的可使用商品范围比较小这种情况,直接配置券的可使用商品。

除此以外,还有超级黑名单的配置,黑名单和白名单只对单个券有效,超级黑名单对所有券有效。当前优惠券系统提供商品级的关联,后续优惠券会支持商品分类维度的关联,分类维度 + 商品维度可以更灵活地关联优惠券和商品。

3.5 高性能保证

优惠券对接系统多,存在高流量场景,优惠券对外提供接口需保证高性能和高稳定性。

多级缓存

为了提升查询速度,减轻数据库的压力,同时为了应对瞬时高流量带来热点key的场景(比如发布会直播结束切换流量至特定商品商详页、热点活动商品商详页都会给优惠券系统带来瞬时高流量),优惠券采用了多级缓存的方式。

数据库读写分离

优惠券除了上述所说的分库分表外,在此基础上还做了读写分离操作。主库负责执行数据更新请求,然后将数据变更实时同步到所有从库,用从库来分担查询请求,解决数据库写入影响查询的问题。主从同步存在延迟,正常情况下延迟不超过1ms,优惠券的领取或状态变更存在一个耗时的过程,主从延迟对于用户来说无感知。

依赖外部接口隔离熔断

优惠券内部依赖了第三方的系统,为了防止因为依赖方服务不可用,产生连锁效应,最终导致优惠券服务雪崩的事情发生,优惠券对依赖外部接口做了隔离和熔断。

用户维度优惠券字段冗余

查询用户相关的优惠券数据是优惠券最频繁的查询操作之一,用户优惠券数据做了分库分表,在查询时无法关联券规则表进行查询,为了减少IO次数,用户优惠券表中冗余了部分券规则的字段。优惠券规则表字段较多,冗余的字段不能很多,要在性能和字段数之间做好平衡。

四、总结及展望

最后对优惠券系统进行一个总结:

  • 不停机迁移,平稳过渡。自独立后已稳定运行2年,性能足以支撑vivo商城未来3-5年的高速发展。

  • 系统解耦,迭代效率大幅提升。

  • 针对业务问题,原则是选择合适实用的方案。

  • 具备完善的优惠券业务能力。

展望:目前优惠券系统主要服务于vivo商城,未来我们希望将优惠券能力开放,为内部其他业务方提供通用一体化的优惠券平台。

往期推荐:

  • 一个bug 导致 77TB数据被删光......

  • 技术领导力就是“成事”的能力

  • 34岁回顾人生,也怕中年危机!

  • 2022年第1件全民喜讯:个税延收,国家发钱了!

  • 扛住100亿次红包请求的后端架构设计

  •    ……

技术琐话 

以分布式设计、架构、体系思想为基础,兼论研发相关的点点滴滴,不限于代码、质量体系和研发管理。

技术琐话

最干货的java+分布式技术公众号,兼及研发管理。本号专家阵容:蚂蚁金服右军、NETSTARS CTO陈斌、江苏百瑞赢李伟山、前蚂蚁金服高级技术专家张翔、美团高级技术专家杨彪等。

258篇原创内容

公众号


http://chatgpt.dhexx.cn/article/JIVkci9M.shtml

相关文章

python3.5 将py文件转换成EXE文件

方法其实就是利用pyinstaller和pywin32 1.安装pyinstaller 输入cmd进入windows命令提示符,如果没有设置python环境变量,则转到python安装目录的scripts目录下, 如果已经设置python环境变量,可以跳过上述步骤。然后输入 pip inst…

把python文件转exe文件 和 打包报错处理

今天,我教大家怎么把py文件转换成exe文件, 首先,我们要用到一个第三方库,他就是pyinstaller pip install pyinstaller zip包下载(CSDN)免费 Pyinstaller代码压缩包-Python文档类资源-CSDN下载 此外&am…

python封装成exe

最近写了一个小小的程序,需要进行封装exe,为了简单,就直接用了pyinstaller这个模块,对于python3.6版本的童鞋来说,简直方便的不要。下面就给大家介绍一下如何用pyinstaller去封装程序为exe程序。 首先,需要…

python如何封装成exe

python文件封装成exe 第一种:.py文件直接封装成exe第二种:整个项目封装成exe ) 第一种:.py文件直接封装成exe 1.cmd进入py文件所在的目录 备注:在py文件所在的目录下,按住shift鼠标右击,然后找到“在此处…

Python打包成exe文件_详细操作

Python打包成exe文件 前言一、安装pyinstaller1.1 安装pyinstaller,使用安装命令:1.2 如果遇到需要更新版本请输入:1.3 检查是否正确安装1.4 稍等,水一下; 二、python打包成exe文件(附带图标打包)2.1 第一种…

将Python文件转化为exe文件

第一步:首先安装相关工具 在Windos中打开命令行窗口 输入pip install pyinstaller 出现以下情况表示安装成功 第二步:打包成exe文件 若需将xxx.py文件打包,只需在终端执行: 注:终端需切换至xxx.py文件所在目录下。…

如何将python的.py文件转换为可执行的.exe文件。

首先,我写了一个print(“hello,world”).py文件。命名为hello.py保存在我的电脑C盘的C:\Users\ly目录下如图所示。 ps:尽量选择在这个文件夹下,如果选择其他盘的文件夹下,生成的.exe的dist文件夹也会出现在…

将.py文件转化为.exe文件

将.py文件转化为.exe文件首先需要第三方库 pyinstaller 1.如果没有安装pyinstaller,则在命令提示符输入 pip installer pyinstaller 如果已安装,则会显示 表示这几个都安装好了。 2.下载安装pyinstaler运行时所需windows扩展:pywin32 mham…

[272]如何把Python脚本导出为exe程序

文章目录 一.pyinstaller简介二.pyinstaller在windows下的安装三.打包四.小实例(windows下)附加 一.pyinstaller简介 pyinstaller将Python脚本打包成可执行程序,使在没有Python环境的机器上运行 最新版是pyinstaller 3.1.1。支持python2.7和…

把py转化成exe(干货)

问题提出: 无奈别人的电脑没有装python编译环境,无法运行py文件。   本教程为在window环境下,使用PyInstaller3.4将Python脚本打包成exe可执行程序。 使用命令: pip install pyinstaller   上图为成功安装效果图&#xff0…

简单3步将你的python转成exe格式

使用pyinstaller可以将你的.py文件直接转换成.exe格式,在没有部署python环境的windows机器上直接运行你的程序!! 废话不多说,直接上正文。 1. 在命令行用pip安装 pyinstaller包 pip install pyinstaller 2.下载安装pyinstaler…

.py文件转为.exe文件的方法

写完一个Python程序想给朋友看?朋友电脑里没Python运行不了? 别急,这篇文章来帮你。 把你的python文件转成exe文件,打开方便,瞬间提示一个档次。 (我个人也遇到过这样的问题,看了别人的博客解…

将Python程序打包成exe文件

我新写了一篇更加完整的文章,与这篇文章相比,它新增了两种打包方式:多python文件打包和含有资源文件的打包方式,具体请戳链接: 用 Pyinstaller 模块将 Python 程序打包成 exe 文件(全网最全面最详细&#x…

Python 将.py转换为.exe详解

本文是由熊猫大哥大的博客修改而来(所以就不能说是“原创”了) 原博客链接:Python学习笔记(15)-Python代码转换为exe可执行程序详解(下面会提到本文与其的一点不同) 建议看看原博客,再看此文,…

Python学习笔记(15)-Python代码转换为exe可执行程序详解

点此查看 零基础Python全栈文章目录及源码下载 本文目录 1. 简介2. pyInstaller安装配置2.1 下载PyInstaller2.2 下载后解压PyInstaller2.3 下载pywin322.4 安装pywin322.5 安装PyInstaller 3. 制作exe3.1 编写python文件3.2 将代码文件放至pyinstaller目录3.3 制作exe3.4 查看…

如何将py文件转化为exe

如何将py文件转化为exe 第一步:安装pyinstaller库。 首先,winr打开运行窗口,输入 cmd,在cmd窗口中输入python -m pip install pyinstaller 等待计算机安装,完成后会有successfully等字眼 第二步:打包文件…

python文件转换成exe可执行的windows文件

一、介绍 python的程序需要运行环境有时候很不方便,因为要交给别人代码可能因为环境的原因运行各种问题,这里给出直接讲python程序转换成exe文件,很方便直接给执行程序就可以,也不用担心运行环境问题而运行不了 二、工具 1、安装…

将Python文件编译为exe可执行程序

Python程序py格式文件的优点是可以跨平台,但运行必须有Python环境,没有Python环境无法运行py格式文件。有没有方法,用户不同安装Python就可直接运行开发的项目工程?答案是肯定的。这就涉及到需要将Python的.py格式文件编写的脚本编…

python:将.py文件转换成.exe文件

一、使用pip安装PyInstaller ①python3.x需要使用PyInstaller才能进行转exe文件。 ②在命令里安装PyInstaller 如下图,我是在Anaconda的python环境下,直接在Anaconda的命令行窗口下,用命令pip install pyinstaller安装的。 也可以如下图&a…

如何将python程序打包成exe文件

将py打包为exe文件需要依赖pyinstaller 首先介绍以下pyinstaller的命令 -F:打包后只生成单个exe格式文件;-D:默认选项,创建一个目录,包含exe文件以及大量依赖文件;-c:默认选项,使用…