# shuati-backend **Repository Path**: xiaosaima/shuati-backend ## Basic Information - **Project Name**: shuati-backend - **Description**: 智能刷题平台后端代码 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2025-07-16 - **Last Updated**: 2025-07-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 项目架构 * 本项目使用Java开发,基于SpringBoot+LangChain4J框架。结合MySQL,Redis,RocketMQ,ES,MongoDB,RAG实现功能扩展和性能提升。 * 在本项目中实现了题目,题库的增删改查和分页查询。管理员可以增加修改删除题目题库关联并且通过AI生成题目题解。用户可以设置自己的用户名片和进行发帖操作,并且用户可以点赞和收藏帖子。用户可以签到,搜索题目,查看题目题解。本项目通过 LangChain4j 集成多模态大模型,结合 MongoDB 实现对话记忆、PgVector 支持向量检索,融合 RAG、Tool Calling 与 MCP 服务,构建多功能模拟面试系统。 * 以下为项目架构图: ![framework](doc/framework.png) # 具体实现 ## 题目业务模块 1. 将题目的标签字段以String存储在数据库中,返回前端时将String转化为List便于展示。 2. **使用BitMap优化用户签到功能**,实现更小的存储占用.通过位运算来定位签到情况,通过牺牲签到数据的可读性换取更优的存储性能。在将签到数据返回前端时,通过位运算将已签到的日期(在某年中的DayOfYear)封装为List返回给前端,可以直接展示。 3. 数据库数据采用**逻辑删除**,提升性能和数据可恢复性。 4. 在题目修改时,**通过Canal监听BinLog和同步ES解耦**。 5. 在题目删除时,通过将Caffeine的value设置为Null,**避免缓存击穿**。 6. 引入DeepSeek大模型实现题目题解的自动生成,并将Status设置为待审核,等待管理员审核通过才可见。编写精简的Prompt降低Token开销。 7. 分页时,使用查询记录返回下一页首ID,将ID作为查询条件**解决深分页问题**。 8. 使用Caffeine进行缓存,并实现缓存预热,并且通过Spring Schedule定时检测和清理Caffeine状态,记录命中率和占用率到日志文件。当命中率低时,**从HotKey同步热点数据**;当占用率高时,首先让Caffeine**自动清理本地缓存**,再次检测占用率,如果占用率依旧高,**手动通过LFU算法**,依据CurrentTotal*0.2**动态清理**冷数据释放空间。实现缓存高可用。 9. 使用Sentinel针对分页查询和单个题目查询进行限流,防治大量数据访问导致系统不可用。编写降级和限流方案,限流返回Null,降级查询缓存数据。提高系统可用性和稳定性。 ### 对于BitMap做签到功能的技术选型 #### BitMap性能 * 在本项目中使用的是为每一个user的每年签到情况分配一个BitMap,因为每一天占用1Bit,全年365天大概占用46字节.100万用户就需要占用43.57MB,极大节省了内存占用。因为使用位运算节约了空间,所以带来了签到日期可读性的损失,也就不能直观的读取签到日期,要进行位运算来获取某天的签到情况。而且因为BitMap的特殊性,也不能直接将BitMap返回给前端,所以在业务层对BitMap进行了另外一层封装,将签到情况转化为List数组。 #### BitMap做签到统计的问题 ##### 代码可读性变差 * 这是最直观的,但是**在可读性和存储性能间做出取舍,可读性可以通过业务层的逻辑解决**。 ##### 稀疏的签到情况会浪费空间 * 在实现的时候也考虑到这个情况,也想过使用Set/keyValue + BitMap动态切换,混合存储。也对Redis Set和Redis键值存储进行了对比 * 相比于Redis Set: Set的内存占用情况:内存空间 = 12字节头部+ 2xN字节。当签到日期超过18天时,占用内存为48字节,超过BitMap的46字节。所以超过18天后,存储性能就不如BitMap。 * 相比于Redis键值存储: Value通过1字节的分隔符+3字节的DayOfYear拼接成字符串,当签到天数小于12天时,占用小于BitMap。但是阈值太小没有使用意义。 * **在动态切换中,要每次签到都要进行存储判断,增加了性能损耗,对于内存节约也提升不大,所以放弃混合存储方案,直接使用BitMap存储签到记录**。 ##### 连续签到天数的计算要遍历 * 如果要对用户的连续签到天数进行统计,需要从当天bit位开始,向前遍历,找到最大连续长度。性能较低,所以对于最大连续存储,我才用额外**使用一个记录最大签到日期的键值对来存储**,在每次签到时,先判断当前bit位的上一位是否为1,是则自增1;不是则设为1。这样就**将最大连续天数统计和BitMap签到位图充分解耦,提高了查询最大连续签到日期的性能**。 ## 模拟面试模块 1. 基于 LangChain4J 框架,结合 `ChatMemoryStore` 与 `ChatMemoryProvider`,实现 **基于 MongoDB 的多轮对话上下文记忆**,确保 AI 在连续对话中保持语义连贯性与上下文理解能力。 2. 将原本的内存向量库升级为 **PgVector 向量数据库**,避免每次启动时重复加载文本向量,同时支持索引加速,实现更高效的 RAG(Retrieval-Augmented Generation)增强检索。 3. 通过 OpenAI SDK 调用 **多模态模型的图像识别能力**,实现基于简历的图文理解与个性化面试优化建议,提升用户沉浸式体验与面试场景真实感。 4. 利用 LangChain4J 的工具调用注解机制,实现多种扩展功能调用,包括 **爬取面试题(来源:面试鸭)、文件操作、终端控制、资源下载与 PDF 生成**,显著拓展 AI 的任务执行能力。 5. 集成智普 AI 开发平台的 **联网搜索(MCP)服务**,支持实时信息查询,增强大模型获取外部最新知识的能力,有效提升问答的时效性与准确性。 6. 采用 **Server-Sent Events(SSE)** 实现推理过程的实时输出,模拟打字机式的回答展示方式,优化用户交互体验并增强系统的响应流畅度。 ### 对于向量存储的技术选型 * 在模拟面试模块初始实现的时候是通过内存向量库来存储向量。在功能完成后,因为基于内存的向量数据库会重启丢失数据并且无法存储大量数据,所以就引入PgVector向量数据库来替换内存向量库。 #### PgVector向量库的优势 1. 支持ACID事务和**持久化存储**。 2. 有很**高的扩展性**,可以搭建分布式向量库。 3. 支持多种检索方式,比如全文检索等。 #### PgVector向量库的劣势 1. 在超大规模向量检索或者高并发检索时会**产生性能瓶颈**,可以通过使用分布式向量数据库解决。 2. 存储和计算资源消耗大。 #### 内存向量库的优势 1. 直接操作内存,可以实现快速响应。 2. 配置简单。 ### 对于对话记忆持久化的技术选型 在实现对话记忆持久化功能的初始阶段,系统采用了 **MySQL 数据库**来存储对话历史。然而在实际使用过程中发现,MySQL 在处理半结构化数据、频繁写入和嵌套数据结构等方面存在一定局限性。因此,为了更灵活高效地存储和管理对话记忆数据,引入了 **MongoDB** 来替代 MySQL 作为对话记忆的存储方案。 #### MongoDB 的优势 1. 灵活的文档结构:支持嵌套的 JSON 文档,天然适用于存储层次丰富的对话内容(如角色、时间戳、上下文等)。 2. 写入效率高:适合高频次的插入操作,如连续的多轮对话记录。 3. 水平扩展能力强:内置分片机制,支持海量数据的分布式存储。 4. 天然支持 TTL 机制:可以轻松配置对话记录过期自动清理,便于缓存式存储。 #### MongoDB 的劣势 1. 不支持事务操作的强一致性(虽然新版支持多文档事务,但性能会受影响)。 2. 内存占用较高:在大规模存储时对服务器配置有较高要求。 3. 复杂查询性能一般:不适合进行大量 JOIN 或复杂聚合逻辑。 #### MySQL 的优势 1. 成熟稳定:作为关系型数据库的代表,社区活跃、生态丰富。 2. 强一致性保证:适用于对数据完整性要求较高的场景。 3. 查询能力强:支持复杂的 SQL 查询、索引机制和多表关联。 #### MySQL 的劣势 1. 数据结构固定:不适合存储结构灵活或嵌套层级较深的对话数据。 2. 写入性能瓶颈:高并发写入场景下容易成为性能瓶颈。 3. 扩展性较差:横向扩展成本较高,不如 MongoDB 灵活。 ## 题库模块 1. 建立题目题库**关联表**,便于通过题目定位题库,使用题库检索题目。避免对于题目表,题库表的全表扫描查询数据,减少了性能损耗。 2. 基于自定义线程池+ComputableFuture+异步实现题目题库关联的批量修改,避免业务阻塞,提高了系统性能。 ## 帖子模块 ### Redis为核心+MQ异步同步方案选型 1. 从MySQL作为操作核心转化为Redis作为操作核心,将MySQL作为一个仅持久化的数据库,**最大程度保证了数据库的安全性和可用性**,减少了数据库因为大量数据请求的崩溃问题。 2. 通过RocketMQ实现数据的异步持久化,通过消息队列保证数据的传输可靠性。 3. 设置**重试机制+死信队列实现消息的重试和补偿**。 4. 通过**状态机和数据库唯一索引**实现消息的幂等性处理。 * 以下为点赞系统流程图: ![thumb_pipline](doc\thumb_pipline.png) #### 对于消息丢失的问题处理 * 因为本点赞模块强依赖于Redis的可靠性,通过Redis的持久化机制可最大程度避免在Redis的信息丢失问题。 * 在消息队列传递消息过程中,通过**最大程度重试机制+死信队列**全力保证数据不丢失。 #### 对于消息顺序性问题的处理 * 本项目的持久化实体(ThumbEvent)中携带了在Redis操作时的**时间信息**(Date),并且封装到ThumbEvent中。 ##### 对于消息传递的顺序性 * 本项目使用了**顺序队列**来处理消息发送,在不出意外的情况下,是不用检查Date属性就可以保证数据顺序持久化到数据库的。 * 当然也会有意外情况,当数据传递失败时。会加入到本地的ConcurrentLinkedQueue中,使用**队列尽量保证重试消息的顺序性**。这时就会有人问了:帅鸽,你重试队列虽然顺序了,但是对于同一批处理的消息你已经不是顺序了,这怎么解决?别急,在持久化时仍然会进行顺序性校验。 ##### 对于消息持久化到数据库的顺序性问题的处理 * 在数据库的Thumb表中,存在updateTime字段,用于记录最新的更新时间。 * 在ThumbEvent持久化到数据库时,会**对比ThumbEvent中的Date字段和要修改数据的updateTime字段**,如果Date在updateTime之后,就表明是最新数据,可以更新;否则,表明该更新数据是脏数据,如果插入会导致数据错误,应该丢弃。这样就充分保证了消息处理的顺序性。虽然会有性能的损失,但是消息的持久化变得更安全可靠。 #### 对于消息重复性问题的处理 * 在ThumbEvent字段中设置了**Status状态值**,当成功发送到消费者后,设置为true,表明消息传递成功。 * 消息的重复性问题也通过**比较Date和updateTime**来处理。 #### 使用Redis为核心和MySQL的原因 ##### 优势 * **高性能**:Redis基于内存操作,读写速度远高于MySQL。 * 可以**削峰填谷**:可以通过MQ将流量平摊到各个时间段,避免MySQL承受高峰流量操作。 * 降低MySQL负载:可以通过结合连接池,降低MySQL的IO压力和锁竞争。 ##### 劣势 * 会导致**数据短暂不一致**,但是会达到最终一致性。 * Redis持久化机制在**极端情况下可能丢失数据**。 * 会导致跨Redis和MySQL的**事务**,需要结合分布式事务方案补偿。 ##### Redis丢失数据解决方案 * 通过启用Redis的**混合持久化机制**,对于两次复制间隔的数据通过生成RDB二进制文件记录,对于复制过程中新增的数据用AOF记录。 * 写入Redis立即发送到MQ,通过重试+补偿保证数据不丢失。 # 运行示例: ![main_page](doc/main_page.png) ![question_bank_page](doc/question_bank_page.png) ![question_page](doc/question_page.png) ![controller_page](doc/controller_page.png) ![chatStreamMulti](doc/chatStreamMulti.png) ![swagger](doc/swagger.png) ![log](doc\log.png)