背景
营销类业务中经常有一些抽奖活动类的玩法,如大转盘、红包雨、老虎机、摇一摇等。在业务支持过程中,经常遇到需求频繁变更,定制化需求多等问题。
比如某次抽奖需求:
1.每个用户都有多次抽奖机会,但每次的概率又不同;
2.抽奖活动周期内,为控制预算并保证活动效果,对每天的奖品数量进行控量;
3.为应对“非正常”用户,在每天指定的时间段,降低中奖概率;
4.……
等等的此类抽奖玩法需求,每场抽奖活动形式大同小异,但背后的业务需求又存在差异,定制化程度特别高。
早期我们的处理方式是每次都针对需求重新开发,那时需求较为简单,抽奖活动数量也较少。随着业务规模日益扩大,抽奖活动频率越来越高,我们先开发一套简单的配置式后台,支持简单的奖品和概率的配置。
但是实际使用中,也有一些问题,比如:
1.差异化抽奖规则很多,根本无法通过简单的概率配置,满足业务需求,定制化成本很高;
2.对于大型专题中抽奖活动的并发控制、数据一致性等提出很多挑战;
3.兄弟业务线也有类似的需求,但基本需要重新实现一遍,系统复用性很低。
考虑到以上问题,加之抽奖玩法是一个相对独立、应用场景广泛的业务,于是我们开发一套通用的抽奖系统,提供系统级业务复用的能力,系统单独维护、单独部署,保证通用性和可伸缩性。下面从:
系统概述
并发控制
可扩展性
配套设施
等几个方面介绍一下这个系统。
系统概述
为保证整体的灵活性,本系统采用领域模型驱动设计(DDD),对系统中不同业务模块进行抽象:
图片1 抽奖请求流
上图中的业务概念:
活动:抽奖活动的主体,多数场景下,一场抽奖活动就是一个活动对象。
轮次:与活动是一对多的关系,例如:一场秒杀活动,可能有多个场次,多个场次可以只报名一次,但每个场次的奖品又不同。类似场景,可以采用多个轮次解决。
轮次奖品:多轮抽奖,奖品虽然一样,但是概率不同。于是引入轮次奖品的概念,不同轮次就可以针对同一个奖品,设置不同的概率。库存同理,在轮次+奖品的维度维护其对应关系,而不是直接绑定在奖品上。不同轮次可选相同的奖品、不同的库存。
概率组:多条抽奖概率信息,组成一个概率组,如:用户首次抽奖,中奖概率是50%,第2到第5次抽奖,中奖概率提高为70%,于是系统把概率独立出来,并引入概率组的概念。这样就可以在一个概率组里配置两条概率,一条是第1次的概率50%,一条是第2到第5次的概率为70%。
中奖规则:描述每次抽奖行为响应结果的方式,比如:活动周期内,用户有3次抽奖机会,但是只能中奖1次,即:之前中过奖,就不能再中。这时通过把中奖规则独立出来,并且在抽奖活动和轮次上都关联中奖规则,这样创建一个抽3次中1次的中奖规则,然后在抽奖活动和轮次都选这个规则,从而实现需求。
反作弊规则:抽奖活动经常面临被一些黑产薅羊毛的处境,识别正常用户和非正常用户的成本较高,且较难保证准确。虽然前端可以通过图片验证码、短信验证码等方式来提高门槛,但是道高一尺魔高一丈,还是有被突破的风险。因此本系统引入反作弊规则的概念,在特定用户、时间段等维度,对抽奖行为进行限制。
回调规则:作为一个独立系统,其他业务系统调用抽奖后,可能需要做记录中奖信息,发送短信等异步处理,于是加入异步回调功能。
业务实体模型如下:
图2 实体模型
系统实现过程如下:
1. 根据设计的模型,为每一个实体定义接口和一个工厂类,通过接口只暴露行为,工厂类依据不同的类型属性,去构建对应的实例。不同类型的实现类可以根据需要使用不同的属性字段去实现功能。
2.在进行一次抽奖时,需要明确到某一个轮次,构建一个抽奖轮次,除了轮次数据外,还有抽奖活动、奖品等其他数据,显然多次数据库查询性能肯定不能满足接口要求,因此我们引入自主开发的分布式实时内存缓存组件,提升查询性能的同时,也保证了数据的实时性。
3. 当构建好一个轮次实例后,便开始处理抽奖流程,过程依次分为:前置验证、执行抽奖逻辑、后置验证、入库、返回结果五个步骤:
第二种,概率之和小于1:
即P1+P2+P3<1,由此计算出为中奖概率1-P2-P2-P3=0.1,若random落在0.9-1中间,则未中奖,落在其他区间,则命中对应的奖品
第三种,概率之和大于1:
即P1+P2+P3>1,此类情况则为100%中奖,每个奖品的权重按概率之和的占比计算,如P1的权重计算公式为:P1/(P1+P2+P3)。
用实际数字举例:P1权重= 20/(20+40+60) = 0.16,然后取随机数判断中奖结果。
。
C. 后置验证:由于价值类奖品存在随机问题(如:现金红包有不同金额),只有在抽中之后才会明确价值,因此需要后置验证库存,防止超卖问题。系统设计上,支持活动和奖品共用库存,此处分两种情况,共用库存时,只验证统一库存;非共用时,需同时验证活动库存和奖品库存,此处验证同样基于Redis。
D. 入库:保存中奖记录并扣减库存,需保证在一个事务中执行,以保证数据完整性和一致性,同时这里利用数据库锁进行库存扣减,防止库存超卖。
E. 返回结果:中奖结果响应包括实时与异步两种,实时结果返回一些简要信息,异步结果返回详细中奖信息,供调用方后续处理。
系统只保存中奖用户记录,防止大量无效抽奖记录信息挤占数据库资源。同时在抽奖过程中,每一个中断处(所有导致未中奖而退出的地方)异步记录抽奖日志,便于问题排查。
可扩展性
为保证良好的扩展性,上述业务对象在实现时都增加了类型字段(如图3),通过策略模式实现。如库存早期只有两种类型,后来业务需求指定日期设置库存数量,只需要增加一种按日期设置次数的库存类型,实现一个对应的子类即可,其他逻辑保持不变。
图3 库存类型扩展
前文介绍的大部分系统概念都是采用这种实现方式,若有新的需求接入,可增加新的实现类,然后进行重新组合,系统的扩展采用“重新组合“的方式替代原有的”逻辑调整“。最终业务中约90%的场景都可以通过配置来实现,剩下的10%也可以通过在某一个对象或者多个对象增加类型来快速支持。
并发控制
系统经历了两次汽车之家818百城车展、及多次大访问量业务考验,这归功于系统在并发能力上的优化。
在接入层,系统对接了汽车之家API网关,对于请求进行限流,特别是直播间秒杀大额奖品的情况,瞬时流量可能达到数万QPS。这种情况下,对于非预期内的超量请求,直接返回预设中奖结果。
在应用层,对性能也有较高要求。一是查询性能,需要支持在多个业务系统的展示,二是抽奖操作的性能,要做抽奖资格校验、抽奖次数校验、中奖次数校验、库存校验、中奖规则校验等。考虑到奖品配置等信息变动频率低,访问频次高,变化时要求及时生效,系统引入了支持本地缓存实时更新的基础数据组件,将奖品配置信息缓存在了应用本地缓存中,后台修改时会触发MQ广播消息,实现缓存实时更新,既保证性能,又保证及时更新。
图4 并发优化
奖品配置信息缓存在本地缓存中,查询性能基本得到了保证。对于抽奖操作,除了要获取奖品配置信息,还需要做抽奖基础规则校验、抽奖资格校验、抽奖次数校验、中奖次数校验、库存校验、反作弊规则校验、二次库存校验(金额类的库存,本次中奖金额+已中奖金额<=总金额),这些校验中用到的配置信息可以从本地缓存拿到,用户资格、抽奖次数、中奖次数等都存到Redis中,以此保证抽奖操作的性能。
抽奖活动后台管理系统
为了方便业务方使用,我们搭建了一个独立的后台管理系统。
主要功能包括:抽奖活动创建和编辑,抽奖记录维护等。
另外,对于一些常见的抽奖活动玩法(比如:大转盘、红包雨等),我们开发了抽奖活动玩法库(抽奖活动类型不断更新中),如下图5。
业务组件基于VUE开发,组件通过绑定抽奖活动Id,可以实现自动加载抽奖活动配置的奖品、UI样式等。开发人员对接这些抽奖活动玩法时,仅需要引入相应的VUE抽奖活动组件即可使用整套抽奖系统。
抽奖活动的奖品和概率配置完全在管理后台进行维护,这部分工作可以由运营或者产品同学去完成,这样就实现了职责分离。开发只需要关心组件需要加载哪个抽奖活动信息即可,不用关心奖品,中奖概率。甚至组件的样式和文案都不用关心,因为样式和文案也可以通过后台系统配置。产品和运营可以在抽奖活动上线后,在不需要开发介入的情况下,随时对产品需求进行变更。
图5 抽奖活动玩法库系统
抽奖系统整体架构
为了更好的理解以上内容,我们将整个系统完整的内容展示在下图中:
图6 整体架构
写在最后
上线不到一年时间,目前该系统已经完整支持了几百场抽奖活动,累计抽奖千万次,整体运行稳定。
我们的系统也提供了抽奖活动全流程的支持,运营后台、服务端应用、UI前端组件、奖品发放及最后统计报表,用户可以灵活选择其中的全部或者部分流程使用到自己的业务中。我们在系统的使用过程中,文章开头提到的痛点问题,基本都得到了解决。
1. 提供了:概率(组)、中奖规则、反作弊规则等概念,灵活支持各种多变的需求,并在代码层面采用策略模式进行抽象,方便系统扩展;
2. 使用网关实现流量控制,系统层面,借用Redis+DB双查询方式,保证实时性和可靠性;
3. 完整的Saas平台加UI组件的模式,基本实现百分百复用,目前部门内部有多个业务使用我们的系统,节约大量开发工作的同时,快速支持业务。
作者