幸运抽奖系统学习笔记

一、项目领域
一、策略领域
1、库表:
策略总表:id、策略id、策略描述、创建时间、更新时间
策略奖品表:id、策略id、奖品id、奖品标题、奖品副标题、奖品库存、剩余库存、中奖概率、规则模型。
用这张表描述策略id下的奖品,不同的奖品对应的中奖概率和规则模型不同
规则模型:随机、兜底奖品、抽几次后解锁
策略规则表:id、策略id、奖品id、规则类型、规则模型、规则比值、…
用这张表描述抽奖策略下有哪些规则,规则的具体数值。
规则类型:策略规则、奖品规则。规则属于策略层面还是奖品层面,策略规则:用户消耗积分不同,抽奖范围不同;奖品规则:随机积分、抽奖几次解锁,兜底奖品
2、策略概率装配
根据策略id关联的策略奖品表,获取最小概率和概率总和,最小概率/概率总和 截断再向上取整即可得到抽奖随机数的一个范围。但是这样可能在概率极低的情况下出bug。所以我就直接用最小概率的小数点位来得到范围,例如0.00001对应范围就是100000。
3、策略权重概率装配
在用户消耗一定积分的时候,我们需要调整用户抽奖的范围,不让用户抽到低价值的奖品,所以我们在设计系统实现的时候需要处理不同策略规则权重的概率装配。
这里的积分范围一般会比较高,例如6000封顶,那么4000、5000、6000这区间会调整用户抽奖范围。如果积分比4000低,就走普通抽奖。例如4000积分,对应的奖品是101、102、103;5000积分,对应的奖品是102、103、104…,后续抽奖的过程中我们可以根据不同的奖品编号(如101)拿到对应的策略规则。
存储在redis中是key-value,策略ID+权重值:数值。
4、抽奖的规则过滤
我先设计了抽奖前的逐步的规则过滤,抽奖中的逐步校验;我发现抽奖前的行为互斥且有序,所以把他们松耦合出来,每个过滤的逻辑包成一个责任链节点,抽奖前的整体过滤就是一条责任链;抽奖过程中一开始用if else判断的,但是后来发现使用决策树这种二叉调用的模式可以省去很多if else判断,并且整体流程便于后续拓展,所以就把每一个校验行为封装成一个决策树的节点,决策的结果和节点后续的指向有关。
最后我抽象出一个模板把抽奖前和抽奖中的流程串联起来,责任链负责抽奖计算、基于抽奖的结果放到规则树中过滤,返回最后抽奖结果。具体的操作交给子类去实现,这样一来可以便于拓展,而来也可以直接从模板结构就看出整体的链路是怎么样的。

(1)前置规则过滤:
抽奖前的多个规则过滤实际是多个策略行为,例如黑名单抽奖策略、权重抽奖策略、默认抽奖策略,并且这些策略执行的时候互斥并且有一定的顺序,所以我使用责任链模式将这些策略串联起来。
定义责任链的规则接口(规定了责任链当前节点的过滤逻辑,它的下一个责任链节点,以及设置下一个节点);一个组装规则的接口;一个抽象类填充责任链;一个默认工厂类继承抽象类。
不同策略有不同的责任链的具体实现,责任链的bean名称就是策略对应的规则名称,方便后续进行装配
使用责任链工厂,使用map保存所有的责任链,根据策略id查当前策略下的所有规则,组装责任链。

黑名单过滤:查询redis的hash,名单由量化工程师那边通过数据标签算出来的。
如果在黑名单范围内就返回兜底奖品(随机积分值)
权重规则过滤:查询对应权重范围的奖品,如果在目标权重范围内,就按照对应奖品范围抽奖;如果不在,则执行普通抽奖。
(2)抽奖中规则过滤:
因为抽奖中涉及到次数锁校验,校验通过则进行库存校验,校验不通过则走兜底奖品;库存校验通过了就执行后续发奖,校验不通过则走兜底奖品。这些行为不像责任链一样是单一步骤而是多分枝链路,所以我使用决策树模型。
实体类:
定义决策树的根节点:根节点、决策树的所有树节点(Map集合)
定义决策树的节点:唯一key、对应规则的数值、规则连线
定义规则连线:起始的规则节点、目标的规则节点。用于标识从哪个节点到哪个节点。
接口:
决策树节点接口:当前节点的逻辑
设置三种规则树节点:校验抽奖次数、校验奖品库存、兜底奖项
5、得到奖品ID后进行库存扣减
首先库存扣减肯定不能直接扣数据库表中的库存值,这样连接数太多容易打垮数据库。一种方式是分布式锁来锁住库存的值,但是这样得申请加锁/等待当前锁释放,也比较耗时。我觉得为了进一步去分摊竞争,尽量做到无锁化可以进一步提高效率。
(1)通过LUA脚本扣减库存,返回扣减库存的结果,如果扣减成功则写入消息队列异步更新数据库库存(这是一种趋势更新),如果扣减失败则异步保存扣减失败的记录到数据库。
(2)为了防止多卖:我们对单条库存数据进行加锁,这是一种分段锁
加锁的key:业务前缀+活动ID+奖品ID+库存(扣减之后的库存)
举个例子:目前库存为100,加锁是099,如果补库存了,我们将库存设为200,但是实际上只有100199这个区间是可以加锁的,0~99已经上锁了,无法再加锁也就无法再领取了。
6、抽奖接口API实现
实际项目中前后端分离,开发的时候接口的定义标准是由前后端工程师对照着PRD文档定义的,前端工程师会按照标准Mock接口
二、用户参与领域
1、用户参与抽奖的库表设计以及用户表的分库分表
(1)分库:
配置库:一般来说就是一个,用户存储抽奖活动表和参与次数表,这种活动相关的配置信息。
用户库:设计了两个,活动下单记录表、活动次数账户、账户次数账户,这种与用户相关的信息。
(2)抽奖活动表:
这是一张总表,活动的配置信息。包括:活动ID、名称、描述、开始时间、结束时间、库存总量、库存余量、参与配置ID、抽奖策略ID、活动状态
(3)活动的sku表:
将活动视为sku,将活动的参与次数和活动配置当成sku的属性。这样做的好处有三点:
对象更清晰:我们将活动视为类似于商品的sku,用户获得参与活动的次数相当于下单一个活动的sku,给自己的活动账户充值可参与的额度次数,用户抽奖相当于消耗额度。
增加灵活性:对于一种sku在不同的大促活动期间可以有不同的配置和活动次数,例如可以针对双十一或者周年庆的节日配置不同的sku,这就类似于买衣服的时候会员和非会员的价格是不同的。而且我们活动也可以对不同身份的用户有不同的次数配置。
拓展营销方式:我们可以使用多种形式兑换抽奖次数,例如积分、下单次数、签到次数等等。
(3)参与次数表:
包括:参与配置ID、总次数、月次数、日次数
(4)活动下单记录表(多表):
系统为用户下一个参与活动的订单,用户可以消费订单参与抽奖。
包括:用户ID、活动ID、活动对应策略ID、订单ID、总次数、月次数、日次数、下单时间、订单状态、唯一索引。
(5)活动次数账户(一张):
用户能够参与活动的次数
包括:用户ID、活动ID、总次数、剩余次数、日次数。
(6)用户活动账户-总
记录用户总、月、日的抽奖次数,相当于做一个镜像。
(7)用户活动账户-月
记录用户月的总抽奖次数与剩余抽奖次数
(8)用户活动账户-日
记录用户日的总抽奖次数与剩余抽奖次数(总次数-剩余次数=用户当然抽奖次数)
(9)用户抽奖订单表
用户要参与活动必须要下一笔订单,这张表用于记录订单信息。
(10)用户中奖记录表
记录用户中奖信息,包括用户ID、活动ID、策略ID、订单ID(抽奖订单)、奖品ID、状态
(11)任务表
中奖之后需要发送MQ消息来发放奖品,任务表记录发奖任务的信息,用于给MQ做一个兜底。

2、用户参与抽奖的整体流程
(1)活动预热:
在活动装配类中专门预热sku库存、预热活动信息、预热活动次数(总、年、月)。提升聚合性。
(2)参数校验:
(3)参与活动-创建参与记录订单:
为用户下一笔活动参与订单:
首先需要定义接口(创建抽奖订单),定义一个抽象类(定义出领取活动订单的所有流程,包括:活动日期的校验、查询是否有未使用订单、账户额度过滤、创建&保存&返回订单)
保存订单:更新个人总、月、日账户的额度值(如果没有账户就从镜像中创建账户);保存参与活动订单(sku订单)
(4)根据抽奖策略执行抽奖:
责任链过滤:

得到奖品ID之后进行规则树过滤:

(5)保存中奖记录:
保存用户中奖记录,发送MQ消息进行发奖
首先需要定义接口(保存用户中奖记录),定义一个服务实现(创建任务对象、消息对象,根据二者构建聚合对象,最后将聚合对象交给仓储进行保存),发送MQ消息并且更新发放任务的状态为已发送。
用户中奖与发奖需要做异步解耦,因为一些发奖的方法(包括一些通知)不在抽象系统里,需要RPC/HTTP远程调用来发放,调用接口可能会有超时问题,需要重试,所以这里需要对发奖任务task写入数据库作为MQ的补偿。
(6)返回中奖的结果给前端:controller层返回即可。
(7)发奖服务:
我们将不同奖品对应的发奖服务封装成bean对象,bean的名称就是奖品的key,例如credit_random、phone等。使用构造函数将bean对象注入到Map中,根据奖品ID查询奖品key,根据奖品key拿到bean对象调用对应的发奖服务。

(3)为用户下单sku,充值可参与的额度次数:
传入的是sku下单实体,包含下单信息:用户ID、skuID、日期、行为类型等等。
对于固定的、串行、互斥的一些验证行为(验证活动日期、状态、sku库存校验、扣减)使用责任链。
构建订单的聚合对象,聚合对象用于聚合活动实体、sku下单实体、活动sku实体、活动次数实体
保存订单,这里分别创建保存订单集合和增加用户次数的DO对象,开编程式事务将二者保存。
返回订单号。
(4)扣减库存(高并发):
为了高并发,先使用LUA脚本扣减缓存中的库存,如果返回剩余库存如果为0,则发送MQ清空库存,确保库存的最终一致性。
对扣减后的库存加分段锁,防止超卖。
超卖的情况:redis扣减库存了,库存也上分布式锁兜底了(两动作可以用LUA脚本实现),扣减数据库的信息已经发出去了,但是现在redis还没持久化或者主从同步就宕机了,那么redis恢复之后库存没扣减、锁还是没上,这时就会出现超卖。
超卖情况的解决:redis不是那么容易挂的。。。
少卖的情况:redis扣减库存了,库存也上分布式锁兜底了(两动作可以用LUA脚本实现),但是应用挂了,MQ信息没发出去。
少卖情况的解决:少卖不是最主要的,我们要确保的是不超卖
延时队列还是MQ:对比

3、用户查询中奖信息:
活动策略对应的奖品列表:奖品ID、奖品标题、副标题、显示的编号、次数规则、是否已经解锁、还需几次可以解锁。
4、用户返利行为入账
对象的行为(签到、打卡、支付、开户、拉新、贷款)等得到返利,并且一个行为可能对应多种返利的配置。
(1)日常行为返利的配置表:
记录行为对应的返利配置:行为类型、返利说明、返利类型(sku、积分等)、返利配置、状态
例如,如果返利类型是sku,则是对应的sku编号,如果返利类型是积分,则是对应的积分数值。
(2)用户行为返利流水订单表:
记录用户返利:ID、用户ID、订单编号、行为类型、返利类型、返利配置、外部透传ID(年月日)、业务ID(用户ID_返利类型_外部透传业务ID)
(3)用户积分表:用户积分是一种中间媒介,既可以抽奖、签到等行为得到积分,也可以抽奖来消耗积分,可以形成一个闭环。
(4)保存返利订单
根据用户行为查询对应的返利配置,根据不同的返利配置创建不同的返利流水订单与MQ任务任务表,将这些组装成聚合对象(便于在一个事务下将二者保存)。
根据聚合对象分别保存返利流水订单与MQ任务表。在事务结束之后发送MQ消息进行返利入账。
(5)返利入账
消息消费者拿到MQ消息后,封装sku充值对象,调用sku充值方法对用户sku进行充值,调用积分充值方法增加用户积分。

三、奖品领域
1、保存用户中奖记录和发奖任务task(兜底)
根据用户奖品记录构造奖品分发的聚合对象->调用仓储保存中奖记录函数->一个事务下保存仓储保存中奖记录与发奖任务task->异步发奖、更新task状态
2、实际奖品分发服务:
奖品分发的消费者拿到消息之后,触发奖品发放服务,不同奖品的分发逻辑不同(积分分发和sku分发),所以我们这里把每一个分发服务作为一个bean,分发的时候调用各自的分发方法即可。
根据每一个奖品的AwardKey拿到每一个奖品对应的发奖服务(bean对象)
调用发奖服务分发奖品方法来发放奖品。
例如积分分发服务:根据奖品ID查找奖品的配置信息(例如随机积分的范围:1~100) -> 生成实际的奖励积分数值->构建奖品发奖聚合对象并存储发奖记录 -> 在一个事务下保存(更新用户积分 + 更新用户获奖记录的状态为已完成),那么我们前端就可以看到发奖的状态。

为了保证代码的健壮性:
多并发情况下多线程插入会出现回滚,一个事务下线程Aupdate之后发现结果是0,就是没有账户 insert 账户,此时另一个线程B 去insert就会因为线程A拿到间隙锁而报错。
那么这个时候我就在执行任务前加一把独占锁,这把锁锁的是用户的账户。
四、活动领域
1、前端页面信息查询
活动配置(用于9宫格页面展示)、用户当前抽奖次数(幸运值&再抽奖x次解锁)、签到(签到功能&签到结果)
2、活动整体预热
(1)预热活动商品库存
(2)预热活动参与次数
(3)预热活动信息
3、活动参与
(1)校验活动状态、日期
(2)查询用户未使用的活动参与订单,如果有订单则直接返回活动参与订单
(3)如果没有,进行额度账户过滤返回账户对象 -> 构建抽奖订单 -> 填充抽奖单实体(将抽奖订单保存到账户对象) -> 保存抽奖订单(记录抽奖信息)对象,并且扣除账户剩余额度。
4、商品sku的查询
我们将活动视为sku,同一个活动是有不同sku配置的!
sku配置:sku编号、活动ID、库存总量、剩余库存、商品金额(积分、金钱)、配置次数(购买后可获得的抽奖次数)
例如:同一个活动,可以使用积分充值,获得1次抽奖机会,这种库存有10000个;可以使用金钱充值,充值5原,获得10次抽奖机会,这种库存有10000个;可以使用金钱充值,充值10元,获得25次抽奖机会,这种库存有10000个。
根据这种情况我们后续可以方便地拓展会员抽奖制度,无非就是多加一个sku配置。
5、调额服务
1、交易策略
根据不同交易情况对账户进行充值,不同交易策略注册为bean,交给IOC管理,遍历后续针对用户不同充值行为调用。
(需要支付的)积分交易策略
(不需要支付的)无支付交易策略
2、用户sku充值
创建sku账户完成充值,给用户增加抽奖次数;
对于【打卡、签到、分享】等无需支付的充值订单,则调用交易策略中的“无支付交易策略”进行充值;
对于【积分兑换】等需支付的充值订单,则调用交易策略中的“需要支付交易策略”进行充值;
五、积分领域
积分是一种比较特殊的奖品,因为积分既可以通过抽奖获得,也可以消耗积分获得抽奖机会,以后还可以拓展积分兑换服务,所以把积分的调增和调减单独抽象出一个调额服务。
1、积分领域调额服务
DDD的设计思想链路是,以用户请求携带领域对象的实体,通过决策命令去完成领域的事件。
在积分调额领域中,领域对象的实体就是交易的实体(它包含了积分调额所需的所有信息),决策命令就是根据交易实体创建交易订单,然后完成积分的调额行为,积分调额行为就是领域的事件,它是一种请求的结果态。
将用户积分的相关操作抽象出一个积分调额服务接口,可以对应多种积分调额的实现类:积分增加、积分扣减。积分增加服务与积分扣减服务只在于状态不同,增加则积分为正、减少则积分为负
积分调额服务的流程:
调用调额服务的创建订单方法(传入交易实体)->创建账户积分实体、订单实体->构建发送奖品的消息任务对象->将积分账户、积分订单、发送奖品的任务组成聚合对象->调用仓储服务保存聚合对象->(增删)账户积分、保存账户订单。
六、返利领域
领域对象的实体:行为的实体(包括用户ID、行为类型、外部透传业务ID)
决策的命令:生成返利行为入账订单
领域的事件:根据不同的配置对用户行为进行返利到账。
因为不同的行为对应的返利不同,例如:签到【增加抽奖次数1次】、消费【增加抽奖次数、增加积分】…
所以需要根据行为对象中的行为类型查询对应的返利集合 -> 根据不同返利类型创建返利订单对象和消息分发的task对象,全部纳入聚合对象中。-> 调用仓储服务保存返利订单与消息分发task -> 异步返利到账;根据返利类型【sku、积分】分别增加账户额度,更改task状态为已完成。
返利:
sku充值:调用活动领域中的返利交易策略去更新用户的总、月、日账户额度。
积分充值:调用积分调额服务增加积分值
这里为了防止消息重复消费,对于签到行为的防重ID,选择的是年-月-日
二、项目问答:
1、项目的整体架构:

2、分布式锁是否有持久化和过期处理?
持久化:AOF 和 RDB 混合持久化(八股文连招)只针对存储数据,但分布式锁一般不进行持久化,因为持久化是为了避免因为 redis 宕机而造成的数据丢失现象或是避免可能出现的死锁现象,而我们的分布式锁设计上每个锁只能由一个用户抢夺,活动过期时锁失效,不存在死锁现象
过期处理:redis 原生命令 set nx px 可保证原子性,过期时间为整个活动持续时间,key 为活动 ID + 用户 ID + 库存数,value 为 requestID 也就是 活动 ID + 用户 ID + 当前时间戳 或者是订单流水号
3、什么是滑块锁?持有滑块锁不释放如何解决?
以活动结束时间,设定滑块锁的有效时间。
个人占用的锁,不需要被释放。等待活动过期后其过期时间生效使锁失效。
在活动发布前,就已经准备好了锁的数量,而滑块锁的特性避免了死锁现象的发生
4、讲一下项目中使用到的设计模式,为什么要使用?
工厂模式、模板方法
5、系统的redis库存同步到Mysql是怎么做的?为什么要当库存清0时发异步消息同步Mysql?
6、订单的幂等性校验是怎么做的,讲一下具体过程?
7、当同一用户的两个订单同时到达,而此时当前sku只有一个库存是如何处理的?
8、如果项目要真实部署到服务器,提供高可用服务,你觉得你还能作那些改造?
9、DDD为了避免MVC出现的荣誉,对项目按领域进行划分,那么跨领域之间的服务如何实现,是否会造成第二次冗余?
项目体量大的情况下会使用Application层或者case层做应用编排,来编排不同领域之间的协作。
如果体量不大,也可以不用case,那就在trigger层下的http服务队领域编排。
10、抽奖项目的目的是什么?技术目标是什么?
技术目标:构建一个统一的营销服务化平台,通过DDD做领域建模,划分边界,让系统的功能可以被编排使用,提升可复用性与拓展性。
11、你设计抽奖系统的时候怎么去衡量一个系统的好坏?
12、有没有解决什么并发问题?
13、与业界传统的抽奖系统相比,你们这个系统有什么特点?
14、奖品库存扣减采用decr操作的时候,如何解决库存幂等问题?也就是说redis的原子锁只能标记库存有没有做加减,但不能标记哪个用户对库存进行了扣减?或者说客户端因为网络重试会导致扣减2次吗?
15、面试官说 你解耦中奖和发奖有什么劣势?面试官继续基于9.1追问,那除此之外呢,维护成本上有什么劣势?
抽奖是一个独立系统,发奖也是一个独立系统。如果从抽奖完成直接调用抽奖,整个链路太长了,不利于提供系统的吞吐量。另外一些非虚拟商品,还要用户填写收货地址。奖品的多样性,发券、发卡,甚至可能一个抽奖是发抽奖包,给一堆的券。这些都不适合一次性的调用接口(还要过风控),所以要做解耦设计,让用户可以知道抽奖完成,点击查看奖品(如果没发到位,会提示奖品发货中(审核中))。
这样做解耦的效果是不错的。 维护成本没有什么,就是MQ消费和使用,这是很成熟的技术方案。是整体系统都是这样的架构。
16、你项目中redis的过期时间怎么设置的?那如果我要更改某个奖品的概率怎么办,如何保证数据库和缓存一致性
生产中的数据一般是不会被更改的,这个是实际场景。如果更改,基本是运营事故。可以下线活动了。
单纯技术角度看,可以设置带有版本号的key更新操作,设置完新的概率,在更新缓存版本key就可以了。
17、在你了解到整个系统的问题之后,如果让你重新设计营销抽奖模块,你怎么设计?
首先四层lvs+f5过滤掉一些恶意请求和流量,其次7层做一个ngnix集群,接着微服务尽可能拆细,针对抽奖的静态页面可以做客户端缓存、CDN缓存、nginx缓存、本地缓存、redis缓存,对于热点key,进行redis分片,根据用户id路由到不同的分片上,避免热key,数据库层面,考虑分库分表,或者分区表、索引优化,同时通过mq解耦不同模块之间的通信,最后加上监控(心累)
18、大营销你们是单节点部署还是集群部署的?(我回答是集群)。那奖品服务、积分服务这些都是单独部署成微服务吗,就比如你说的负责抽奖的这个服务,有多少个节点呢?
我这个是学习项目,没有这么多资源让我实际去部署。但是具体部署多少个节点取决于日活、用户数、并发数等,一般情况部署2-4个节点。
19、你是通过重新插入一个任务记录,的方式来进行重试,这里会额外进行一次数据库io,如果同时请求量很大,可能回对服务器、数据库的性能有影响,如果你不用写库的方式,还有其他办法吗?
task表写记录适合业务流程一个事务的,是非常可靠的设计。
用延时队列分散请求,不集中入库。
20、通过canal同步msql分库分表的数据到es,怎么保证数据一致性呢,如果同步的时候数据库的指发生了改变,就会有一部分数据没有同步到es,这种情况你怎么处理?
数据同步ES,binlog 是可以补偿重试的
21、比如我一个AOP里面要对方法执行前后都记录日志,这些日志都是要存到数据库里面的,也就是需要进行数据库io,但是这是一个请求量很大的方法,我又不想影响方法执行的性能,你怎么处理?
可以在AOP中启一个ScheduledThreadPoolExecutor线程池,进行延时或者周期性的批量保存日志。这样主要是能异步,并且分散大量的数据库io。
22、项目DDD模型的理解
https://articles.zsxq.com/id_37mnf5wafh1z.html
(1)定义:DDD是一种软件设计方法,它使用领域、界限上下文、实体、值对象、聚合、工厂、仓储等概念对模型进行切割。
(2)设计手段:
战略设计:通过抽象、分治的过程,合理的拆分为独立的多个微服务
战术设计: MVC 三层架构下,Service + 数据模型的开发模式,会让 Service 扁平的、大量的,平铺出非常复杂的业务逻辑代码。再加上行为对象与功能逻辑的分离,贫血模型的开发方式,让行为对象的不断交叉使用,也是让系统不断增加复杂度,并到难以维护的根因。
充血模型:可以指一个对象,它包含自身的属性之外还提供与自身相关的一些行为逻辑,比如信息校验、拼接缓存key的方法。当然我觉得包也可以视为充血模型,因为DDD框架下包里包含模型、仓储服务、工厂等服务。
领域模型:原来的MVC三层框架是将一个服务分为controller、service、DAO,主要聚焦于service和DAO交互。DDD框架

每一个方法的入参都使用对应的对象封装,提升代码整体的聚合度和可读性。
每一个行为都对应一个接口,整体抽奖的标准流程又实现了接口的抽象类给出,具体的行为下沉到抽象类的子类来实现
在 DDD 规范中,通常会把具有唯一ID标识,影响数据库数据变动的操作,定义为实体对象。而用于描述对象属性的值,如枚举值、没有生命周期对象,可以被定义为 VO 对象。
活动领域下:
装配策略领域、额度领域、参与抽奖领域
做聚合对象的目的是为了给多分表入库做统一的事务。
23、用户相关的表分库分表
为什么要分库分表:
目前公司的分库分表策略非常成熟,在此基础上开发的效率不低于单库单表 ,所以不需要等到数据量比较大的时候再分库分表,这样做一来没必要,而来后期数据迁移成本高。
分库分表的数量是2的n次幂,这样计算hash的时候不会出现散列偏差
一开始就使用分库分表是否会占用更多文件资源呢?
我们可以在一台物理机上去创建5个虚拟机,那么五个虚拟机里面分别部署一个数据库进行文件存储,我分库分五个库,实际上也只是用了一台机器,后期业务量上来了迁移成本就是硬件成本。
24、如何实现库存不超卖?
答:用Redis来抗高并发,用Redisson延时队列异步地去数据库扣减库存。具体来说
子问题 1:Redis扛不住挂了怎么办?
子问题 2:如果用分布式锁咋实现?间八股文的分布式
子问题 3:Redis扣减库存如何保证线程安全问题
我这里调用的是rissonClient.getAtomicLong(key).decrementAndGet(); 先拿到key对应的原子长整型对象,调用它的decrementAndGet方法,这个方法会向Redis发送decr命令,它是Redis内置的原子命令,因为Redis是单线程,所以它可以保证多线程环境下对变量的操作是原子性的。
25、Zookeeper的Watcher机制实现动态配置
1、ZK基本概念
ZK是一种中间件,可以为分布式项目提供分布式协调服务,例如
(1)数据结构:从逻辑上看ZK内存是一个树形结构,每一个节点对应一个路径(目录),一个节点有多个子节点,使用的是绝对路径,根节点是/ 。
ZK使用HashMap存储树形结构,key是全路径字符串,value是节点对象,包含节点信息。
(2)数据存储:ZK在内存中也存数据,ZK在磁盘上规定两种数据类型,一种是log,一种是snapshot。log文件是增量记录,保存写请求。snapshot是全量记录,是一种内存快照。
(3)数据一致性:因为对于每次请求,ZK采用先记录磁盘再改内存。ZK正常退出的情况下会强制刷磁盘文件和生成快照。
(4)ZK服务端的集群部署:
集群中有三种角色:Leader、Follower、Observer。
Leader:处理写请求、负责数据同步(ZAB)、被选举产生
Follower:处理读请求,参与Leader选举,接受数据同步并持久化数据
Observer:仅处理读请求,用户拓展集群的读性能。
一个集群有且仅有一个Leader,Leader负责对整个集群的写请求事务进行提交。Follower和Observer只能处理读请求。但是Follower可以参与Leader的投票竞选,Observer没有权利投票。
(5)选举过程:
涉及三个信息:epoch(选举的轮次)、zxid(本节点处理过最大事务的ID)、myid(每个节点都有一个myid文件,文件内容就是一个数字,是全局唯一的)
先比较zxid,再比较myid,因为myid不一样,一定能比出来。
(6)服务端与客户端
服务端:Leader、Follower、Observer
客户端:
连接多个服务端节点,创建/删除节点、读取节点数据,在节点上注册Watcher监听节点变化。
进行会话管理,通过心跳信号保持会话活跃,会话超时后终止。
(7)服务端如何保证客户端请求的顺序?
ZK底层用Map存储客户端的请求的,key就是客户端的表示,value就是一个先进先出队列,存储该客户端的请求。所以只能保证同一个客户端的请求顺序,不同客户端的请求顺序不能保证。
(8)客户端的key是什么?
客户端连接后会分配一个sessionid,sessionid=时间戳+节点myid+底层特性生成的long字段,保证不重复。
(9)客户端与服务端之间的会话过程
客户端在连接ZK会上报超时时间,再加上sessionid。ZK服务端会维护一个ticktime配置,将不同客户端的超时时间映射到相同时间间隔的时间点上,然后把这个时间点和sessionid关系存起来。
后台线程就可以根据这个时间间隔周期性给过期客户端发送会话过期的信息,也就是轮询的周期可以固定下来。
客户端可以每次对话会刷新超时时间,并且可以通过ping操作在空闲的时候刷新超时时间
(10)心跳机制:
客户端与服务端通过TCP连接。
客户端:发送心跳信号Ping请求给服务端,如果没有收到响应会自动重试(也可以连接其他节点),如果所有节点都无响应就不会发送请求。
服务端:接受心跳信号并响应。
进行会话超时检测:记录每个会话最后一次心跳时间,如果超过超时时间没收到心跳信号就会清理会话。
心跳处理优化:异步处理心跳信号,使用批量确认机制减少网络开销
2、ZK的Watcher机制实现动态配置
ZK监听的的路径是申请的系统使用地址,运维人员在配置中心修改配置的时候,ZK的Watcher会监听到节点值变化,然后发送消息给动态配置中心(动态配置(ZK客户端)会通过ZK客户端向ZNode节点注册一个Watcher),动态配置收到ZK发送的消息之后会通过java反射更新对应属性的值。
动态配置使用注解来标记需要变更的字段,在在动态注解中创建一个ZK连接对象。
启动时赋值:扫描bean对象的属性上是否标注了指定的注解,如果ZK节点有值,就把这个值复制给该属性,如果ZK没有值,就把注解中的默认值赋值给该属性。
运行时动态赋值:客户端会向ZK服务端创建一个Watcher来监听节点变化,节点发生变化时ZK服务端会发送信息给客户端,客户端通过java反射修改属性值。
26、如何保证接口的可用性
限流&黑名单:
使用AOP切面类的环绕通知对指定需要限流的接口进行增强。具体来说,根据ZK动态配置的开关来决定是否要开启限流。生成用户访问频次Map(指定频次的过期时间24小时)与黑名单(指定过期时间24小时)Map限制用户的访问。先判断用户是否为黑名单,再判断用户访问频次是否达到上限。如果需要限制,就返回一个固定的结果回去。
降级:
根据ZK动态配置的开关来决定,是否需要进行接口的后续逻辑
熔断:
使用注解Hytrix,接口调用超时150ms就熔断

27、XXL-Job、Spring、Quartz对比
1、Spring定时任务只能在单机上实现任务调度,不能分布式调度
2、XXL-Job可以做分布式任务调度,部署方便,只需要部署调度中心和注册执行器即可在界面进行任务调度。XXL-Job还提供了任务调度的监控与报警功能可以监控不同节点的任务执行情况
3、Quartz也是分布式任务调度,他部署起来比较麻烦,得从服务到数据库都需要配置。并且监控和报警功能要自己配置。

28、对于redis资源加锁的问题:
(1)独占锁:适合于个人行为,不对集中资源占用的情况,例如用户开户、下单场景下防止用户重复点击(幂等),重复产生一些申请授权。
(2)无锁
(3)分段锁:适合于对集中资源存在竞争的情况,例如秒杀场景下库存扣减,这个时候无论怎么考虑锁的快速释放都会产生大量的等待时间,所以要把锁的粒度细化,这个思想类似于InnoDB相比与其他引擎多了行级锁的原因。我们只对库存的一个一个编号加锁
(4)性能对比:我个人在本地压测过用多线程来扣减库存,对于1000条库存,无锁扣减库存大概花了4毫秒,独占锁扣库存大概花了120毫秒,分段锁去扣减库存大概花了4毫秒。
此时分段锁和无锁实际上性能非常接近,比独占锁好很多!
究其原因就在于,独占锁同一时刻只有一个线程拿锁,扣减库存,但是分段锁同一个时刻只有一个线程扣减库存(因为decr操作在redis单线程中顺序执行,没有并发问题),但是这些线程加锁不用等,扣完就能加锁,因为锁的key由业务前缀+库存剩余组成的,这些锁它是动态的,不一样。
而且这种分段锁也可以实现不超卖。
29、redis里存啥了
1、奖品库存:业务前缀+策略ID+奖品ID:奖品库存
2、概率范围(list):业务前缀+活动ID+策略ID:[“奖品ID1”,“奖品ID2”,…]
3、概率表(hash):业务前缀+活动ID+策略ID:奖品ID:[最小值,最大值]
4、策略权重:业务前缀+策略ID+权重值:奖品ID1_奖品ID2_奖品ID3
30、Guava本地缓存实现单机限流
使用单机还是全局可以使用ZK进行配置
使用Guava限流是单机限流
使用Guava与RateLimiter(底层是令牌桶算法)对用户访问的频次进行限流,Map<String,RateLimiter>,把用户ID作为key,RateLimiter实例作为value放到本地缓存中,用户访问的时候会调用RateLimiter.tryAquire()去拿令牌,返回true就放行,否则直接返回结果。我这里设置的令牌桶是一秒生成一个令牌,Guava的key-value对的淘汰策略是写后1分钟就过期,也就是对用户限频记录是1分钟,此后没有操作就过期。
使用Guava存储黑名单,Map<String,Integer>,Guava的key-value对的淘汰策略是写后24小时就过期,也就是对用户限频记录是1分钟,此后没有操作就过期。黑名单的拦截逻辑是用户访问被限流的次数超过一定次数就会被加入黑名单。
使用注解的形式对目标方法进行切入,注解的属性有:Guava存储的key、触发限流后被调用的方法名、每秒允许请求的数量、加入黑名单的阈值
@RateLimiterAccessInterceptor(key = “userId”, fallbackMethod = “drawRateLimiterError”, permitsPerSecond = 1.0d, blacklistCount = 1)
31、redis分布式缓存实现全局限流
redis限流是一种全局限流,它的配置和本地限流类似,使用redisson.client获得ratelimiter进行限流,但是需要注意的是,因为这是分布式缓存,所以黑名单中的限流次数需要用AutomicLong(原子计数器)保证线程安全。然后设置rateLimiter、黑名单key的过期时间。
redis服务端层面是不会出现线程安全问题,因为命令都是单线程一个一个执行,但是在客户端就会出现线程安全问题,主要是多个客户端同时请求会出现竞态问题。
解决办法:使用原子操作(INCR、DECR,这些命令执行的时候不会被中断)、使用事务(麻烦)、使用LUA脚本(把多个命令封装到一个脚本中一次性执行)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值