抽奖项目技术亮点

活动是通过秒杀领取的。(即:活动对应着某一商品)
这里超卖指:对于一个活动它的参与量有数量限制,就是活动的库存,当活动的领取数大于活动库存总量,就是超卖
用户秒杀参与活动的资格(领取活动)

表设计

针对这个抽奖系统,会有不同的抽奖活动那就是【活动表】,不同的抽奖规则【抽奖策略表】,对人群的过滤【准入规则表】
然后记录用户信息的【用户表】,一个用户可以参加不同的活动【用户领取活动表】,抽奖完成后会生成该用户的【抽奖单】。

怎样保证幂等性

(1)在用户领取活动表中添加state状态用于记录当前领取的活动有没有执行抽奖。目的是当抽奖过程中发生失败(系统,网络等原因),还没生成抽奖单到数据库中,这时用于保留未使用抽奖的状态,避免又去重复领取一遍活动。
也就是说:用户领取活动后,当前活动抽奖为未执行状态,
抽奖活动开始执行时,先判断活动state,未执行才抽奖,否则不抽。以此避免在抽奖过程中发生失败,该次领取活动失效,又要重新领取活动导致该用户的活动总次数减少。

先说下面这俩(上面这个感觉表达不清楚):
(1)用户参与一次抽奖对应一个抽奖单:这是通过【用户领取活动表】中的领取ID(雪花算法),对应生成抽奖单的UUID,UUID设置了唯一约束,用来保持幂等性

(2)怎么保证kafka重复消费的幂等性?【用到MQ场景:Redis扣减数据写回DB;发奖】
生产者发送每条数据的时候,里面加一个全局唯一的业务id,消费者拿到后,先根据这个id去Redis里查一下之前消费过吗。如果没有消费过,就处理然后将id写入redis。如果消费过了,就不处理,以此保证不重复处理相同的消息。

抽奖单中添加mq_state标识MQ消息发送是否成功,如果发送失败就通过定时任务补偿MQ消息;发送成功就更改mq_state状态。

mq为什么出现非幂等性情况

1、生产者已把消息发送到mq,在mq给生产者返回ack的时候网络中断,故生产者未收到确定信息,认为消息未发送成功网络重连后生产者重发消息,但实际情况是mq已成功接收到了消息,造成mq接收了重复的消息
2、消费者在消费mq中的消息时,mq已把消息发送给消费者,消费者在给mq返回ack时网络中断,故mq未收到确认信息在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。

解决办法
1、mq接收生产者传来的消息:
mq内部会为每条消息生成一个全局唯一、与业务无关的消息id,当mq接收到消息时,会先根据该id判断消息是否重复发送,mq再决定是否接收该消息。

2、消费者消费mq中的消息:
也可利用mq的该id来判断,或者可按自己的规则生成一个全局唯一id,每次消费消息时用该id先判断该消息是否已消费过

项目中哪里使用事务

使用自研路由组件分布式事务如何解决

1.在活动领取流程涉及路由切换的分布式事务,面对这个问题,为了避免同一个事务下,连续操作DAO而多次调用自定义注解的路由切换,导致声明式事务失效。
所以,将数据源的切换放在事务处理前,事务操作通过编程式参与次数表的活动次数扣减写入用户领取活动表连在一起合并为一个事务,保证两次操作的原子性,进行处理。
【这俩表是同一个数据源,只是DAO操作上就添加着路由切换,就会执行,从而导致事务失效】
2.鉴于抽奖系统的实时性要求,希望用户流程体验更加流畅,支撑更大的并发量,没有对整个流程添加过多的或者大块的事务,降低性能,而是采用最终一致性的方式进行处理
3.由在面对秒杀场景时,在分库分表后可以支撑更大的秒杀体量。同时对于单key的秒杀,还采用了滑块分段锁的方式使用redis和MQ进行处理,来提高吞吐量和减少数据库压力。

注:为什么切换路由会使声明式事务失效?
因为虽然路由组件通过AOP计算出了路由,但没有取到,而是复用了Spring事务给我们保存的connection,所以引起了路由失效。
(具体来说有些复杂见 Spring声明式事务引起的路由失效分析

解决方式:
解决方法正是在Spring事务开启之前,就手动地计算路由保存到RouterHolder之中,再手动开启Spring事务,这样就能取到正确的路由。

分布式事务是怎么实现的?

seata 两段式提交,但基本大家用的不多。因为要尽可能降低对数据库连接的长时间占用,要做到快速释放连接。
所以基本都是 MQ 和任务补偿做最终一致。
【保证一致性就是保证并发安全】
疑问:
(1)‘MQ 和任务补偿做最终一致?’ MQ怎么解决的分布式事务????
(2)MQ有没有做持久化:. MQ 消息是基于库表记录的任务扫描发送消息的,所以是有对应的持久化处理的。另外也可以创建一个单独的 Task 表,表中专门写 MQ 消息记录,用于发送失败重试等,这样可以统一管理。

怎么解决你和其他服务之间的分布式事务的?

我编的:

(1)秒杀场景下:使用redis decr奖品库存扣减并Setnx设置库存锁兜底保证不超卖,库存扣减写入异步延时队列,并定时任务扫
描扣减库存,缓解数据库压力。
(2)将发奖流程使用MQ异步处理。
(3)同一个库里的不同表间的增添和修改,使用spring的注解声明式事务处理
(4)分库分表后的用户抽奖单,为了保证分布式事务处理使用编程式事务

1.项目设计了那些表

  1. 先介绍业务:抽奖系统作为营销活动平台中的一个环节,承接着活动玩法、积分消耗、奖品发放等系统的纽带,帮助整个业务完成用户的活跃。
  2. 后阐述领域:实现上要尽可能做到职责隔离,对应系统的具体实现上要拆分出:活动、算法、规则、策略、用户、订单等领域
  3. 引入表设计:根据领域驱动中对各个模块的定义,设计数据库表,也就对应了活动表、抽奖策略配置表、准入规则引擎表、用户抽奖单记录表、以及配合这些表数据结构运行的其他表,如:记录用户领取活动表、用户活动参与次数表 等。

针对这个抽奖系统,会有不同的抽奖活动那就是【活动表】,不同的抽奖规则【抽奖策略表】,对人群的过滤【准入规则表】
然后记录用户信息的【用户表】,一个用户可以参加不同的活动【用户领取活动表】,抽奖完成后会生成该用户的【抽奖单】。

1.1 哪些表设置了唯一键

2.为什么自研路由组件

  1. 我们的做法是因为有成熟方案,所以前期就分库分表了。但为了解释服务器空间所以把分库分表的库,用服务器虚拟出来机器安装。这样即不过多的占用服务器资源,也方便后续数据量真的上来了,好拆分。
  2. 市面的路由组件比如 shardingsphere 但过于庞大,还需要随着版本做一些升级。而我们需要更少的维护成本
  3. 我们的路由组件可以分库分表、自定义路由协议,扫描指定库表数据等各类方式。研发扩展性好,简单易用
  4. 自研的组件更好的控制了安全问题,不会因为一些额外引入的jar包,造成安全风险。
  5. 不能为了等到系统到了200万数据,才分库分表,那么工作量会非常大。

我们的这个路由组件,只是针对该抽奖系统的,在将用户的大量抽奖单在保存到数据库中时,通过用户ID计算出对应的库和表,将用户抽奖单使用分库分表保存来减轻数据库压力,

2.1路由怎样实现的

(1)自定义一个注解@DBRouter(key = “uId”),用于放置在需要被数据库路由的DAO操作上。比如新增用户领取活动。
(2)在AOP 切面拦截中,根据用户ID进行相应的数据库路由计算,并且使用扰动函数加强散列,得到一个索引位置后,在根据库表的数量折算出具体落到那个库表中,最后将计算的库表信息保存到线程的ThreadLocal中。
(3) 通过Mybatis 拦截器,拦截 SQL 语句动态修改添加分表信息,再设置回 Mybatis 执行 SQL 中。
具体操作:获取StatementHandler,获取自定义注解判断是否进行分表操作,statementHandler.getBoundSql()语句获取SQL,从Threadlocal中读取目标库表,替换SQL表名,最后通过反射修改SQL语句

扩展编程式事务

如果一个场景需要在同一个事务下,连续操作不同的DAO操作,那么就会涉及到在 DAO 上使用注解 @DBRouter(key = “uId”) 反复切换路由的操作。虽然都是一个数据源,但这样切换后,事务就没法处理了。
解决:这里选择了一个较低的成本的解决方案,就是把数据源的切换放在事务处理前,而事务操作也通过编程式编码进行处理。

3.规则引擎的设计目的

  • 主要作用是解决抽奖场景中个性化运营的处理,如:人群身份标签、交易记录、活动资格等规则的可配置化的交叉使用。
  • 所以基于这样的情况,此规则引擎的设计是一个二叉树判断,实现手段运用到了组合模式、工厂模式等。并为了便于维护和使用,进行了库表对二叉树的抽象设计,树根、节点、子叶,映射为二叉树编码的相关属性信息。

因为用if-else语句去判断是哪种数据比较麻烦且代码量大大增加,对以后的维护增加了难度,所以我们使用组合模式,将对象组合成树形结构。

搭建规则引擎树,需要的表【规则树总表】【规则树结点表】【规则树结点连线表】,规则树节点放在数据库中方便动态化配置,
每个节点的逻辑就是一个过滤器(作比对),最后交给树结构执行引擎串联节点间的关系,最后将接口交给外部调用
可以在传入信息或者数据库里,拿到比对值然后在树结构节点里做比对,
在执行引擎里,遍历树结构,while(判断叶子结点还是中间结点){拿到中间结点的决策key(就是判断依据age/gender…),得到具体值,放到过滤器中得到下一个节点往左侧走还是右侧走},遍历到叶子结点结束,就能拿到最后的活动号

3.1怎么使用规则引擎过滤的

规则引擎的设计是一个二叉树判断,通过使用组合模式,将判断节点组合成树形结构,就不用使用if-else语句去判断,便于维护和使用。
这个规则引擎包括:logic 逻辑过滤器、engine 引擎执行器。
逻辑过滤器是一个个二叉树的判断结点
引擎执行器就是在遍历树结构,通过从该树的根节点开始 ,while循环判断,是中间结点,就拿到中间结点的判断依据,查询用户的具体属性,然后放到过滤器中得到下一个节点往左侧走还是右侧走,直到到达叶子结点结束,最后得到该用户筛选后能参与的活动ID

DDD的分层架构,那讲下每个领域的实体

  1. 首先如果理论看的多,喜欢问实体。因为大部分理论是说实体对象是充血模型。但如果开发的多知道只把实体看做领域很难编写代码,要把整个领域模块看做充血模型,之后问每个领域模型是如何设计的。
  2. 那么无论怎么问,你只要回答各个领域模型是如何设计的即可。比如Rule规则领域模型,实体对象类有哪些,聚合对象类有哪些,怎么实现的流程,如何提供的服务。

4.抽奖算法

使用的单项随机概率抽奖就是分配好的奖品概率是固定的。
将概率值存放在数组中,根据概率值直接定义中奖结果,比如20%的一等奖中奖率,就开辟100的数组空间20个经过散列后随机分布的下标位置能中一等奖,
抽奖时用户随机在100范围内生成数组的索引+扰动,查找对应位置对应是否有奖,用空间换时间。
(不公布抽奖结果,大量抽奖并发打进来概率是一样,中奖抽空的位置数组设为没奖)

5.使用了模板模式、组合模式、工厂模式解决代码的

1.模板模式处理抽奖流程,
基于模板设计模式,规范化抽奖执行流程。包括:提取抽象类、编排模板流程、定义抽象方法、执行抽奖策略、扣减中奖库存、包装返回结果等。主要就:以抽象类 AbstractDrawBase 编排定义流程,用 DrawExecImpl 做具体抽奖流程实现。
比如抽象类中定义:1. 获取抽奖策略2.判断是否可以进行抽奖3.执行抽奖算法4.包装结果
具体实现类:抽奖过程具体实现。
2.工厂搭建发奖domain
本质:就是为了简化if else判断不同类型使用不同的代码处理, 使用map将不同的类型和对应的代码联系到一起。让代码变得更整洁。
工厂模式:是一种创建型设计模式,在父类中提供一个创建对象的方法,允许子类决定实例化对象的类型。
工厂模式通过调用的时候提供发奖类型工厂返回对应的发奖服务。通过这样由具体的子类决定返回结果,并做相应的业务处理。

“发放奖品”工厂作用:外部提供一个奖品类型,工厂提供这个奖品类型需要提供什么样的服务去处理。
3.组合模式
组合模式搭建用于量化人群的规则引擎,用于用户参与活动之前,通过规则引擎过滤性别、年龄、首单消费、消费金额、忠实用户等各类身份来量化出具体可参与的抽奖活动。
组合模式就是不用ifelse来判断,而是通过组合节点搭建一棵二叉树,而库表中则需要把这样一颗二叉树存放进去

6.秒杀

在抽奖这种短时间峰值流量高的场景下,奖品的库存扣减上,我们先扣减Redis中预热的库存(库存预热),再通过异步MQ和定时任务来更新数据库,从而减少瞬时对数据库的压力。

由于我们使用 Redis 代替数据库库存,那么在缓存的库存处理后,还需要更新数据库,保证数据库与缓存的一致性。
可以通过定时任务,发送一个 MQ 消息对数据库中的库存进行处理。因为 MQ 可以消峰,减少对数据库的压力。

6.1为什么用滑块锁

滑块锁最早是为了恢复库存,但其实还有另外一个作用。
如果缓存失效,key被删除,缓存key从0开始计数,那么你之前已经对key… key_n加锁了,这样可以保证不超卖。避免风险。

6.2秒杀场景下,使用Redis decr奖品库存扣减和SetNx设置库存锁兜底保证不超卖,

使用decr操作时(相当于当前线程拿到锁)能保证线程安全,将对应的key值-1后返回结果,如果结果<0就用incr将库存+1,恢复库存。
当然可能出现网络问题导致incr没恢复库存,这种情况发生比较少,但至少发生后只会造成库存剩余(没全卖出),而不会造成超卖。为了解决这个问题,可以使用lua脚本将库存扣减和回复变成一个原子性操作或加一个分布式事务锁,但这可能会牺牲一部分性能。

6.3为什么还要setnx加个锁兜底呢?

decr 请求操作也可能在请求时发生网络抖动超时返回,这个时候decr有可能成功,也有可能失败。setNx对该商品编号加锁,避免了其他行啊成占用该商品,会更加可靠。
setNx是对商品编号(活动ID)加锁,一般在确认领取后(插入领取记录)删除锁,如果删除失败可以定时在活动结束后删除。这样并不会导致死锁,虽然这个商品最后没有卖出(活动没领取),最重要的是保证不超卖。
setNx 因为是非独占锁,可以在活动结束后释放;而独占锁在秒杀过程中不好把握线程释放时间,释放的晚了活动用户都走了,释放的早了,流程可能还没处理完。
如果没有锁,可能会超卖。

也就是说:decr操作扣减库存时可能发生网络抖动导致redis扣减失败,但当前用户依旧是按照获得的活动在进行接下来的抽奖,
这时有别的线程来将会重复获得当前活动再扣减库存,就导致了超卖问题
所以我们在decr扣减库存后用setnx对当前商品的具体编号加锁
即使网络抖动发生在decr时导致扣减失败,setnx也能确保这个活动(编号ID)不会被其他线程占用。直到改领取信息被写入用户领取记录中后,才被删除。

6.4库存恢复(是网络故障导致incr没把库存加回去)

关于库存恢复,一般这类抽奖都是瞬态的,且redis集群非常稳定。所以很少有需要恢复库存,如果需要恢复库存,那么是把失败的秒杀incr对应的值的key,加入到待消费队列中。等整体库存消耗后,开始消耗队列库存。

为什么异步MQ +定时任务

由于我们使用 Redis 代替数据库库存,那么在缓存的库存处理后,还需要更新数据库,保证数据库与缓存的一致性
可以通过定时任务,发送一个 MQ 消息对数据库中的库存进行处理。
因为 MQ 可以消峰,减少对数据库的压力,而且使用定时任务也是为了避免MQ消费引起并发问题,所以如果并发量较大,使用定时任务处理缓存和数据库库存同步。

我自己的疑惑:

xxl-job怎么还有数据库表单?
Xxl-job是一个分布式任务调度平台。它使用数据库来存储任务调度相关的信息,如任务调度状态、执行日志等。因此,即使你不直接操作数据库表单,xxl-job依然需要数据库来保存这些信息。

如果你指的是需要创建Xxl-job特定的数据库表单,那么你可以在Xxl-job提供的资源中找到SQL脚本。通常,这些脚本会在其源码包中的"doc"目录下的"db"子目录中。你需要根据你使用的数据库类型(如MySQLPostgreSQL等)来选择相应的SQL脚本执行。

7.为什么用Kafka

消息队列主要用于:在分布式系统中存储转发消息。场景:异步处理,应用解耦,流量削峰和消息通讯四个场景。
使用MQ消息的特性,把用户抽奖到发货的流程进行解耦。这个过程中包括了消息的发送库表中状态的更新消息的接收消费发奖状态的处理等。

在抽奖单中加入mq_state字段用来判断是否发送成功,定时任务检查扫描用户的抽奖单看mq_state是否标记为已发送,发送失败的话就需要补偿发送MQ消息,发送成功消费者处理MQ消息,执行发奖。

8.为什么用xxl-job

XXL-JOB是一个分布式任务调度平台,处理需要使用定时任务解决的场景。

xxl-job在抽奖系统中主要扫描数据库中是否有MQ消息发送失败,并且定时控制活动状态的变更。
具体实现:
主要用在通过定时任务扫描用户的抽奖单看mq_state是否标记为已发送,发送失败的话就需要补偿发送MQ消息,发送成功消费者处理MQ消息,执行发奖。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值