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

Posted 技术琐话

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了优惠券系统架构设计与实践相关的知识,希望对你有一定的参考价值。

一、业务背景


优惠券是电商常见的营销手段,具有灵活的特点,既可以作为促销活动的载体,也是重要的引流入口。优惠券系统是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开发手册里面看到关于系统设计可扩展性的一段话,谈到设计可扩展性的本质是找到系统的变化点,并隔离变化点;极致扩展性的标志,就是需求新增时,无需在原有代码交付物上进行任何形式的修改。这段话是阿里技术架构师的经验总结,他们用实际案例告诉我们, 想要实现业务的快速支撑,系统的可扩展能力不可忽视

    对于大多数架构师而言,可扩展的系统架构设计有一个朴素的认识,就是解决服务横向扩展、容量、可用性及性能瓶颈问题,虽简单粗暴,却也是最有效的方式,就是我们俗称的“加机器”。这一认识,在实际生产运作中,变成了一种很普遍的做法。对于一个规模和数据量都迅速增长的系统而言,高性能和高可用问题自然是我们要优先考虑的。但是随着时间的推移,业务 的不断发展,面临的业务场景越来越复杂,为了解决这些复杂的业务问题,我们的实现方案也越来越复杂,理解、维护、迭代的难度随之增加,加速了系统腐化速度。

    最终摆在我们面前的问题,除了要解决性能慢与体验差的问题,还需要面对功能与代码指数级增长带来的系统扩展性问题以及业务变化带来的差异化服务问题。而很多系统,在架构设计时并未充分考虑到这些问题,存在平台代码和业务代码耦合严重难以分离、业务和业务之间代码交织缺少拆解的现象,导致系统的重构、代码推倒重来成为常态,从而影响业务交付能力,还浪费人力财力。

    同样在实际开发过程中,可扩展性问题也经常被开发人员忽略掉,满足业务需求最简单的处理,就是在代码中用“if else”分支来处理自己的逻辑思维,“if else”是开发人员写代码时,使用频率最高的关键词之一,然而有时过多的“if else”会让我们感到脑壳疼。当我们去review代码时,都会发现类似一个业务处理类上万行代码,一个函数方法上千行代码的场景。缺少组件化、代码隔离,通常都是直接在核心代码中逐块增加业务代码,这种写法在很短时间内会让核心服务“太胖”,复用性较差甚至无法复用,核心代码的频繁变动,需要全量部署发布,容易导致版本不稳定、代价高、风险大。

    根据实际经验总结,不论从代码质量、可读性、可扩展性还是从可维护性、开发效率方面看,缺少代码抽象、扩展性考虑,采取简便直接上线的方法所需的额外返工,从长远来看都会很糟糕。

    在聊架构的时候,我们一定需要聊一聊架构设计的目的,在我看来,为什么要实现架构设计,用一句话来概括就是"架构设计的真正目的是为了解决系统的复杂度带来的问题,控制好复杂度,就可快速实现业务创新。"我们往往有“为了高性能、高可用、高扩展,所以要做架构设计”这样的想法并在实际当中这么做,不管什么系统,不管什么业务场景,上来就要去“三高”,那么将非常有可能会让架构设计过于复杂,项目落地遥遥无期。历尽千辛万苦,最终客户不满意,老板不高兴,那也是白搭了。

    至此,我们简单的对为什么要研究思考高扩展架构进行了说明,可扩展性是衡量架构设计好与坏的一个重要因素,但是一个系统要在一开始就设计出比较好的可扩展性是有一定难度的,可扩展性体现在不同层次、不同维度上,有大有小,它不是个体行为,需要依靠团队共同完成。任何系统最开始相对都是简单的,随着业务增加,慢慢开始变的复杂起来。我们需要在这个循序渐进的过程中,在极其不确定的业务变化中,寻找到真正的可扩展点,让架构具备高扩展性。

    实现高扩展架构的目标、原则和方法是什么?



    扩展性涉及面非常广,突破系统架构的扩展性约束是构建复杂业务系统必须解决的难题之一。一份良好可扩展的架构方案绝对是无价的,我们定义高扩展架构的目标是让系统架构简单清晰、应用系统间耦合低、容易水平扩展、业务功能增改方便快捷,实现服务/模块/代码级动态化、热插拔管理机制,提高研发效率,快速响应业务需求,提升客户感知。

    在系统扩展性设计方面我们定义了几个关键原则:
    鲸品堂|复杂业务系统高扩展架构设计与实践

    1、解耦拆分原则:稳定部分与易变部分分离;核心业务与非核心业务分离;主流程与辅流程分离;应用与数据分离;服务与实现细节分离。

    鲸品堂|复杂业务系统高扩展架构设计与实践

    2、松耦合原则:跨域调用异步化,不同业务域之间尽量异步解耦。非核心业务尽量异步化,核心和非核心业务之间,尽量异步解耦。

    鲸品堂|复杂业务系统高扩展架构设计与实践

    3、服务依赖原则:核心服务不依赖非核心服务;非核心服务可依赖核心服务;稳定部分不依赖易变的部分、容易变的部分可以依赖稳定的部分。

    鲸品堂|复杂业务系统高扩展架构设计与实践

    4、服务自治原则:服务能彼此独立修改、部署、发布和管理。避免引发连锁反应。

    对于实施可扩展架构,一些简单而有效的方法:
    鲸品堂|复杂业务系统高扩展架构设计与实践

    1、要定义规范:明确代码扩展性规范与约束,给出示例,引导开发人员在实现业务逻辑时,能够进行结构化、抽象化思考,保障代码的可维护性和可读性。

    鲸品堂|复杂业务系统高扩展架构设计与实践

    2、要搭好基础框架:高扩展架构有很多成功的案例,但实施起来没有标准,不能复制,也难以衡量,所以我们需要结合我们自身规范,自身需求,搭建基础框架,降低技术门槛,让开发人员关注业务逻辑编码。

    鲸品堂|复杂业务系统高扩展架构设计与实践

    3、要引入设计模式:应用面向对象思想,原则,使用常见设计模式,进行代码层面的设计。

    鲸品堂|复杂业务系统高扩展架构设计与实践

    4、要找到扩展点:结合业务需求,识别易变化的点,梳理扩展点清单,对于存在业务变动的代码,利用扩展点重构原有代码,抽象稳定基础能力。

    鲸品堂|复杂业务系统高扩展架构设计与实践

    5、要做好代码审查:对代码的审查内容很多,在关注代码的编写是否规范、技术处理规范、业务逻辑实现等的同时,考虑扩展设计来阻止代码腐化。


    为了实现高扩展的架构,我们做了哪些实践?



    基于上述考虑,我们今年在一些新产品、新项目建设过程中,做了一些关于可扩展性架构的实践,尝试对传统架构模式进行变革,为后续打造新一代产品架构做好准备。

    高扩展实践一:通过领域分层设计,将业务规则分离出来,抽象在领域层,提升系统的可读性、复用性和扩展性。

    传统三层架构,非常简单,基本没有太多开发门槛,在它带来便捷的同时,也带来了一些不利因素,其中之一就是开发人员缺乏对业务场景的深度理解以及对该类问题的抽象思考,当再次遇到同类功能时,都需要重复做一遍。长此以往,相似的代码模块也会很多,十分不利于系统的升级维护。

    在我们已有的微服务架构中,我们尝试着以一种更轻量级的领域设计来融合到微服务系统设计中。通过领域模式的核心思想,来管理业务域的核心逻辑,在概念上保留领域对象、基础设施、领域服务、领域事件,同时领域对象采用贫血模型,通过领域方法来描述领域能力,逻辑功能高度内聚。同时,在领域服务层,我们分离读和写,只有写服务依赖领域能力来实现核心的状态变更,读服务直接基于基础设施层来提供能力。分层架构示意图如下:

    鲸品堂|复杂业务系统高扩展架构设计与实践

    通过这样的分层,我们在层次间的依赖上面,保持了足够的灵活性;而在核心的业务逻辑上,也具备领域能力的高度内聚,保证了一定的复用性和扩展性。同时,也降低了对开发人员的要求,让对领域模型理解不深的人员也能保证一定的完成质量。系统实际分层架构如下图所示:
    鲸品堂|复杂业务系统高扩展架构设计与实践
    鲸品堂|复杂业务系统高扩展架构设计与实践

    基于领域模式实现功能时,经常遇到的问题之一,是哪些逻辑应该放在领域内?如果把所有业务逻辑都放到领域内,那过度膨胀的领域就失去了自身表达的意义。我们在实践中,通常会先将业务逻辑拆分为原子的功能点和控制流程,将明确属于领域内的逻辑合并,将不明确的功能点放在应用层,在后续迭代中再根据业务沉淀模型能力。

    在分层设计实现中,我们需要将领域逻辑与业务场景流程控制分离。在领域层实现核心业务功能;在应用层通过流程控制聚合各个领域,实现特定业务场景,同时在应用层实现不属于领域内的业务场景细节逻辑。流程控制方面需要结合业务,原则上以简洁实用为主,保证既能满足业务功能,又能保持扩展性和可读性。在我们业务中,大部分业务场景是基于领域能力组合实现,少部分业务场景我们引入了轻量流程引擎、状态机、规则引擎、策略控制模式等。

    DDD虽然有很多优点,但是我们在实践和持续迭代过程中也遇到一些问题。最明显的问题是DDD对设计人员要求较高,需要设计人员对领域模型和业务知识有较深入的思考与理解,才能设计出符合领域规范的实现方案。在理解不充分时,会出现生搬硬套现象,代码最终的实现往往会变成“四不像”,不仅不能合理表达领域的能力,而且还会因为未正确实现约束导致代码混乱。

    高扩展实践二:面向功能拆分,产生微内核架构,实现业务和平台分离、业务和业务分离。

    以最近我们新上线的促销中心为例讲解高扩展性系统设计与实现。促销中心是一个平台型的业务系统,要做到业务与业务的隔离、业务与平台的隔离。

    为了达成这个目标,我们自研了一套插件框架,促销中心基于这个框架构建了促销业务调度框架及业务插件库。接入这个框架后,平台逻辑和业务逻辑得到了分离,各种促销类型(满减、优惠等)之间的逻辑也不再耦合,而是变成jar包级别隔离,同时支持了基于促销类型(策略)的动态路由模式。

    促销平台核心程序负责促销计算初始化、促销规则执行、促销记录生成、促销分析等,实现了由主流程框架统一进行调度。我们把各种类型促销业务从核心程序中单独抽取出来,做成一个个业务插件。通过这种方式,也将核心程序和应用程序彻底进行了分离。核心程序和应用程序是通过接口联系起来,这层接口都是“声明”,包括接口定义、数据对象的定义和扩展点的默认执行策略定义等。促销平台架构如下图所示:

    鲸品堂|复杂业务系统高扩展架构设计与实践
    鲸品堂|复杂业务系统高扩展架构设计与实践
    此外,插件框架支持在应用不停的情况下新增、更新、卸载业务插件,无感知进行升级,满足业务快速上线要求,系统更加稳定性、业务接入更加容易。各业务系统统一使用插件框架提供的插件包发布页面,完成业务插件包的装载。流程如下图所示:
    鲸品堂|复杂业务系统高扩展架构设计与实践
    鲸品堂|复杂业务系统高扩展架构设计与实践

    高扩展实践三:搭建业务能力运营平台,实现管理域和运行域分离架构,让业务快速接入、快速扩展。

    管理域指的是系统业务管理员配置业务流程、参数、规则或业务扩展点的实现工具的代码运行空间。运行域指的是系统业务逻辑处理程序的代码运行空间,为了支撑实际业务运行而存在的,而管理域则是为了对运行域的动态调整控制而存在的。

    在应用的分层架构中,我们可以从用户能够接触的层次往系统抽象层次依次分为:页面、功能、能力、数据模型。在不同的业务场景下,有大量的业务模块是可以在这四个维度中的某几个进行共用的,那我们就可以在架构上做一项考虑:把每一个应用中的页面、功能、能力和数据模型都打散,分别沉淀到对应的页面、功能、能力、数据模型的共性库中,这样以后每次来一个新的业务只要通过从共性库中选择自己所需要的页面、功能、能力、数据模型就可以快速搭建匹配特定业务逻辑的应用。

    按管理和运行域划分,页面、功能、能力、数据模型都是属于运行域的内容,而由于这些内容在业务不断的演化过程中会越来越丰富,所以我们需要提供一个平台来对这些内容进行管理,随之管理域就出现了。如图基于管理域和运行域分离的应用架构示意:

    在管理域中有如下三个基础功能模块: 元数据管理、能力管理、组件管理。

    首先, 元数据管理 主要完成对数据模型的管理,包括对内置对象的数据结构进行扩展、部分字段的定义,也包括对应用中的业务对象进行定义、字段设置等功能,在整个业务逻辑中元数据只是作为配置参数存在。

    其次, 能力管理 主要是实现能力运营及功能扩充的管理,主要包括两部分,第一是对如业务系统透出的能力进行注册和发现,第二是给业务系统预留接口,可让应用自己进行定制化开发实现业务逻辑,再通过机制进行注册和发现并提供给应用使用。具体的实现机制可参考 Java 中的 SPI 机制,简单来说步骤如下:首先在代码中使用统一注解,通过扫描可以自动把注解部分的代码上报到能力管理实现注册;在能力对应的代码进行修改了之后,扫描机制也能在一定的时机进行更新;然后通过统一的编码规范,对某些类预留抽象类,可暴露给应用方自己进行实现;应用方对抽象类进行实现后,可通过扫描机制发现,并让应用进行调用。

    最后, 组件管理 主要可以分为页面组件和服务组件的管理。

    前端的组件最终是以页面编辑器的方式进行实现,在此不做详细表述,而管理后台的页面组件库则需要与全局管理中的资源、菜单、角色管理配合满足业务的需求。其核心业务逻辑在于,所有系统中的页面、菜单、页签、按钮以及其它页面元素都可以由统一的提供方提供,放到公共库中,使用方只需要按照标准进行调用即可,能够实现最大限度的资源复用。

    功能服务组件则是把业务和平台分离的关键,大量具有业务特性的聚合服务从核心业务逻辑中抽离,形成一个一个的服务组件,以插件的方式装配到主干业务流程中,由于使用统一的标准进行开发,在业务组件中预设大量的配置项,通过自动注册和发现机制实现配置项自动化上报,而组件一被生产方生产出来后就会进入统一的服务组件库中,能够被任一业务方进行调用。

    通过以上三个能力,能够实现对数据模型、能力、功能以及页面的统一管理的需求,只是对这些内容管理还是不够的,不能实现最终目的,在能够对其进行管理之后,我们需要能够按业务需求通过配置化、低代码的方式把这些内容组装拼接起来,以满足业务的实际需求。

    为了解决不同业务动态化配置的问题我们需要引入业务身份这一概念,业务身份主要是把前后台的应用放在同一个空间下进行管理,我们通过管理域可在相应业务身份下进行业务逻辑的配置,这些配置可以让系统中包含的各个应用在运行域时能够识别所需要使用的页面、功能、能力和数据模型,能很好地运用业务身份进行业务逻辑的扩展。

    总结展望



    高扩展性”作为今年我们团队的三大架构研究方向之一,结合实际项目已经有了应用落地,目前业务系统已上线运行,我们正在积极将实践框架和经验复制到更多的产品上。基于框架能够实现增量式开发、增量式发布,解决了一直以来业务系统代码交织耦合、难以拆解的困扰。在代码质量和开发效率上也取得了显著提升,业务支撑更加敏捷、快速迭代。

    “没有最好,只有最合适!回到最开始说的那句话,架构设计的真正目的是为了解决系统的复杂度带来的问题,不是为了追求所谓的高大上。就像任何一种复杂的架构一样,解决扩展性问题都不存在什么银弹。以业务需求作为指引,做出权衡和妥协对于实现高扩展性来说至关重要。高扩展性设计不仅仅是一个技术问题,它更像是一种模式,可以是思维模式,也可以是运行模式。高扩展架构是我们继续努力的方向,我们将持续进行探索。

    以上是关于优惠券系统架构设计与实践的主要内容,如果未能解决你的问题,请参考以下文章

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

    浅谈优惠券系统设计

    深入剖析优惠券核心架构设计

    深入剖析优惠券核心架构设计

    架构师内功心法,属于游戏设计模式的策略模式详解

    架构师内功心法,属于游戏设计模式的策略模式详解