项目中用到的各个技术栈
在整个项目开发中熟练的使用了SpringBoot、MyBatis、Redisson等技术框架,在编程功能实现,熟练的结合与Spring容器,以及在项目中较多使用了Redisson框架,向Redis写入key-value、queue、map、lock等数据类型,实现业务功能。
引入分布式技术栈框架 Nacos + Dubbo,用于微服务间调用,提高信息数据传输效率。
以整个互联网电商公司视角来看,它会包括;商品系统、购物系统、营销系统、交易系统、结算系统、清分系统、对账系统、以及各类运营系统。而分布式抽奖系统就是其中的一个微服务系统,这个系统可以被分布式部署和使用。
而把一个系统扩展为分布式系统,会有很多相应的技术栈引入来解决数据传输问题,聚合查询问题,链路监控问题等。
Dubbo 这样的 RPC 框架,是比 HTTP 在微服务间的通信效率更高的
Nacos的作用
服务注册中心:Nacos作为服务注册中心, Dubbo服务提供者启动时会将自己的服务信息(如服务名称、IP地址、端口等)注册到Nacos上。这样,服务消费者就可以从Nacos中发现并获取可用的服务提供者信息,实现服务的调用。
服务发现:Dubbo服务消费者通过Nacos来发现特定服务的提供者。消费者在调用服务时,会向Nacos查询服务提供者的地址列表,然后根据Dubbo的负载均衡策略选择一个服务提供者进行通信。
mq主要用于
监听活动sku库存消耗为0
监听积分账户调整成功消息,进行交易商品发货
监听用户行为返利消息
监听用户奖品发送消息,发奖
写入中奖记录,发送MQ消息失败如何处理?
本身发送MQ是可能存在万分之一或者十万分之的失败的,而数据库操作和MQ操作,本身不能做数据库事务。但又要保证失败后的补偿处理。所以要结合中奖记录在写一条发送MQ的任务记录,任务记录上有一个状态,标记是否发送完成,这样就可以通过任务扫描的方式完成 MQ 的补偿发送。
生产者可能多次发送同一个MQ,怎么保证奖品不会超发?
这是一个幂等的设计处理,MQ 的消息是必须含带具有唯一标识的业务ID的。比如订单ID、奖品ID、支付单ID、交易单ID、贷款单ID等等。接收MQ的系统,通过唯一ID业务,更新或者写库的时候可以保证幂等性。这样也就不会产生超发的可能。
XXL-JOB主要用于
扫描数据库中的task表来发送任务信息给MQ
更新活动sku库存(从Redis队列里更新到数据库)
更新奖品消耗库存(从Redis队列里更新到数据库)
Zookeeper用于:
基于 Zookeeper 实现分布式动态配置中心服务,用于分布式应用节点系统中的环境属性值变更。这样我们可以让所有分布式系统中,类下的属性值做动态的调整,及时的对系统进行;切量、熔断、降级、黑白名单等用途。
在互联网公司中面向C端的应用场景,有非常多的动态配置使用,包括你看到的一些金融场景的额度费率、电商场景的特惠商品、出行场景的促销活动,都会有一些动态配置的使用。
如何对所有分布式节点的应用,活动信息本地内存更新?这就是zookeeper的作用
通常我们会有诉求在不重启系统的时候,就要动态变更所有分布式应用节点中某个属性的值,如开关、缓存、调试日志开启/关闭、熔断、限流、或者抽奖黑名单以及概率等。这些东西通常不是 Redis 存储,而是应用中具体字段的属性值,这样效率更高。
而这个操作需要使用到类似于 Zookeeper 组件的临时节点监听,动态变更字段值。
使用方法:
以Zookeeper为配置中心服务,基于 Zookeeper 的节点监听值变更机制,动态修改应用程序中属性值。
首先,我们需要定义出一个 Zookeeper 监听的配置路径,一般这个路径在配置中心中是申请的系统使用地址,以确保值的唯一。
之后,每个类对应的属性,需要映射出一个监听的节点。比如;Zookeeper 监听了 /big-market-dcc/config 那么类中 a 属性可以是 /big-market-dcc/config/a 这对这个路径设置的值,就可以被监听拿到了。
最后,把获取到的监听值,通过 Java 反射操作,把值设置到对应的属性上。这样在 SpringBoot 应用程序中,使用某个类的属性值的时候,就可以动态的获取到变化的属性值了。
为什么用Zookeeper作为配置中心?
nacos前面用作注册中心了,为啥不直接采用主流的nacos做配置中心
1. 进入企业中,并不是所有公司都用 nacos
2. 不能因为一个动态配置服务,就要为不需要nacos的引入一套nacos再来维护
3. Zookeeper 更加轻量,对于中小场景非常适合
4. 也可以使用 nacos 替代 Zookeeper,在 dubbo 官网提供的注册中心,包括 nacos、Zookeeper,还有 redis
5. 采用 Zookeeper 可以多学到一些额外的技术使用
从前端用户行为将抽奖流程拆分
抽奖前刷新页面就会触发查询奖品列表(用于展示九宫格中的奖品信息)、查询策略抽奖权重规则(用于展示抽奖阶梯,即总抽奖次数达到一定次数后必中某些奖品)、查询用户积分值和可用抽奖次数以及是否签到(用于展示会员卡信息),这些操作对于用户而言都是无感知的,刷新页面就会被动触发
之后就是点击策略装配按钮,来装配抽奖概率、权重规则、奖品库存等信息(目前是点击按钮触发,后续准备将其改为刷新页面自动装配)
点击积分兑换抽奖次数,后台进行校验,sku库存校验,发奖,库存扣减等一系列操作
怎么想到将抽奖分成前中后三个阶段的?
这个的设计得益于在 Spring/MyBatis 框架源码的学习,在源码中经常会出现对一个流程进行拆分解耦,流程可扩展的点,如 Spring 是 Bean 对象的拆解,MyBatis 是会话流程的拆解。所以在设计大营销的抽奖模块时,对于需求中的各类功能点;黑名单抽奖、权重抽奖、默认抽奖、抽奖N次解锁、兜底抽奖等等情况,是可以拆解为抽奖前、中、后,3个行为动作的,基于这样的考虑后,就可以设计出非常容易扩展的松耦合结构。
点击抽奖,后台开始运作,根据概率范围选择抽奖算法是O(1)或O(log(n)),抽奖过程中,包含了黑名单规则、权重规则、默认规则的责任链,次数锁校验、库存处理、兜底奖励处理的规则决策树,大致流程是用户点击抽奖后,后台首先判断用户是否是黑名单用户,如果是则被拦截在黑名单规则给用户发库里配置的黑名单奖励,否则继续判断用户是否处于权重规则,即用户的总抽奖次数是否达到了抽奖阶梯的标准,若达到标准则被权重接管,给用户必中奖品,若没有权重接管,则继续往下走默认抽奖,默认抽奖就是普通抽奖,若触发了默认抽奖,则进行规则树的过滤,判断用户的日抽奖次数是否达到了某个值,若未达到则直接走兜底奖励(除了被锁住的奖品其他的奖品都配置成兜底奖品),若次数足够,则进行库存扣减,如果没库存了,那么还是走兜底奖励,如果有库存,才可以返回被锁住的奖品
注意被权重接管仅有一次,就是达到权重值的那一次,之后再进行抽奖就是走正常流程
抽奖环节的重点知识有:设计模式(责任链模式处理抽奖前、规则树模型处理抽奖中、模板模式串联责任链和规则树、整个框架中大都包含了抽象工厂模式)、抽奖后的库存处理(不超卖库存规则)、抽奖算法的选择(O(1)算法、O(log(n))算法中的for循环、二分查找、多线程等)
抽奖算法如何提供O(1)时间复杂度,提高抽奖效率?
在大营销系统中,运营人员配置好抽奖活动后,开始上线对外后,会进行数据的预热数据。这个预热的过程会把活动信息、策略信息、库存信息都存储到 Redis 里进行使用。
而抽奖的策略就是记录了一个策略下N个奖品的概率,将概率转换为对应的整数数量,写入到缓存中。那么在抽奖的时候就按照整数数量生成随机数来抽奖。这样用空间换时间的效率是非常高的
抽奖也是一种瞬时峰值很高的业务场景,那么对于抽中奖品后的库存扣减是怎么做的?
关于库存的扣减,是一个非常重要的流程。尤其是这种单独资源竞争的场景,如果设计的不好,很容易把服务打挂。
所以在这套系统设计中,为了避免库存扣减直接更新库表的行级锁,而导致大量的用户进行等待状态。所以把数据库表的库存同步到 Redis 缓存中,在通过 incr 扣减的方式进行消费,同时为了确保在临界状态、库存恢复、异常处理等情况下不超卖,而对每一条产生从 incr 值,与抽奖的策略ID组合一个key,进行 setnx 加锁兜底,来保证不超卖。—— 这样的设计是颗粒度更小的锁方案设计,性能接近于无锁化。
你讲到库存的扣减是通过 Redis 滑块锁实现的👍🏻,那么最终同步库是怎么做的,怎么降低对数据库的压力的?
关于 redis 缓存和数据库表库存数据的流程,设计了异步更新,保持最终一致性的设计。在执行完库存的扣减操作后(在抽奖中规则树库存节点流程),发送一个扣减完成到 Redis 的异步队列(可以使用MQ+延迟消费),之后通过定时 Schedule Job 来消费队列。这样就可以控制效率速率,降低对数据库的压力。(因为我们不能 Redis 扣减的多快,就直接打到库表上,那样对数据库的压力依然很大,容易打挂)
抽奖奖品库存如何处理,怎么保证最终一致性?
在抽奖秒杀这样的场景下,都需要把库存缓存到 Redis 中进行使用。而不能数据库表加行级锁,否则大量的秒杀进行通过加锁和等待释放,就会夯住数据库链接直至拖垮整个服务。
那么使用缓存通过大营销项目中的颗粒度更低的分段锁后,怎么来保证一致性呢。这里需要3个步骤,首先是每次扣减完库存,都会写入到 Redis 延迟队列 / MQ 延迟消息,缓慢更新数据库库存。之后是 Redis 内的预热库存消耗完毕后,发送最终 MQ 消息,更新数据库的剩余库存为 0,最终活动结束后,还有任务补偿,扫描抽奖所产生的的参与记录单,更新最终的库存消耗。这里就可以用订单 MQ 通过 Flink 计算,更新最终库存也是可以的。
setNx 锁的目的是兜底,比如活动配置有 10 个库存,消耗开始 9、8、7、6 但因为一些问题,无论是redis还是其他系统导致的,运营需要重新调整恢复库存。但这个时候恢复错了为9个,但已经消耗到6个。那么 8、7、6 就会产生新的加锁key,这个加锁key会被redis已经加锁的key拦截,避免超卖。因为加锁不是竞争,不耗费性能但可以做兜底,是个不错的选择。【实际中系统运行最容易出问题的点,就是运营配置问题和调整活动】
为什么选择责任链模式?
抽奖的前置规则在抽奖中是一个什么行为。其实它可以被抽象为一种策略行为,比如;黑名单抽奖策略、权重抽奖策略、白名单抽奖策略等。而这些策略规则是一种互斥行为,比如走了黑名单规则,就不应该在继续走权重规则了。那么对于这样的情况,责任链的设计就更加合适了。
为什么选择规则树模型?
对于抽奖中到抽奖后的规则,它是一个非多分支情况的规则过滤。单独的责任链是不能满足的,如果是拆分开抽奖中规则和抽奖后规则分阶段处理,中间单独写逻辑处理库存操作。那么是可以实现的。但这样的方式始终不够优雅,配置化的内容较低,后续的规则开发仍需要在代码上改造。
从领域区分
活动领域
armory活动预热装配
partake创建活动抽奖参与订单
product商品sku服务,用于查询当前活动ID下,创建的sku商品(涉及积分兑换抽奖次数的部分)
quota抽奖活动账户额度服务,又分
policy策略分为返利,直接到账(无需支付)、积分兑换抽奖次数(需要支付)
rule活动规则
将活动配在数据库中,可控制活动的开启和关闭,以及配置活动类型,在创建活动抽奖参与订单时会先去数据库查询活动状态和活动日期,然后查询未被使用的活动订单记录,再就是对账户和订单的构建,然后将这两者聚合起来存入数据库
rule活动规则包括活动sku下单(sku库存)、活动信息、活动时间、活动状态这一系列校验和扣减,可以看出这是一个固定的流程,因此抽出一个责任链的结构来实现该流程
关于库存扣减,扣减的实际上是redis缓存里的库存,在扣减成功后,还需要写入延迟队列,延迟消费更新库存记录到数据库,这部分操作采用RBlockingQueue阻塞队列、RDelayedQueue延迟队列,这里是延迟的方法消息到Redis队列中,以此来减缓消费,同时是双重减缓,一个是延迟队列,一个是定时的任务调度,给延迟队列设置一个3秒后才会被队列处理的任务,保证能够减缓消费
很精妙的设计,使用redis的延迟队列+rabbitmq的监听队列
为什么不用mq来实现延迟队列功能?
因为mq在同一个队列中必须是按顺序进行消费的,而我们使用Redis来做延迟队列,在遇到redis缓存中库存消耗完的情况,可以直接触发任务来发送mq消息设置数据库库存为0,但mq就只能一个一个消费到最后一个库存为0,这样一对比谁更占优势就很明显了
policy交易策略中,不同类型的交易策略实现类,通过构造函数注入到 Map 中,以供后续流程中获取策略类型是需支付类或无需支付类
奖品领域
该领域涉及发奖部分,流程就是:首先会根据奖品ID去数据库中award表查询奖品的配置信息,根据配置的信息来生成随机积分,然后就是更新用户积分账户,若无用户积分账户则改为新增,下一步就是更新奖品记录,将user_award_record表中对应用户和订单的状态由create改为completed
另一部分则为保存用户中奖记录,流程就是向user_award_record表写入记录,task任务表写入任务记录并设置状态为create,然后将user_raffle_order表中对应的用户及订单状态由create修改为used,就是设定该订单已被使用,最后发送mq消息,若发送成功则直接将任务状态设置为completed反之fail
积分领域
涉及积分方面的问题就是积分调额,增减积分,创建账户积分额度订单
首先查询账户是否存在以及积分额度是否充足,然后就是保存账户积分、保存账户订单,写入任务(给任务状态设为create),发送mq消息,若发送成功则直接将任务状态设置为completed反之fail
返利领域
首先查询返利配置-查询 daily_behavior_rebate 表中配置的返利类型,有以下两种类型
然后创建任务并设置任务状态为create,向task表添加任务,user_behavior_rebate_order表插入数据,同步发送mq消息,若发送成功就将任务表状态修改为completed,发送失败则任务状态修改为fail
从上面可以得出一个结论,task这个表的作用就是为了设置mq消息发送的成功与否,任务领域则专门汇总任务相关的内容,如更新任务表任务状态,将任务类发送mq消息,查询未发送的任务列表(即任务状态为fail或状态为create且距今超过6小时的记录)
待新增:
ERP后台管理:添加黑名单功能,添加权重配置管理(管理日n次抽奖后解锁新奖品,管理总抽奖n次必中奖品权重),添加策略奖品管理(可配置奖品名称,奖品概率,奖品库存(关于增加库存这点,需要设置一下,增加库存之后刷新redis缓存里的库存))
库存的恢复,基于 incr 和 总量 + 恢复量对比,恢复量可以由失败恢复和任务校准当前产生单量和库存差异做对比。- 不过一般实际生产主要保证不超卖,货品是有很多的,今天没处理完,明天继续卖。
redis缓存装配策略时自动清空上一次装配的内容,可能需要添加库存刷新
前台:添加用户登录,添加中奖记录滚轮