关于架构设计:vivo-全球商城优惠券系统架构设计与实践

40次阅读

共计 5563 个字符,预计需要花费 14 分钟才能阅读完成。

一、业务背景

优惠券是电商常见的营销伎俩,具备灵便的特点,既能够作为促销流动的载体,也是重要的引流入口。优惠券零碎是 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 商城,将来咱们心愿将优惠券能力凋谢,为外部其余业务方提供通用一体化的优惠券平台。

作者:vivo 互联网开发团队 -Yan Chao

正文完
 0