# 《大营销平台系统》,关于面试中的技能、简历、问题汇总
作者:小傅哥
博客:https://bugstack.cn (opens new window)
课程:https://t.zsxq.com/17gswKIeX (opens new window)
沉淀、分享、成长,让自己和他人都能有所收获!😄
此部分主要用于向读者提供星球项目之一的《大营销平台系统》项目如何体现到简历中,包括;专业技能、项目经验。
# 一、项目介绍
面试官您好,大营销平台的 Raffle 抽奖模块,是我独立负责实现的一个(学习/工作)项目,此项目模块在架构设计上运用了 DDD 分层架构和模板模式、责任链模式、组合模式、工厂模式等,这样的设计模式对业务流程进行解耦和实现。
Raffle 抽奖模块的完整开发,让我对 SpringBoot 框架技术,分布式技术栈的运用更加熟练,也把设计模式在实际场景的使用了起来,积累了丰富的设计实现经验。这些技术学习的内容,也可以更好的应对以后的开发工作。非常感谢您给我这次面试机会。
# 二、简历模板
注意:🙅🏻♀️不要直接复制粘贴简历模板内容,以此结构和描述方式,用个人第1学习视角来描述。项目课程还包括;活动、积分、兑换、返利、分布式应用内容。学习到每个阶段后,可以写简历。写好自己的简历后可以使用 openai 4o模型 (opens new window) 告诉它你是一个 Java 技术专家,请根据描述内容,重新使用专业技术术语编写简历。几次下来,简历内容会非常好。
- 项目名称:大营销平台 - Raffle 抽奖/积分/兑换/返利服务 - 按需起名,可以是积分玩法,用户虚拟资产运营平台、积分返利抽奖服务等
- 项目架构:微服务架构、DDD 领域驱动模型、前后端分离设计
- 核心技术:SpringBoot、MyBatis、MySQL、Redis、SpringCloud/Dubbo【按需添加,只是对外的接口形式】、React、TypeScript
- 项目描述:Raffle 抽奖模块是整个大营销平台系统中非常重要的一个模块,也是本次项目中我来负责的设计和实现的模块。此模块主要以支撑各类差异化抽奖流程,如;通用抽奖、黑名单、人群、N消耗积分指定抽奖范围、抽奖N次解锁奖品等各类玩法的支持。在此系统模块的设计中运用到了模板模式、责任链模式、组合模式、工厂模式,解决代码的可扩展性,并对抽奖的计算和秒杀做了设计的优化,可以支撑单机 4c16g 服务器 500 ~ 800 TPS 的吞吐量(参考值,以实际压测为准),抽奖接口响应时长
45毫秒~100毫秒
左右。「不同服务器,带宽,以及是否还配置有环境相关,会有不同的数据效果」 - 核心职责:
- 以PRD文档诉求和对功能的评审,设计出抽奖的领域模型功能,以及在抽奖的流程抽象上,分为;抽奖前、抽奖中、抽奖后,的节点上扩展各项行为动作。如抽奖前的人群判断、抽奖中库存扣减、抽奖后兜底奖励等。
- 依赖于领域模型的定义,设计出抽奖库表。抽象抽奖过程为抽奖策略表、策略明细表、规则配置表、规则树动作表,这样会让抽奖更好扩展。
- 设计模板模式定义抽奖流程标准,再在模板模式中,调用责任链完成抽奖,对于抽奖中和后的动作使用组合模式的规则树进行动态处理【支持库表配置】。
- 在项目架构中定义统一标准的 api 由触发器层实现,在触发器层定义监听、任务、http、rpc模块,所有的行为动作,都理解为触发行为。
- 抽奖也是一种峰值流量高的业务场景,因此在设计奖品库存扣减上,采用了 Redis decr 分段消费和加锁兜底的设计,同时对于消费成功的库存,异步队列方式 + 定时任务更新库存。这样可以不超卖的同时,又减少数据库的压力。
- 在项目开发中熟练运用了 IntelliJ IDEA、WEbStorm、Docker、MySQL、云服务器、SSH工具,并已将项目完整部署到线上【在校伙伴可以提供线上案例版】。
- 项目采用分布式架构设计,Dubbo 提供 RPC 接口,Nacos 作为注册中心。多机实例任务由 xxl-job 进行调度管理。分库分表数据通过 canal 同步 binlog 日志到 ElasticSearch 进行聚合查询。
- 通过 RabbitMQ 解耦系统流程,包括;抽奖发奖流程、异步调账消息、异步行为返利,以及在 Redis 缓存库存消耗完毕后,推送MQ消息更新数据库库存和清空延迟队列。
# 三、面试问题
小傅哥本身也是一个面试官,面试过实习、校招、社招、架构师,不同的面试诉求会有不同的考察范围。通过这些考察范围的问题让求职者通过案例举证,来阐述自己的能力项。而这些能力项的匹配,则是招聘的要求。这里不得不提到,有些小卡拉米的简历,只言片语的项目描述,浮皮潦草的个人职责。是真的没进入面试就没了!
接下来是关于大营销平台项目的面试问题解答,可以参考。
- 架构设计:https://www.bilibili.com/video/BV17x4y187nS (opens new window)
- 设计模式:https://www.bilibili.com/video/BV1f2421M7kc (opens new window)
- 设计模式(扩展):https://www.bilibili.com/video/BV1oYs3ejENB (opens new window)
- 分布式问题:https://www.bilibili.com/video/BV1Cr42137de (opens new window)
# 1. 你的项目工程是怎么搭建的?
项目的搭建类似于使用 start.spring.io 脚手架创建的,在我们的项目中,使用了统一 maven-archetype-plugin 插件,自定义了一套 DDD 工程骨架脚手架。同类项目都是使用这套脚手架创建项目,因为脚手架定义了统一的版本标准和对应配套的开发环境,所以使用起来更加容易。【如果延伸提问,你还了解哪种搭建脚手架的方式,可以回答 FreeMarker (opens new window)】
# 2. 在你的这个项目中,都用到了什么开发工具?项目是怎么部署的
使用到了 IntelliJ IDEA、WebStorm、Sequel Ace/Navicat、ApiPost、Docker、SwitchHosts、Termius(SSH)等,项目上线的时候使用了2个方式,一个是本地构建前后端镜像,PUSH 到 Docker Hub,再通过编写 docker compose 脚本,在云服务器部署。另外一个是搭建 Jenkins 配置部署流水线的方式进行部署。
# 3. 整个项目开发过程中,你熟练的掌握了哪些技术栈
在整个项目开发中熟练的使用了 SpringBoot、MyBatis、Redisson 等技术框架,在编程功能实现,熟练的结合与 Spring 容器,通过 Map 自动装配 Java 语言实现的策略模式。以及在项目中较多的使用了 Redisson 框架,向 Redis 写入 key-value、queue、map、lock 等数据类型,实现业务功能。
# 4. 在项目开发过程中,你有遇到过哪些运行时异常,怎么排查解决的
如刚开始项目开发引入脚手架以外的组件,进行调试的时候,因为Jar版本不同。出现过编译通过,调用的时候方法不存在。通过 Maven Helper 插件,检查到有其他组件多引入了相同Jar包。另外还有一些如开发调试中发现的空指针问题,如查后需要增加空对象判断。此外其他一些更多是功能流程实现细节上,如项目中的规则树节点判断流程问题,抛出一些自定义的异常。这些通过在方法上断点调试逐步的解决。
# 5. 因为你的项目是前后端分离的,接口跨域怎么做的?
首先我们知道,Web跨域(Cross-Origin Resource Sharing,CORS)是一种安全机制,用于限制一个域下的文档或脚本如何与另一个源的资源进行交互。这是一个由浏览器强制执行的安全特性,旨在防止恶意网站读取或修改另一个网站的数据,这种攻击通常被称为“跨站点脚本”(Cross-Site Scripting,XSS)。
所以在我的前后端分离项目中,通过配置 @CrossOrigin 注解来解决跨越问题。开发阶段 Access-Control-Allow-Origin: *
、上线阶段 Access-Control-Allow-Origin: https://gaga.plus
# 6. 看到你的项目用到了 DDD,这也是我们很感兴趣的技术点,你可以介绍下你在使用 DDD 做这个项目时,都运用了 DDD 哪些知识。
DDD 是一种软件设计方法,软件设计方法涵盖了范式、模型、框架、方法论等内容,而 DDD 的很多规范约定,都是为了提高工程交付质量。如几个很重要的知识点;框架分层结构、领域、实体、聚合、值对象、依赖倒置等。它所有的手段,都希望以一个功能逻辑的实现为聚合,将功能所需的对象、接口、逻辑,按照领域划分到自己的领域内。
就像在这个项目中,我负责实现的抽奖中的策略,就是一个独立的领域模型。在这个领域中我需要提供策略的装载、随机数算法计算、抽奖模板调用(含责任链和规则树)功能,这样一个领域就像划分好的一个独立个体,它拥有属于它的对象信息(实体、值对象、聚合),当需要使用数据库资源、缓存资源,以及外部接口资源的时候,都通过依赖倒置进行调用。也就是说,我的领域不做其他模块的引入,而是领域只负责业务功能实现,所需的所有数据,则有外部接口通过依赖倒置提供。更多理论知识 (opens new window)
# 7. 你的抽奖流程中,哪些被定义为值对象,哪些被定义为实体对象
在 DDD 的规范定义中,值对象通常用于描述对象属性的值,不具备唯一ID,不影响数据变化。如;数据库中字段的枚举值、业务流程中属性对象。如抽奖流程中,RuleLimitTypeVO 规则限定方式的枚举值对象、还有 RuleTreeVO 规则树值对象。而那些实体对象,则具备唯一ID,会影响到最后的写库动作。如;抽奖发起实体、奖品信息实体对象。并且我们可以把一些和实体对象相关的功能聚合到对象内,这样的通用性会更好,避免所有调用方都需要自己编写逻辑。
# 8. 关于访问数据层的依赖倒置,是怎么使用的,有什么好处,你可以描述下吗
DDD 中的依赖倒置是一个非常好的设计,尤其是与 MVC 结构对比的时候,MVC 的贫血模型结构设计,数据库持久化对象,很容易被当做业务对象使用,这样后期非常难维护。但在 DDD 的分层结构用,是以 domain 领域实现为核心,一个 domain 领域下所需的外部服务,都由领域层定义接口,让基础层做具体实现。而数据库持久化操作,定义的 PO 对象,就被这样的方式被限定在基础层了,外部是没法引入使用的,也就天然的防止了数据库持久化对象进入业务中。
# 9. 我看你简历有提到,把抽奖划分为抽奖前、中、后,三个动作。请具体结合场景讲解下,为什么这样设计
这个的设计得益于在 Spring/MyBatis 框架源码的学习,在源码中经常会出现对一个流程进行拆分解耦,流程可扩展的点,如 Spring 是 Bean 对象的拆解,MyBatis 是会话流程的拆解。所以在设计大营销的抽奖模块时,对于需求中的各类功能点;黑名单抽奖、权重抽奖、默认抽奖、抽奖N次解锁、兜底抽奖等等情况,是可以拆解为抽奖前、中、后,3个行为动作的,基于这样的考虑后,就可以设计出非常容易扩展的松耦合结构。
# 10. 是什么场景下使用了责任链模式,什么场景使用了组合模式,为什么?
在设计完抽奖前、中、后,搜耦合的结构模型后,对于抽奖前要执行哪种抽奖,但单向选择问题。所以这里使用了责任链模式,进行节点流程判断,从黑名单、权重,最后到默认,走一个单独的具体抽奖,所以使用责任链更为合适。
之后是进入抽奖的中和后,这两部的流程是相对复杂的,需要判断用户抽奖了几次,对于不同次会限定是否能获得某个奖品,同时还有库存的扣减,如果库存不足或者不满足n次抽奖得到某个奖品,则会进行兜底。那么这就是一个树规则的交叉流程,所以会使用了组合模式构建一颗规则树,并通过数据库表的动态配置决定在抽奖前完成后,后续的流程要如何进行。
# 11. 抽奖也是一种瞬时峰值很高的业务场景,那么对于抽中奖品后的库存扣减是怎么做的?
关于库存的扣减,是一个非常重要的流程。尤其是这种单独资源竞争的场景,如果设计的不好,很容易把服务打挂。
所以在这套系统设计中,为了避免库存扣减直接更新库表的行级锁,而导致大量的用户进行等待状态。所以把数据库表的库存同步到 Redis 缓存中,在通过 incr 扣减的方式进行消费,同时为了确保在临界状态、库存恢复、异常处理等情况下不超卖,而对每一条产生从 incr 值,与抽奖的策略ID组合一个key,进行 setnx 加锁兜底,来保证不超卖。—— 这样的设计是颗粒度更小的锁方案设计,性能接近于无锁化。
# 12. 你讲到库存的扣减是通过 Redis 滑块锁实现的👍🏻,那么最终同步库是怎么做的,怎么降低对数据库的压力的?
关于 redis 缓存和数据库表库存数据的流程,设计了异步更新,保持最终一致性的设计。在执行完库存的扣减操作后(在抽奖中规则树库存节点流程),发送一个扣减完成到 Redis 的异步队列(可以使用MQ+延迟消费),之后通过定时 Schedule Job 来消费队列。这样就可以控制效率速率,降低对数据库的压力。(因为我们不能 Redis 扣减的多快,就直接打到库表上,那样对数据库的压力依然很大,容易打挂)
# 13. 你提到了接口的单一职责设计,这部分具体讲解下。
单一职责原则的核心思想是,一个类应该只有一个引起它变化的原因。也就是说一个类应该只负责一项任务或功能,如果一个类承担了过多的职责,那么这个类就会变得复杂,难以维护和扩展。
这样的原则在一些需要长期使用、迭代、维护的功能设计上,是非常重要的。我们要尽可能的让大营销的抽奖领域领域模块具备独立性,所以要使用单一职责原则。在这个原则约束下,设计了3个接口类;抽奖策略接口、奖品信息接口、库存处理接口(异步扣减等),这样3个接口的设计,在将来需要扩展的时候,会非常容易。(可能会问具体编码,问的比较多样性,这部分需要自己阅读代码来学习)
# 14. 在项目中你提到了可以支持不同场景的抽奖诉求,比如;多少积分后可以抽奖一个固定范围的奖品,或者抽奖n次后,才可以中奖某个奖品。这部分你是怎么做的?库表怎么设计的?
这块的流程,就是前面关于大营销抽奖领域模型的设计,从而确定的库表设计。也就是常说的领域->驱动设计。
库表包括;策略表、策略明细(库存、概率、规则key)、奖品表、规则表、规则树(3个表)—— 这部分在学习项目后,需要具备能在纸上画出库表ER图。
# 15. 你的抽奖接口响应时间是多少?
这样的问题主要考察你是否做了项目的上线,以及了解过接口的响应时间。如果做过就非常好回答,没做过乱说是挺容易被继续提问的。
参考数据;2c2g 云服务器,部署项目(含mysql、redis),占用63%内存,抽奖接口响应时间为38~55毫秒(项目有完整的手把手部署教程,还有监控部署教程,可以自己部署验证)。
# 16.(开放问题)你在做项目中,什么问题难住你的时间最长,为什么?
这是一个开放问题,重点考察你对项目的开发中个人的积累。你可以针对自己的学习过程中,有哪个流程的实现,让你最为有感触,即可回答。
如;可以对大营销抽奖模型流程的设计和库表设计,最为耗时,因为你不断的在思考如何拆解出一个好扩展的松耦合结构,同时拆解后,还要保证搜耦合下的高内聚。所以这部分是比较耗时的。同时也可以说在设定某个方法的,名称、入参、出参时,做了大量的思考。因为名字的定义非常影响以后的理解。好的代码就是文档,所以对于这样的东西花费不少时间。
# 17. 为什么使用分库分表,做了分库分表数据聚合查询如何处理?
首先我们要知道,是前期就做好分库分表方案,还是后期在做系统重构分库分表和数据迁移哪个成本高。显然是后面的成本更高。
而互联网中大厂中,分库分表的架构设计都是非常熟练的,因为有成熟方案,所以前期就分库分表了。但,为了节省服务器空间。所以把分库分表的库,用服务器虚拟出来机器安装。这样即不过多的占用服务器资源,也方便后续数据量真的上来了,好拆分。
那么这里的分库分表后的数据怎么提供汇总、聚合的查询呢?
这里需要用到阿里的 canal 组件,基于 mysql 的 binlog 日志,把自己伪装为一个从数据库,通过 dump 交换完成数据的接收和处理。最终把数据同步到 Elasticsearch 等文件服务中在提供聚合查询。对于需要实时的查询以及数据的处理,还可以用到 Flink 方式进行流式计算。
# 18. 抽奖奖品库存如何处理,怎么保证最终一致性?
在抽奖秒杀这样的场景下,都需要把库存缓存到 Redis 中进行使用。而不能数据库表加行级锁,否则大量的秒杀进行通过加锁和等待释放,就会夯住数据库链接直至拖垮整个服务。
那么使用缓存通过大营销项目中的颗粒度更低的分段锁后,怎么来保证一致性呢。这里需要3个步骤,首先是每次扣减完库存,都会写入到 Redis 延迟队列 / MQ 延迟消息,缓慢更新数据库库存。之后是 Redis 内的预热库存消耗完毕后,发送最终 MQ 消息,更新数据库的剩余库存为 0,最终活动结束后,还有任务补偿,扫描抽奖所产生的的参与记录单,更新最终的库存消耗。这里就可以用订单 MQ 通过 Flink 计算,更新最终库存也是可以的。
# 18. 写入中奖记录,发送MQ消息失败如何处理?
本身发送MQ是可能存在万分之一或者十万分之的失败的,而数据库操作和MQ操作,本身不能做数据库事务。但又要保证失败后的补偿处理。所以要结合中奖记录在写一条发送MQ的任务记录,任务记录上有一个状态,标记是否发送完成,这样就可以通过任务扫描的方式完成 MQ 的补偿发送。
但这里要知道,做完本身的中奖记录和任务记录后写库事务后,要顺序的可以是多线程的方式,完成一次MQ发送,并且更新数据库 Task 记录。这里是为了业务流程最快的推进,如果是更新失败也没关系,还有兜底的任务补偿。【任务补偿的数量并不多,但非常需要这个手段】
# 19. 生产者可能多次发送同一个MQ,怎么保证奖品不会超发?
这是一个幂等的设计处理,MQ 的消息是必须含带具有唯一标识的业务ID的。比如订单ID、奖品ID、支付单ID、交易单ID、贷款单ID等等。接收MQ的系统,通过唯一ID业务,更新或者写库的时候可以保证幂等性。这样也就不会产生超发的可能。
# 20. 抽奖算法如何提供O(1)时间复杂度,提高抽奖效率?
在大营销系统中,运营人员配置好抽奖活动后,开始上线对外后,会进行数据的预热数据。这个预热的过程会把活动信息、策略信息、库存信息都存储到 Redis 里进行使用。
而抽奖的策略就是记录了一个策略下N个奖品的概率,将概率转换为对应的整数数量,写入到缓存中。那么在抽奖的时候就按照整数数量生成随机数来抽奖。这样用空间换时间的效率是非常高的。
# 21. 如何对所有分布式节点的应用,活动信息本地内存更新?
通常我们会有诉求在不重启系统的时候,就要动态变更所有分布式应用节点中某个属性的值,如开关、缓存、调试日志开启/关闭、熔断、限流、或者抽奖黑名单以及概率等。这些东西通常不是 Redis 存储,而是应用中具体字段的属性值,这样效率更高。
而这个操作需要使用到类似于 Zookeeper 组件的临时节点监听,动态变更字段值。详细:https://bugstack.cn/md/road-map/zookeeper.html (opens new window)
# 22. 应用刚启动完成,外部调用过程中发现操作数据库连接池不足,超时断开,过一会又好了?是什么问题?
因为它是刚开始有问题,过一会又好了,所以很有可能是池化的连接数配置的最小值与最大值不是一个,这样应用就会先初始一个最小范围的连接数,随着调用没了在初始化到最大连接数。所以一般我们会把最小连接数和最大连接数配置为一个,避免使用的时候还需要初始化。因为初始化连接也是需要花费时间的。【再有注意配置链接的超时时间,不要太小,也不要太大】
# 23. 消费MQ的过程中,如果使用多线程会遇到什么问题?
这是一个非常容易产生事故的问题,本身 MQ 消费就是多个应用分别消费,如果有消费失败的,可以抛异常重试。但如果是一个消费 MQ 的应用,里面写了多线程,就可能会出现大量的 MQ 挤压,消费不过来,导致系统瘫痪。而如果你重启,那么这些拉下来的 MQ 消息也就随时丢失了。
# 24. 分库分表怎么让任务扫描到指定的库表?
分库分表以后,需要扫描每个库表中的任务表,则需要手动设定具体要扫描的库和表。如果分库分表的数量比较多,可以用不同的任务配置扫描不同的库表方式来部署,这样可以提高扫描效率。
如果说扫描出来的数据需要更低的延迟性,可以考虑做低延迟任务调度设计 (opens new window)。
综上都是微服务&分布式架构设计中会出现的场景问题,这些东西是需要实际编码学习才能更好的理解的。更多的问题汇总:https://bugstack.cn/md/project/big-market/notes.html (opens new window)
# 25. 如果在多机部署的情况下,是不是每台机器都会有这个定时任务,如果它们都捞到同一条发送失败的消息,会不会导致消息的重复发送?怎么避免?
问题:https://t.zsxq.com/4uoyl (opens new window)
- 幂等的这个是ok的,没问题的。
- 一个任务就是要有多机备份,避免一个挂了,就没有人执行了。之后这里的方案是加锁;
- 2.1 设计一个抢占锁,多个任务抢占同一个锁,谁抢占到了,谁可以执行。
- 2.2 如果抢占的执行失败了,删掉锁,重新执行。
- 2.3 如果删锁失败,对于是谁抢占的,谁可以做重入锁,继续执行。
- 2.4 锁有失效时间,如果抢占到的自己挂了,等待锁失效后,重新轮候抢占。
# 26. 抽奖服务量化人群指标是什么?拉新 留存 促活吗 感觉这些数据好难算 面试官问留存率 拉新成本 我都不知道怎么答
量化人群,是量化分析师,通过 Python、R 语言对历史数据进行建模(R语言有数据分析公式)。目的是为了根据一个人的,人标签和行为数据,跑出谁喜欢xxx,谁爱购买xxx,谁有xxx意向。之后创建出人群包。这样系统会根据人群包做抽奖、发券等的差异化。
# 27. 有个问题想确认一下 大营销系统和业务系统 是在同一个注册中心里吗,业务系统是直接调用抽奖服务的dubbo接口么
一个公司里,rpc 都是一个注册中心。
# 28. 关于幂等和流程解耦
- Q1: rabbitMQ判断重复消费的逻辑是什么,是直接在数据库中查询返利记录表是否有相同的订单ID(这个订单ID对应着当天日期?)的记录,如果发现重复就不消费?
- Q2: 感觉“签到返利”这一块也有点像幂等性的实现,即可能会执行多次签到动作,但每天只会执行一次返利操作?
- Q3: 对于“签到返利”这种行为,用MQ解耦是不是太小题大做了。首先它并不是什么高流量的行为,没必要用MQ来削峰填谷;其次,这样会不会导致执行了 签到 操作之后,用户积分得隔较长一段时间才会更新,出现数据不一致的情况?
回答:https://t.zsxq.com/zbUUb (opens new window)
# 29. 前几天面试官问我策略领域和奖品领域为什么要划分开
- 这部分要从建模讲,最开似的系统建模是整理出用户用例图,根据用例图梳理四色建模的领域事件。四色建模过程 (opens new window)
- 在领域事件脑暴完成,之后就是识别领域角色和对象,这个过程会显而易见的发现有抽奖领域、发奖领域。他们可以作为解耦设计,独立使用。因为抽奖不一定发奖,抽奖可以独立提供算法结果,由外部其他系统使用。发奖也可以除了抽奖的发奖,还有积分兑换的发奖等。如果抽奖和发奖合并,那么外部调用就会不那么清晰。
# 30. 根据抽奖问题问了如果把redis中 滑块锁过期时间设置为活动过期时间的时候,如果活动时间很长导致滑块锁过多怎么解决
可以考虑给活动库存的锁的key上年月日,每个日的key,明天就重新从新的key开始了。之后这样就比较好较短时间存储了。 更多讨论 (opens new window)
# 31. 分布式锁使用场景
项目中有两个地方使用了分布式锁,分别来处理库存的抢占竞争和分布式任务调度的抢占。
- 库存的抢占设计的是接近于无锁化的库存编号自增后加锁,做兜底设计,这样的用户的抢占就是 incr 后的结果加锁,降低竞争。
- 另外一个是项目是分布式架构,有多个任务执行(补偿mq、流转订单状态等),之后如2个任务,一起补偿发mq,避免发送多了,就会做一个抢占设计。谁先拿到可执行key,那么这个任务就执行。这样确保了,一个任务挂了,也可以有另外任务做处理。
# 四、公司面试
# 【美团】1. 领域模型是怎么设计的,抽奖过程是什么样,DDD四层架构和职责,以及为什么要这么设计?少卖和超卖。
问题:https://t.zsxq.com/180Qz9WCg (opens new window)
- 虽然目前大营销是开发了第1个阶段,但在前面讲解中把所有的流程是都介绍了。领域模型官方话术是头脑风暴,罗列事件和行为,在根据实体来划分领域的。简单说也就是根据业务流程划分的。这个过程包括;活动域、抽奖域、积分域、兑换域。也就是用户通过某种行为记录,发放计算,兑换活动参与资格,完成抽奖获得奖品。
- 抽奖的流程可以根据目前的设计实现来描述,举例,掘金的这个抽奖就可以用星球的大营销来实现;https://juejin.cn/user/center/lottery?from=lucky_lottery_menu_bar 可以多体验下,体验透彻了,在结合项目的实现,也就能描述出来了。
- 关于 DDD 的分层结构介绍,和每层的关系;https://bugstack.cn/md/road-map/mvc2ddd.html
- 超卖不会出现,有个保证的点,一个是 decr 值的限制,另外一个是对每个key加锁的兜底设计。确保了不会超卖。少卖是有可能的,核心原因是因为 decr 操作和数据操作不是是一个事务,有可能库存扣减完了,但最终操作库失败了。那么这个库存就丢失了,可能会少卖。但一般并不会对少卖做过多的流程,如果想管理,也可以把少卖的库存异常,加入单独的 redis 队列来重新消费就可以了。
# 【淘天】2. 对于数据库和redis的一致性怎么解决
问题:https://t.zsxq.com/18ddtmzqW (opens new window)
- 第1个手段;每次消费 decr 值,写入延迟队列,趋势更新数据库数据。最终一致。
- 第2个手段;decr 库存值消耗为0时,发送mq消息,更新最终库存量。(可能不准,比如 decr 值消耗中,少卖的情况)
- 第3个手段;活动到期后,任务扫描活动产生订单量,校准库存。
# 【其他】3. 什么情况下使用DDD架构,什么情况下使用mvc架构?
DDD 是软件设计方法,对复杂的项目更为合适。但这里要清楚,DDD 如果只的是设计方法中的建模工程结构,和 MVC 对比的话。DDD 的结构更为先进,即使不使用 DDD 的软件设计方法,只遵循这套结构,都是可以编写出非常好的代码的。MVC 的约束相对较低,个人开发还好,如果多人协作,会出现腐化严重的问题。
# 【其他】4. 设计模式带来了什么好处?举个例子
设计模式可以让工程设计的迭代性、扩展性、维护性,都更强,更好。如,抽奖策略计算中,用到了责任链、组合模式的规则树。规则树可以动态的调整配置的节点,来满足各种业务诉求。还可以结合产品需求,迭代的时候添加对应的节点开发就可以。避免了大量if...else的使用,让变动范围缩小到指定的类中。研发成本更低,提测质量更高(测试更快),交付效率更强。这些都是使用设计模式的优势。
# 【其他】5. 最近大营销抽奖项目被问到抽奖项目按道理应该不是一个高并发的过程,不是想那种抢优惠券这种属于高并发,那如果抽奖不是高并发,那为啥还要把库存缓存到redis去抗并发呢?
要好好留心日常的场景,都是面试的话术;
- 春晚红包是不是抽奖,拼多多一进页面就有各种【转转转】来获得一个券,支付完成又一个转转转。直接领券远没有抽奖来的刺激,即使是发券,也是用抽奖方式更多。
- 每秒的请求量如果超过1000tps,打到库上资源竞争,都会出现大量的数据库连接等待。一般一个应用分配的数据库连接池也就那么20来个。如果都打到库上,都能把库打挂。
- 面试这么问,大部分是为了通过一个质疑的场景,来看是否有思考过。而不是面试问什么就是什么,反而是问什么就不是什么,但不是什么,要拿出举证理由。
# 【其他】6. 抽奖算法提问
- 问:数据库路由算法 答:hashcode保证两次散列结果一致,扰动函数保证散列均匀。 问:除了hashcode数据库路由算法还有其他的吗?
- 问:抽奖除了加分布式锁还有没有别的思路? 答:这里本质上还是保证库存不超卖,说了低并发下的数据库行锁,redis队列 问:还有其他思路吗? 答:想不出来了(面试官说依赖高性能的中间态,这块没理解)
- 计次模型,设计一天一次的参与规则,怎么实现?
- 抽奖算法的实现?这个随机怎么可控,怎么避免高价的奖品一开始就被抽掉了,类似于活动中间阶段才能中一次一等奖
- 了解过其他抽奖算法吗?
- 黑名单如果上到一定规模,比如百万级别,有其他的设计思路吗?
- 扣减库存的分段竞态锁用 incr 还是 decr,为什么是 incr/decr?
- 如果活动开始了,要加奖品库存怎么办?
- 问题1;https://bugstack.cn/md/algorithm/logic/math/2022-11-05-fibonacci.html (opens new window)
- 问题2;还有,预生成中奖结果。比如微信春晚红包,提前把奖品随机好,写入到个人记录。用户开奖只是到点查看结果。【如果提到高性能中间件这个可以再继续让面试聊下,因为有可能问题和听者理解的不一致了】
- 问题3;这个课程里提供了总、月、日库存。
- 问题4;有次数锁、有权重,可以避免一开始就中一等奖。也是课程的。
- 问题5;算法如;线性同余生成器、梅森旋转算法、洗牌算法、加权随机。
- 问题6;黑名单一般会设定人群标签,写入到 Redis 使用。之后库里只是配置了一个人群标签的标识。
- 问题7;incr 和总量比,decr 和 0 比。decr 适合过程中不允许补充库存的。incr 可以在过程中添加库存,因为总量可以增加对比。【问题8】
# 【携程】7. 我跟他说用户在抽奖系统用积分兑换抽奖机会,我方会向mq发消息,积分微服务那边(不是我们组负责的)拿到消息之后扣减会员的积分,他问我:
- 你为什么在这种积分扣减用mq?我说流量削峰,他说这不是关键。
- ️如果用户用脚本频繁兑换抽奖机会,你们怎么应对的。
- 果下游服务迟迟没有对mq进行消息消费,你们怎么处理的?你假设作为系统的架构者,是怎么监控这种状况的。
解答:https://t.zsxq.com/0jjqr (opens new window)
- 当你描述为积分和抽奖是单独的系统,你要生成兑换商品订单,拿订单的唯一单号,rpc/http 调用积分系统扣减积分,返回一个交易单号和时间。【如果是微信、支付宝类支付,就需要等待回调,因为还要走支付渠道很多流程,没法直接返回】
- 你通过积分支付接口返回的信息开始变更订单记录,发放抽奖次数。
- 用户是用自己的积分兑换的,账户会进行额度拦截。之后接口会配置频次拦截【大营销第4阶段有这个设计】
- 系统是有边界管控的,在实际工作中,当前的系统要保证发送mq,下游的系统要保证消费mq。mq的消费会有监控配置,比如日常每分钟100次,如果连续n次在n分钟内,低于80次或者高于140次,则进行报警。这样就可以监控到了。
# 【其他】8. 关于上下游交互的问题
- 每次面试官都会问一嘴,基于ratelimiter是做单机限流吗? 实际生产中主要是使用分布式限流吗,为什么要基于分布式做呢,单机限流满足不了需求吗,分布式限流具体实现方式是什么呢,基于redis、zookeeper自己实现还是基于限流框架做呢
- 限流、降级有必要做到外部服务粒度的嘛?(例如有3个外部系统,有必要为每个系统单独限流、降级吗,例如只是不提供给某个系统服务)
- 奖品单状态的变更,我还是有一点模糊,如果是下游发奖,那下游应该是不能访问奖品服务的库吧,那下游发完奖如何更新奖品单状态呢?会有回调接口或者是下游再发一个发奖完毕的消息吗?
- 抽奖活动会和业务系统绑定吗?是不是得在活动表加一个字段,然后鉴权的时候只能调用当前系统的抽奖活动
解答:https://t.zsxq.com/7uTKz (opens new window)
# 【网易】9. 在抽奖过程时,需要预热处理,可是不可能让用户点击活动装配呀,这个问题你怎么解决?
第一个视频里提到了,是在活动运营配置后,可以审核通过就预热了。等待活动到了有效期就可以使用了。
# 【网易】10. 在抽奖的结果中,如果一个用户在前端抽到了一个奖品,对用户是可见的。可是在扣减库存时,却发现没有了对应的库存,此时应该怎么办?
库表设计中,抽奖的每条策略是有库存限制的,抽到后才会展示给用户。如果库存不足直接就是兜底积分了。这个也有人遇到过反过来问的,为什么要给策略上也加库存,直接奖品加库存不就可以。其实是不可以的。
# 【其他】11. 既然为了想承受更高并发使用redis做库存扣减,那生成奖品id后续要等中奖订单入库才能给用户展示结果吗?那用redis做库存扣减不就没意义了
最核心的其实是,如果没有redis做库存扣减,就要数据库里的一个库存记录做扣减。那么就会有成千上万条的请求在同一个表行记录开始独占竞争加锁,其余的请求进入等待,直至耗尽所有数据库连接。那么整个系统服务也就会被拖垮,一个普通的查询也会从原来的几十毫秒变成到一分钟也拿不到结果了。
使用redis,就是为了解决这个事情。之后在写入库里的记录,都是无竞争的。那么就不会让数据库被夯住。可以快速被处理。在大厂的数据库配置,基本这类操作不会被 redis 慢多少。