目录
1、请介绍整个系统后端的架构设计,有哪些模块以及各模块之间的关系?
2、你在项目中是如何设计库表的?可以从字段、索引、关联等方面回答。
3、为什么使用策略模式来封装不同的应用评分算法?它有哪些好处?具体如何实现?
[4、你是怎么根据应用来选取要执行的评分算法的?是用 if else 么?](#4、你是怎么根据应用来选取要执行的评分算法的?是用 if else 么?)
[8、你是怎么实现 AI 生成题目功能的?](#8、你是怎么实现 AI 生成题目功能的?)
[9、你是如何封装通用 AI 模块的?提供了哪些方法?](#9、你是如何封装通用 AI 模块的?提供了哪些方法?)
[10、如何流式调用 AI 并实时获取到 AI 返回的内容?](#10、如何流式调用 AI 并实时获取到 AI 返回的内容?)
11、你是如何实现一道一道''流式生成题目的,用到了什么算法?
[12、什么是 RxJava? 为什么使用 RxJava 来处理流?它有什么优点?](#12、什么是 RxJava? 为什么使用 RxJava 来处理流?它有什么优点?)
[13、什么是 SSE 技术?它有什么优点和不足?适用于哪些场景?](#13、什么是 SSE 技术?它有什么优点和不足?适用于哪些场景?)
[14、为什么用 SSE 技术将生成的题目实时返回给前端?有没有其他实现方案?](#14、为什么用 SSE 技术将生成的题目实时返回给前端?有没有其他实现方案?)
[15、你是怎么实现 AI 评分功能的?](#15、你是怎么实现 AI 评分功能的?)
[16、使用 AI 的过程中,有没有出现不稳定的情况?如果有,你又是如何处理的?](#16、使用 AI 的过程中,有没有出现不稳定的情况?如果有,你又是如何处理的?)
[17、你为什么要使用缓存来优化 AI 评分功能?这么做有什么好处?](#17、你为什么要使用缓存来优化 AI 评分功能?这么做有什么好处?)
18、你了解哪些缓存技术?在顶目中又是如何运用缓存的?比如怎么设计缓存?
[20、什么是 Redisson? 你在项目中如何使用 Redisson 实现了分布式锁?](#20、什么是 Redisson? 你在项目中如何使用 Redisson 实现了分布式锁?)
[23、你为什么使用 Sharding JDBC 技术实现分库分表?它的大致原理是什么?](#23、你为什么使用 Sharding JDBC 技术实现分库分表?它的大致原理是什么?)
[24、你是如何使用 Sharding JDBC 实现分库分表的?具体怎么分表、用了什么分表算法?](#24、你是如何使用 Sharding JDBC 实现分库分表的?具体怎么分表、用了什么分表算法?)
26、有哪些实现幂等设计的方法?你在项目中具体又是怎么实现的?
[27、什么是雪花算法?为什么它能生成分布式全局唯一 id?](#27、什么是雪花算法?为什么它能生成分布式全局唯一 id?)
28、什么是线程池隔离?你在项目中为什么要使用线程池隔离,有什么好处?
30、你的项目支持哪些统计分折功能?后端如何查询出要统计分析的数据?
[32、为什么想做这样一个 AI 回答应用平台?](#32、为什么想做这样一个 AI 回答应用平台?)
[33、为什么要开发 MBTI 性格测试小程序?它和你的答题应用平台有什么联系?](#33、为什么要开发 MBTI 性格测试小程序?它和你的答题应用平台有什么联系?)
[34、你有使用过 AI 工具来辅助编程么?都是如何帮你提高开发效率的?](#34、你有使用过 AI 工具来辅助编程么?都是如何帮你提高开发效率的?)
35、在开发过程中,你遇到过比较复杂的技术问题挑战吗?如果有,请谈谈你是如何解决这些问题的?
1、请介绍整个系统后端的架构设计,有哪些模块以及各模块之间的关系?
整个系统的后端分为:
- 用户模块:提供登录、用户增删改查等管理功能
- 应用模块:提供应用增删改查、管理、分享等功能
- 题目模块:提供题目增删改查、管理等功能
- 评分模块:提供评分规则定制、答案评分功能
- 回答模块:提供回答记录查看、管理功能
- AI 模块:提供 AI 创建题目、AI 智能评分功能
各模块之间的关系如下:
- 用户登录后,使用应用模块创建应用或者获取应用信息
- 创建应用需要调用题目模块生成题目或利用 AI 模块生成题目
- 用户在答题页面根据应用 appId 调用题目模块据得到题目列表,提交答案后调用评分模块,评分模块根据应用的类别执行不同的评分策略,比如自定义评分或调用 AI 模块评分
2、你在项目中是如何设计库表的?可以从字段、索引、关联等方面回答。
根据业务设计用户 / 应用 / 题目 / 评分结果 / 用户答题表。其中题目表采用 JSON 存储复杂的嵌套题目和选项,便于维护扩展,并通过给题目表添加 appId 索引提升检索性能。(感兴趣的同学可以自己测试一下性能的提高比例)
项目中按照业务功能分析,一共设计了用户、应用、题目、评分结果、用户答题五张表。
各表字段统一设置了 createTime、updateTime、isDelete 必备非业务属性字段,便于维护。
根据查询场景,部分表字段还进行了冗余,例如用户答题表需要展示评分结果的名称和描述,因此冗余了评分结果表的 resultName 和 resultDesc 等字段,避免用户查询答题记录时,还需要关联查询评分记录表,提升查询性能。
根据查询场景也做了一些索引设计,例如首页提供通过名称搜索应用功能,因此应用表的 appName 建立了索引。
再比如题目表的获取场景经常需要通过应用 appId 得到题目,因此题目表的 appId 字段也建立了索引。
3、为什么使用策略模式来封装不同的应用评分算法?它有哪些好处?具体如何实现?
因为使用不同评分算法进行判题的业务场景天然适配策略模式。
不同算法对应的就是不同策略,所以使用策略模式来封装了评分算法,便于维护和扩展。
具体实现方式:
1)定义 ScoringStrategy 接口,约束了评分的入参和出参。
2)定义 ScoringStrategyConfig 注解,一共有 appType 和 scoringStrategy 两个属性标识了策略对应的应用属性和评分类型。
3)每种评分算法都实现 ScoringStrategy 接口,且标注对应的 ScoringStrategyConfig 注解。
4)定义 ScoringStrategyExecutor 方法作为驱动类,内部利用 Spring 注入了 ScoringStrategy 实现类列表,得到所有评分算法。定义了 execute 方法,遍历评分算法列表,通过反射获取策略实现类注解上的属性即可筛选出对应的评分算法。
4、你是怎么根据应用来选取要执行的评分算法的?是用 if else 么?
核心实现是注解。
一开始我想的是 if else ,但是根据 开闭原则,后面要添加新的评分算法需要修改 if else 的代码,因此我想到了 Spring 自动注入 + 自定义注解来实现自动选取评分算法。
具体来说,所有评分算法都实现了同一个策略接口,Spring 可以自动注入这些实现类,根据注解上定义的 appType 和 socringStrategy,就可以从这些实现类中筛选出要执行的评分算法。
5、你的平台支持哪些类型的应用?支持哪些评分算法?
平台现在支持得分类应用和测评类两种应用类型。
一共支持 3 种评分算法,分别是:
- 自定义计算测评类策略
- 自定义计算得分类策略
- AI 分析测评类策略
6、如何实现测评类应用的评分算法?请详细讲解。
1)测评类每道题对应的答案都设置了属性,以 MBTI 为例,内向的答案对应 I
属性,外向的答案对应 E
属性。
2)根据用户选择的答案计算出对应属性总数量,例如 I 为 10 个、E 为 2 个、J 为 10 个、P 为 2 个,将其构建成一个属性得分 map,key 为属性值、value 为累计数量。
示例 Map 结构如下:
{
I: 10,
E: 2,
J: 10,
P: 2
}
3)评分结果表的结果也设置了对应的属性,例如 ISTJ 包含了 I、S、T、J 这四个属性,ESTJ 则包含了 E、S、T、J 。对于每种评分结果,遍历属性集合,从上述 Map 结构中获取到属性对应的评分。
比如 ISTJ 评分结果对应的得分为:10 (I) + 0 (S) + 0 (T) + 10 (J) = 20 分
4)遍历所有评分结果,使用上述算法计算得分,最终获取到得分最高的评分结果。
通过定义 Map 的方式,拿空间换时间,减少了算法复杂度,提高了算法的执行效率。
7、你是怎么快速开发项目后端的,如何避免重复编码?
总结来说利用了:项目模板 + MyBatisX 插件 + 自定义代码生成器 + AI。
1)首先我基于后端通用项目模板进行开发(相当于项目的 initializr),该模板内置了很多后端开发常用的能力,如全局异常处理器、通用响应对象包装、整合了数据库和 Redis 等,避免重复工作。
非常熟悉后端通用项目模板的同学,也可以说基于自己开发的模板二次开发。
2)在我设计完库表后,利用 IDEA 的 MybatisX 插件生成对应的实体类、Mapper 等文件。
3)再利用自定义代码生成器,根据实体名称等信息生成了包含增删改查等通用功能的 Controller、Service 等文件。该生成器基于 FreeMarker 模板引擎实现。
4)现在 AI 工具能力已经很强了,在编码的过程中,也使用 AI 插件生成了一些代码,比如测试数据。
8、你是怎么实现 AI 生成题目功能的?
1 ) AI 生成题目基础功能实现:通过编写 Prompt 实现让 AI 返回 JSON 格式的题目内容。并通过系统预设、少样本学习、任务拆解等技巧优化 Prompt 、通过 AI 开放平台界面调试 Prompt, 提高 AI 生成内容的完善度和准确度。
2 )流式优化:基于 AI 生成题目较慢,选用 ChatGLM AI 的流式 API 开通过 SSE 实时推送单道题目给前端,提高用户体验。
3 )具体实现:在 AI 平台没有完整返回单题目对应的字符时,不应该返回给前端。为了拼接单道题目,我基于 RxJava 的操作符链式调用处理 AI 异步数据流,先通过 map 获取并处理字符串(过滤持殊字符如 '\n') 、 filter 过滤空值、 fIatMap 映射字符串为个字符,再通过括号平衡算法准确拼出单道题目,使得逻辑简单清晰。
9、你是如何封装通用 AI 模块的?提供了哪些方法?
1 )基于智谱 SDK 实现 AI 调用大模型的能力
2 )通过创建配置类,顶目启动时自动读取配置文件中的秘钥来初始化 AI 客户端实例
3 )编写 Manager Bean 来调用 AI 客户端,提供了多种对 AI 的请求封装方法,并提供了统一的异常处理能力。
比如随机性低的同步调用 AI 、随机性高的流式调用、简化消息传参的方法等。
10、如何流式调用 AI 并实时获取到 AI 返回的内容?
ChatGLM 的 AI SDK 提供了流式调用的能力,我自主封装了 AI Manager 简化 AI 流式调用的传参,返回值是 Flowable 响应式对象。我利用 RxJava 框架来处理 AI 实时返回的 Stream 流数据,将 AI 返回的字符生成结果拼接成"一道一道"题目,通过 SSE 实时准送给前端。
11、你是如何实现一道一道''流式生成题目的,用到了什么算法?
首先流式调用 AI 接囗,利用 RxJava 的操作符链式调用处理 AI 实时返回的异步数据流。先通过 map 获取并处理字符串(过滤待殊字符如 '\n') 、 filter 过滤空值、 flatMap 映射字符串为单个字符,再通过括号平衡算法准确拼接出单道题目,便得逻辑简单清晰。括号平衡算法的解释因为每道颗目是 JSON 结构,左括号数一定等于右括号数。所以可以遍历当前字符串,用一个计数器统计括号数量出现一次左括号则计数加一,出现一次右括号则计数减一,当计数为零时,说明完整的一道题已返回完毕,此时通过 SSE 返回给前端。
12、什么是 RxJava? 为什么使用 RxJava 来处理流?它有什么优点?
RxJava 是一个基于事件驱动的、利用可观测序列来实现异步编程的类库,是响应式编程在 Java 语言上的实现。
1 )它提供了响应式编程模型,使得代码更具可读性。比如 doOnNext 、 doOnError。
2 )提供了丰富的操作符,简化异步操作事件、处理数据转化等。比如 map 、 filter、 flatMap 。
3 )简化线程管理,可以很容易地在不同线程中进行数据流的处理。比如 0bserveOn 、 subscribeOn。
它的一个核心应用场景就是 UI 场景,像 Android 开发都会用到 RxJava。 UI 场景天然涉及到响应和事件这两点,比如我们在手机 app 上点击某按钮,对应 app 就会弹出某个界面,点击按钮其实就是一个事件,那么弹出界面就是对应的响应。
13、什么是 SSE 技术?它有什么优点和不足?适用于哪些场景?
SSE 服务器发送事件 (Server-Sent Events) 是一种用于从服务器到客户端的单向、实时数据传输技术,基于 HTTP 协议实现。
它有几个重要的特点:
- 单向通信: SSE 只支持服务器向客户端的单向通信,客户端不能向服务器发送数据。
- 文本格式: SSE 使用纯文本格式传输数据,使用 HTTP 响应的 text/event-stream MIME 类型。
- 保持连接: SSE 通过保持一个持久的 HTTP 连接,实现服务器向客户端推送更新,而不需要客户端频繁轮询。
- 自动重连:如果连接中断,浏览器会自动尝试重新连接,确保数据流的连续性。
它的优点:
- 简单易用: SSE 在实现上相对简单,无需复杂的配置和建立连接过程。
- 实时性:可以实现服务器向客户端的实时数据推送,使得客户端能够及时获得更新。
- 标准化:基于标准的 HTTP 协议,因此具有广泛的浏览器支持。
- 无需握手机制:与其他技术(如 WebSocket) 相比, SSE 不需要握手机制,可以降低连接的延迟。
不足:
- 单向通信: SSE 是服务器向客户端的单向通信模式,客户端无法向服务器发送数据。
- 连接维持: SSE 需要保持持续连接,可能会造成服务器资源占用。
适用场景:
- 实时更新:股票价格、体育比赛比分、新闻更新等需要实时推送的应用。
- 日志监控:实时监控服务器日志或应用状态。
- 通知系统:向客户端推送系统通知或消息
- AI 对话:见 AI 模型页面对话。
14、为什么用 SSE 技术将生成的题目实时返回给前端?有没有其他实现方案?
SSE 具有如下优点:
-
简单易用: SSE 在实现上相对简单,无需复杂的配置和建立连接过程。
-
实时性:可以实现服务器向客户端的实时数据推送,使得客户端能够及时获得更新。
-
标准化:基于标准的 HTTP 协议,因此具有广泛的浏览器支持。
-
无需握手机制:与其他技术(如 WebSocket) 相比, SSE 不需要握手机制,可以降低连接的延迟。
其他实现方案:
- 轮询(前端主动去要数据)
前端间隔一定时间就调用后端提供的结果接囗,比如 200ms 一次,后端处理一些结果就累加放置在缓存中。
- WebSocket
全双工协议,前端能实时推送数据给后端(或者从后端缓存拿数据)后端也可以实时推送数据给前端。
15、你是怎么实现 AI 评分功能的?
-
ChatGLM 的 AI SDK 提供了流式调用的能力,我自主封装了 AI Manager 简化 AI 调用的传参和编码。
-
获取需要提供给 AI 的参数列表,包括应用信息、题目信息和用户答案。但是需要注意,题目数量可能会很多,如果将完整的题目结构(包括选顶列表)输入给 AI, 可能会超出最大 token 限制,所以可以进行优化只保留用户选顶对应的答案。示例结构如下:
{
"appName": "MBTI 性格测试",
"appDesc": "测试你的 MBTI 性格",
"question": [
{
"title": "你喜欢和人交流",
"answer": "喜欢"
}
]
} -
利用上述参数来编写 Prompt ,让 AI 返回 JSON 格式的评分结果名称和详细描述。
-
调用 AI 并保存评分结果到数据库中,返回给前端用户即可。
16、使用 AI 的过程中,有没有出现不稳定的情况?如果有,你又是如何处理的?
我遇到了几种不稳定的情况:
- AI 没有按照要求返回 JSON 数据,有时额外返回了内容。我通过优化 Prompt (比如给 AI 示例样本结果)和对 AI 结果进行字符串截取处理,提高了稳定性。
- 由于网络原因导致调用 AI 平台失败。我通过 Guava Retrying 库实现了失败后的自动重试逻辑,且最大重试 3 次。
- AI 调用余额不足。我通过编写定时任务定期查询剩余额度,如果额度不足。会提前通过邮件(或者其他方式)进行告警;并且如果用户便用时余额不足,会给一个友好提示。
第 2 、3种惜况要自主实现,也并不难。
17、你为什么要使用缓存来优化 AI 评分功能?这么做有什么好处?
之前的 AI 评分功能存在 2 个问题:
- AI 调用需要费用,如果用户对同样的题目做出同样的选择理论会得到一样的解答,不要每次都询问 AI, 存在资源的浪费。
- AI 评分的响应时间较长,效率有待提升。
考虑到"减少响应时长" 和 "数据复用" 我选择缓存技术来优化 AI 评分功能。出于项目前期采用单机部署,所以选用了成本相对较低的 Caffeine 本地缓存来缓存用户笞案 Hash 对应的 AI 评分结果,提高评分性能 (10S 到 5ms) 的同时大幅节约成本;开通过 Redisson 分布式锁解决缓存击穿问题。
18、你了解哪些缓存技术?在顶目中又是如何运用缓存的?比如怎么设计缓存?
有哪些缓存技术?
缓存技术在项目中一般可以分为两种:本地缓存和分布式缓存。
- 本地缓存可以便用哈希表 Ehcachev Caffeine 等实现。
- 分布式缓存可以便用 Rediss Memcached 等实现。
项目中如何运用缓存?怎么设计?
对于我的项目,由于不考虑分布式或扩容、且不要求持久化、不用保证多台机器缓存间的一致性,所以选择成本低的本地缓存 Caffeine 来实现。
- 缓存 key 设计
回归到需求"用户对同样的题目做出同样的选择,理论会得到一样的解答,所以可以将应用 id 和用户的答案列表作为 key。但答案列表可能很长,可以利用哈希算法 (md5) 来压缩 key, 节省空间。注意如果是分布式缓存,还需要在 key 开头拼接业务前缀。此处我们可以单独为每个业务创建本地缓存,相互隔离,所以 key 可以简单一些。
- 缓存 value 设计
缓存 AI 回答的结果,为了可读性可以存 JSON 结构,为了压缩空间可以存二进制等其他结构。
- 缓存过期时间设置
假设有 20 道题目,那么不同选择累计总次数一共是 2 的 20 次方, 100 多万。过期时间根据实际业务场景和缓存空间的大小、数据的一致性的要求设置,台适即可,此处设置为 1 天。
- 业务流程
在 AI 回答前,哈希处理用户答题选择,得到摘要,拼接缓存 key。通过摘要查找缓存,若命中则直接返回答题结果。若缓存中未找到,则请求 AI 回答。正确解析 AI 返回的 JSON 后,将其放置在缓存中。
- 缓存相关问题的解决
还需要注意缓存过期后的处理,防止产生缓存击穿、雪崩等问题。我在项目中通过 Redisson 分布式锁解决了缓存击穿。
19、什么是缓存击穿?你如何解决缓存击穿问题?
- 什么是缓存击穿?
缓存击穿是指在高并发的情况下,针对一个不存在于缓存中但是访问量很高的 key, 每次请求都要去数据库查询,导致数据库压力过大,性能下降的情况。当某个热点 key 的缓存过期时,大量请求同时访问这个 key, 由于缓存为空,导数所有请求直接访问数据库,引起数据库压力骤增。
对于本顶目,多个用户同时提交答案交给 AI 测评时,如果缓存过期,那么会有大量同时调用 AI 接囗的并发请求,导致被 AI 服务器限制。
- 如何解决缓存击穿问题?
常见解决缓存击穿思路:
1 .设置热点缓存数据永不过期,并且提前预热。2 .缓存不存在时,通过加锁让操作串行执行,并设置缓存。3 .定时更新热点数据的缓存。
我选择第二种方案--加锁。如果服务部署在多个机器上,就必须要使用分布式锁。分布式锁的实现细节很多,所以我直接使用 Redisson 实现分布式锁。 Redisson 为 Redis 提供了多种数据结构的支持,提供了线程安全的操作,简化了在 Java 中使用 Redis 的复杂度。
20、什么是 Redisson? 你在项目中如何使用 Redisson 实现了分布式锁?
- 什么是 Redisson?
Redisson 是一个基于 Redis 的 Java 调用类库,提供了丰富的分布式对象和服务,其中包括分布式锁、分布式集合、分布式对象等功能。主要为了简化 Java 开发人员在分布式环境中便用 Redis 的复杂性,提供了易于使用的 API 以及各种分布式实现。
- Redisson 如何实现分布式锁?
编写配置类,从配置文件中读取 Redis 配置并初始化 Redisson 客户端 Bean;通过 Redisson 客户端的 getLock("myLock") 方法获取分布式锁;通过 tryLock 方法尝试获得锁,并设置抢锁等待时间(3秒)和超时释放时间( 15 秒);利用 try.. .finally... 保证执行操作结束后,锁会被当前线程释放。
21、什么是分布式锁?使用分布式锁时有些注意事项?
分布式锁是在分布式系统中用于控制对共享资源或临界区的访问的一种锁机制。它可以确保在多个节点或实例上同一时间只有一个进程能够获取锁,,从而保证数据的一致性和避免并发访问下的数据竞争和冲突。
注意事顶如下:
- 需要合理设置超时释放时间,避免造成死锁。
- 业务处理完毕后及时释放锁,防止业务阻塞。
- 必须由当前线程释放锁,不能释放其他线程加的锁,防止业务执行受到影响。
- 合理设置抢锁等待时间,避免长时间无效等待。
- 可以利用看门狗机制实现锁的续期,防止由于业务处理时间大于锁超时释放时间,导数一把锁被多个线程拥有,从而出现错误。
22、什么是分库分表?为什么你要在项目中使用分库分表?
- 什么是分库分表?
分库分表是指将一个数据库按照一定规则分成多个库(分库),或将一个表按照一定规则分成多个表(分表)的数据库架构设计方法。通常在数据量大、访问频繁的业务系统中会采用分库分表的方式来提高数据库的性能、可扩展性、可用性。
- 为什么要在项目中使用分库分表?
如果我们的平台发展迅速,用户量激增,用户提交的答案数也会越来越多。从数据库层面去思考,用户答题记录表可能会比较庞大,查询的性能也会下降。因为数据越多,底层存储的 B+ 树就越高,树越高则查询的次数就越多,那么性能也就越差。因此我选择应用的 appld 作为分片键,运用取模算法,对用户答题记录表进行分表,分为了 2 张表。其实这里也是出于学习的目的,实践了下 Sharding JDBC 框架。
23、你为什么使用 Sharding JDBC 技术实现分库分表?它的大致原理是什么?
因为 Sharding JDBC 是 Apache 旗下的开源顶目,生态好、社区活跃、简单易用、容易上手。
它的大致原理可以用几个字总结:改写 SQL。
比如我们想根据 appld 来将对应的用户答题记录表进行分表。将 appld % 2 等于 0 的应用的所有用户的答题记都划分到 user_answer_0, 等于 1 的应用的所有用户的答题记录都划分到 user_answer_1。按照我们正常的想法处理逻辑就是:
if(appId % 2 == 0){
userAnswer0Service.save(userAnswer);
} else {
userAnswer1Service.save(userAnswer);
}
而用了 Sharding-JDBC 后,我们只要写好配置 Sharding-JDBC 就会根据配置执行我们上面的逻辑,在业务代码上我们就可以透明化分库分表的存在,减少了很多重复逻辑!它会解析 SQL ,根据我们指定的分片键,按照我们设置的分片策略来计算得到对应的路由分片(数据库或者表)最终改写 SQL 后进行 SQL 的执行。
24、你是如何使用 Sharding JDBC 实现分库分表的?具体怎么分表、用了什么分表算法?
Sharding JDBC 开箱即用,只要在配置文件中填写好数据源信息、分片算法、分表依赖的列等,就能自动实现业务透明的分库分表。本项目中,我对数据增长较快的用户答题记录表进行分表,选择应用 appld 作为分表键,采用取模分片算法,将用户答题记表拆分为 2 个表 (user-answer-o 和 user-answer-l ),提高了单表查询性能和可扩展性。(感兴趣的同学可以自己测试一下百万行数据的性能提高比例)
25、什么是幂等设计?你项目的哪个功能使用了幂等设计?
幂等设计在编程场景指的是:使用相同参数多次调用同一接囗,产生的结果或影响和单次调用是一致的。为防止用户多次提交重复答案,基于雪花算法为每次答题分配唯一 id ,开通过数据库主键实现幂等设计,避免了重复的脏数据。
26、有哪些实现幂等设计的方法?你在项目中具体又是怎么实现的?
有 4 种常用的幂等设计方法:
- 利用数据库唯一索引的一致性保证幂等性
- 利用乐观锁在某些场景下也能实现幂等性
- 一些天然的幂等操作比如 delete 操作(因为删除一次和多次都是一样的)
- 利用分布式锁防止并发,通过逻辑判断可以实现幂等性
在项目中为防止用户多次提交重复答案,后端基于雪花算法为每次答题分配唯一 id, 并且在用户进入答题页时返回给前端。用户提交答案时会带该 id ,利用数据库主键可以防止重复 id 数据的插入,如果 id 重复会抛出 Du pIicateKeyException 异常,直接忽略即可。这样就避免了重复的脏数据。
27、什么是雪花算法?为什么它能生成分布式全局唯一 id?
雪花算法 (Snowflake Algorithm) 是一种用来生成分布式系统中全局唯一的 ID 的算法。由 Twitter 开发的,用于满足分布式系统中生成唯一 ID 的需求。
雪花算法生成的唯一 ID 通常是一个 64 位的整数,按照以下结构组成:
- 首位符号位(固定为 0 ):符号位始終为 0 ,保证生成的是正整数。
- 41 位时间戳(毫秒级):表示生成的的时间,可以支持约69年的时间范围。
- 10 位机器标识(分布式部署时的机器 ID) :可以支持 1024 台不同的机器。
- 12 位序列号(同一机器同一毫秒内的自增序列):表示同一台机器同一毫秒内生成的不同 ID 的序列号。
雪花算法原理图如下:
雪花算法能生成分布式全局唯一 ID 的原因:
- 雪花算法允许在同一毫秒内生成多个不同的 ID, 通过序列号的自增保证在高并发情况下生成的 ID 唯一性。
- 通过机器部分的标识符保证了在不同的机器上生成 ID 时不会发生冲突。
- 利用时间戳部分的信息,确保生成的 ID 按时间递增,可以方便地对 ID 进行排序和分析。
28、什么是线程池隔离?你在项目中为什么要使用线程池隔离,有什么好处?
什么是线程池隔离?
线程池隔离是一种通过为不同的任务类型或者不同的资源分配独立的线程池来隔离不同类型的任务或资源的执行的机制。这种机制可以有效地控制各种任务之间的相互影响,提高系统的稳定性和可靠性。
项目中为什么要使用线程池隔离,有什么好处?
如果所有业务操作都使用一个线程池,最大的问题就是相互影响。所以,在一些业务敏感的场景,需要隔离线程池,它有以下几点好处:
- 故障隔离,缩小事故范围。
- 资源隔离,防止业务之间抢占资源。同时支持更精细化地管理资源,比如不重要的场景给小一点的线程池,核心场景配置大线程池。
- 性能优化,一些业务场景的任务是 CPU 密集型,一些是 I/ 0 密集型,不同任务类型要配置不同的线程池。
回到本项目,因为服务器资源有限,对于 AI 批量生成题目功能,我为会员创建了核心线程数更大的隔离线程池,尽量保证普通用户的操作不会影晌到会员使用的体验。
29、你具体怎么实现线程池隔离?怎么设置线程池的参数?
怎么实现线程池隔离?
我针对普通用户和会员用户定制了两个不同的线程池,使用处理响应式数据流时,可以通过 observeOn(scheduler) 方法,根据用户的身份指定使用的线程池。
怎么设置线程池的参数?
普通用户线程池:核心线程数和最大线程数都为 1 ,即单线程,任务队列长度为 1000 ,超过则拒绝提交。这样可以有效限制普通用户调用 AI 的频率。
会员用户线程池:核心线程数为 10 ,最大线程数为 20 ,任务队列长度为 5000 ,超过则拒绝提交。即允许更多会员同时使用 AI 。
我还通过编写单元测试,验证了线程池隔离的有效性。
30、你的项目支持哪些统计分折功能?后端如何查询出要统计分析的数据?
项目支持哪些统计分析功能?
支持 2 种统计分析功能:热门应用排行统计和应用回答分布统计。
作用分别是:
- 热门应用排行统计:在 AI 答题应用平台中,我们可以分析哪个 App 用户使用的最多,后期功能迭代时,可根据统计结果,把热门的应用排在首页的靠前位置,并且添加缓存以提升访问速度。
- 应用回答分布统计:根据用户测评结果的分布情况,可优先对群体大的用户进行定制化开发或广告投放,吸引更多用户。
后端如何查询出要统计分析的数据?
编写 SQL 语句,通过 group by 聚合语法实现对数据的分组统计。在 Java 程序中,通过 MyBatis 的自定义 SQL 注解实现(如 @SeIect) 复杂 SQL 语句的执行。
后端查出数据后,前端可以通过 ECharts 实现可视化。
31、请介绍一下本项目的完整业务流程?
本项目一共有三类用户角色,分别是:应用创建者、应用使用者、管理员。
应用创建者可以创建自定义应用,依次填写应用基本信息(名称描述等)、题目和选项、以及不同选顶对应的评分结果。支持选择得分类和测评类这 2 种应用类型,支持选择自定义评分或 AI 评分这 2 种评分算法。管理者负责审核应用创建者申请的自定义应用,含上下架应用和分析应用等管理功能。此外,管理员也可以针对本站的各类数据进行管理比如用户的回答结果。应用使用者可以在首页搜索和挑选应用进行笞题,提交答案后得到评分结果。注意,应用使用者也可以创建应用,应用创建者也可以参与应用答题,二者的身份不做明确的区分。
32、为什么想做这样一个 AI 回答应用平台?
起初在网上看到 MBTI 性格测试应用还要收费,觉得开发这样一个答题应用并不难,就打算自己做一个。但后面转念一想,既然能开发一个答题应用为什么不能开发多个答题应用呢?毕竟题目结构和答题流程都是一致的,也不要太多的额外开发成本,还能够动态维护题目数据,而不是在前端硬编码。而且现在 AI 越来越流行就想看用 AI 提高创建应用生成题目的效率、且让 AI 给用户的回答评分从而提高回答的丰富度,不用自己去网上翻别人的答案了。
33、为什么要开发 MBTI 性格测试小程序?它和你的答题应用平台有什么联系?
MBTI 小程序只是 AI 答题应用平台的一个实例 Demo ,我做它主要有几个理由:
- 通过这个小项目确定了题目结构、跑通了答题功能的业务流程,为后续开发平台打下了基础
- 网上有很多收费的 MBTI 性格测试,所以我选择做这个主题的小程序,比较实用有趣。
- 这个小程序开发比较简单,我也借此学习了跨端小程序的开发框架和小程序开发流程。
后续还可以优化完善小程序,将前端硬编码题目数据改造为调用平台的后端接囗根据 appld 动态获取题目数据,从而让小程序支持切换多种不同的答题应用。
34、你有使用过 AI 工具来辅助编程么?都是如何帮你提高开发效率的?
必须的,我使用的是国内的免费 AI 编程助手 CodeGeex, 主要便用了下列功能:
·自动生成单元测试
·自动给代码补充注释
·解释分析代码,帮助我理解一些类库的源码
·示例数据生成帮助我快速构造了一些模拟数据
·代码补全,帮我提高了编码效率
·代码生成,帮我生成一些简单的算法
35、在开发过程中,你遇到过比较复杂的技术问题挑战吗?如果有,请谈谈你是如何解决这些问题的?
可以从以上任意一道主观的面试题出发去讲,比如你是怎么实现 AI 生成题目功能并使用 SSE + RxJava 进行优化的?如果准确地根据实时字符流拼接出一道完整的题目并立即返回给前端?如何一步步封装了通用的 AI 模块?发现有多种评分算法 (if else) 时,便用策略模式优化代码;发现用户可能同时提交多个重复回答时,采用幂等设计并通过缓存防止 AI 资源的浪费。