WenQu -- 异步处理
前面我们进行了文件处理的工作我们实现了文档的解析分块上传服务理论上功能已经实现比较完整了接着我们会发现程序中存在的另一个问题在我们进行文件上传过程中我们程序的进程被阻塞了这是因为文件的上传解析分块工作并不能马上完成对于较大的文档我们的处理时间要比原有的更长因此这将会导致我们的体验感不佳我们需要想办法进行处理使它能够在处理文档的同时保证我们的其他操作不受影响这就是我们今天要做的事情 – 异步处理。异步处理的核心概念对于异步处理来说最为核心的概念是发起的任务和任务的结果是解耦的。异步处理的三要素回调函数把处理结果的函数作为参数传进去任务完成后自动调用。Promise / Future返回一个“承诺对象”可以稍后通过.then()或await来获取结果。事件驱动就像浏览器有一个循环不断检查“有没有任务做完了有的话就执行对应的回调。”在 WenQu 中是怎么实现的了解完了基本的概念我们来看看在 WenQu 中我是怎么实现的。首先我们需要搭建异步处理的基本设施AsyncConfig这个配置类可以使我们打开 spring 的异步处理功能ConfigurationEnableAsync// 打开 Spring 的异步开关publicclassAsyncConfig{Bean(docTaskExecutor)publicExecutordocTaskExecutor(){ThreadPoolTaskExecutorexecutornewThreadPoolTaskExecutor();executor.setCorePoolSize(2);// 常驻线程executor.setMaxPoolSize(4);// 最大线程executor.setQueueCapacity(200);// 排队等待的任务数executor.setThreadNamePrefix(doc-parse-);executor.initialize();returnexecutor;}}接着我们来瞧瞧三要素我们先从前端看起functionpollDoc(kbId,docId){if(state.docPollTimers[docId])return;state.docPollTimers[docId]setInterval(async(){try{constdawaitapi(GET,/documents/${docId});if(!d)return;if(state.currentKb?.id!kbId)return;constidxstate.docs.findIndex(xx.iddocId);if(idx0)state.docs[idx]d;if([READY,FAILED].includes(d.status)){clearInterval(state.docPollTimers[docId]);deletestate.docPollTimers[docId];renderDocs();if(state.currentKb)loadKbs();}}catch(err){if(errinstanceofApiError(err.code40400||err.code40401)){clearInterval(state.docPollTimers[docId]);deletestate.docPollTimers[docId];if(state.currentKb?.idkbId)renderDocs();}elseif(errinstanceofApiError(err.code40100||err.code40101)){clearInterval(state.docPollTimers[docId]);deletestate.docPollTimers[docId];}}},5000);}这段代码就是轮询的方法当我们进行文件上传启动这个异步处理时轮询开启每隔 5s 就进行查询看看处理的状态。接着我们继续我们先整体看看/** * 异步处理实现类 */Slf4jServiceRequiredArgsConstructorpublicclassDocumentProcessServiceImplimplementsDocumentProcessService{privatefinalDocumentMapperdocumentMapper;privatefinalDocChunkMapperdocChunkMapper;privatefinalTextExtractorFactoryextractorFactory;privatefinalKnowledgeBaseMapperknowledgeBaseMapper;privatefinalTextChunkersentenceChunker;OverrideAsync(docTaskExecutor)publicvoidprocess(LongdocumentId){// 查文档DocumentdocdocumentMapper.selectEntityById(documentId);if(docnull){log.warn(文档不存在{},documentId);return;}// 状态设置为解析中documentMapper.updateStatus(documentId,DocumentStatus.PARSING,null);log.info([{}]开始处理,doc.getName());try{// 解析StringtextextractorFactory.get(doc.getType()).extract(Paths.get(doc.getFilePath()));if(textnull||text.isBlank()){thrownewIllegalStateException(解析结果为空);}documentMapper.updateStatus(documentId,DocumentStatus.CHUNKING,null);// 查知识库的 chunk_size 和 overlapKnowledgeBasekbknowledgeBaseMapper.selectConfigByIdAndUserId(doc.getKbId(),doc.getUserId());if(kbnull){thrownewIllegalStateException(知识库不存在或无权访问);}// 切分intchunkSizekb.getChunkSize()null?400:kb.getChunkSize();intoverlapkb.getOverlap()null?80:kb.getOverlap();ListStringchunkssentenceChunker.chunk(text,chunkSize,overlap);// 组装成实体并批量入库seq 从 0 开始编号ListDocChunkdocChunksIntStream.range(0,chunks.size()).mapToObj(i-DocChunk.builder().documentId(documentId).kbId(doc.getKbId()).seq(i).content(chunks.get(i)).build()).collect(Collectors.toList());docChunkMapper.batchInsert(docChunks);// 回填块数置成功documentMapper.updateChunkCount(documentId,docChunks.size());documentMapper.updateStatus(documentId,DocumentStatus.READY,null);log.info([{}] 处理完成共 {} 块,doc.getName(),docChunks.size());}catch(Exceptione){// 任何一步失败 → 记 FAILED 错误信息// error_msg 列是 varchar(1000)异常栈太长会截断报错只保留简短摘要Stringmsge.getMessage()null?e.getClass().getSimpleName():e.getMessage();if(msg.length()900){msgmsg.substring(0,900);}log.error([{}] 处理失败,doc.getName(),e);documentMapper.updateStatus(documentId,DocumentStatus.FAILED,msg);}}}在具体方法里我们使用注解Async(docTaskExecutor)来启动异步处理spring 读取到了这个注解后就会拦截这个方法进行后续的异步请求处理。这部分是异步处理的内部方法可以注意到在这个方法内我们是进行了很多的数据库操作的为什么我们没有使用事务去保证数据一致性呢这是因为我们在这里面的操作对数据库操作时其实本质上也进行了一致性校验要是数据出现不一致问题程序就会进行报错而事务这是我们刻意设计的因为异步处理有的操作需要相当长时间使用事务会阻塞其他操作。我们再来看看三要素中的其他俩/** * 文档功能实现类 */ServiceSlf4jRequiredArgsConstructorpublicclassDocumentServiceImplimplementsDocumentService{privatefinalKnowledgeBaseServiceknowledgeBaseService;privatefinalDocumentMapperdocumentMapper;privatefinalTextExtractorFactoryextractorFactory;privatefinalDocumentProcessServicedocumentProcessService;/** * 文件上传根目录 */Value(${wenqu.upload-dir:./uploads})privateStringuploadDir;/** * 文件上传文件落盘MySQL 只存文件路径 */OverrideTransactionalpublicDocumentVOupload(LongkbId,MultipartFilefile,LonguserId)throwsIOException{// 查库是否存在并校验身份knowledgeBaseService.getKnowledgeBase(kbId,userId);// 校验文件if(filenull||file.isEmpty()){thrownewBusinessException(ResultCode.FILE_EMPTY);}StringnameObjects.requireNonNull(file.getOriginalFilename());Stringextname.contains(.)?name.substring(name.lastIndexOf(.)1).toLowerCase():;if(!Set.of(txt,md,doc,docx,pdf,xls,xlsx,ppt,pptx,html,csv,epub).contains(ext)){thrownewBusinessException(ResultCode.UNSUPPORTED_FILE_TYPE);}if(file.getSize()20L*1024*1024){thrownewBusinessException(ResultCode.FILE_TOO_LARGE);}// 先落盘./uploads/{userId}/{kbId}/{时间戳}_{原文件名}PathdirPaths.get(uploadDir,String.valueOf(userId),String.valueOf(kbId));Files.createDirectories(dir);Pathtargetdir.resolve(System.currentTimeMillis()_name).toAbsolutePath();try{file.transferTo(target);// 写库DocumentdocDocument.builder().kbId(kbId).userId(userId).name(name).type(ext).size(file.getSize()).status(DocumentStatus.UPLOADING)// 设置成中间状态.build();documentMapper.insert(doc);// 更新文件路径和状态doc.setFilePath(target.toString());doc.setStatus(DocumentStatus.UPLOADED);documentMapper.updateFilePath(doc);// 触发后台异步处理解析 → 切分 → 入库// 必须在事务提交后触发否则异步线程查不到刚插入的文档TransactionSynchronizationManager.registerSynchronization(newTransactionSynchronization(){OverridepublicvoidafterCommit(){documentProcessService.process(doc.getId());}});// 返回VO对象returnDocumentVO.builder().id(doc.getId()).kbId(kbId).name(name).type(ext).size(doc.getSize()).status(DocumentStatus.UPLOADED).chunkCount(0L).createdAt(System.currentTimeMillis()).build();}catch(Exceptione){// 文件已落盘但 DB 写入失败 → 删除文件避免孤儿文件Files.deleteIfExists(target);throwe;}}/** * 文章列表查询 */OverridepublicPageResultDocumentVOpageQuery(LongkbId,LonguserId,intpage,intpageSize){// 校验知识库存在且属于当前用户knowledgeBaseService.validateKnowledgeBase(kbId,userId);// 开启分页查询PageHelper.startPage(page,pageSize);// 调mapper层查询PageDocumentVOpagesdocumentMapper.query(kbId,page,pageSize);Longtotalpages.getTotal();ListDocumentVOrecordspages.getResult();returnnewPageResult(records,total,page,pageSize);}/** * 文档详情先查文档再校验所属知识库归属 */OverridepublicDocumentVOgetDocument(Longid,LonguserId){DocumentVOdocdocumentMapper.selectById(id);if(docnull){thrownewBusinessException(ResultCode.DOCUMENT_NOT_FOUND);}// 校验所属知识库存在且属于当前用户knowledgeBaseService.validateKnowledgeBase(doc.getKbId(),userId);returndoc;}}这是文档处理的详细代码我们重点来看看下面这段要非常注意的事情是我们在进行异步处理时必须在事务提交后触发否则异步线程会找不到刚插入的文档。// 触发后台异步处理解析 → 切分 → 入库// 必须在事务提交后触发否则异步线程查不到刚插入的文档TransactionSynchronizationManager.registerSynchronization(newTransactionSynchronization(){OverridepublicvoidafterCommit(){documentProcessService.process(doc.getId());}});这里就是我们所说的函数回调但是我们也发现并没有使用到.then()等方法这是因为在我们这个处理中不需要使用到我们就简单使用注解Async去解决了。至此我们的异步处理就解决了我们采用的是较为轻量型的方案对于本项目来说个人学习已经够用当然我们不会止步于此为了更高的并发更安全的线程策略以及处理因为服务器宕机导致任务被截断而产生的“僵尸任务”问题我们后续将重构这部分代码引入更为规范的方法 – 消息队列。当然这并不是我们现阶段要做的事情了。后面我们首先要做的就是向量化。我是 _AgAiN请见证我的学习之路。项目链接WenQu

相关新闻

2026开题季AI工具实测:大模型编假文献、智能体凑内容,垂直学术工具到底强在哪?

2026开题季AI工具实测:大模型编假文献、智能体凑内容,垂直学术工具到底强在哪?

我已经收集了足够的信息,现在来写这篇分享帖。让我梳理一下要点:通用大模型:ChatGPT(GPT-5.5/GPT-4o)、Claude(Opus)、Gemini(3.1 Pro/2.5 Pro)、DeepSeek(R1…

2026/9/2 1:09:16 阅读更多 →
云原生交付复盘怎样转成可复用防线

云原生交付复盘怎样转成可复用防线

云原生交付复盘怎样转成可复用防线复盘的价值在于改变下一次的操作路径。能自动检查的配置做成规则,能稳定执行的恢复动作做成脚本;其余判断保留在运行手册里,写清触发条件和停止条件。 纸面复盘与重复踩坑:为什么文档记录容易缺乏…

2026/9/2 0:19:29 阅读更多 →
云原生交付原型如何补齐稳定性边界

云原生交付原型如何补齐稳定性边界

云原生交付原型如何补齐稳定性边界服务网格从演示走到长期运行,差别不在配置能否生效,而在连接、资源和变更是否可控。先验证流量策略、探针和观测链路,再逐步扩大注入范围,比一次全量切换稳妥。 Demo 陷阱与长连接崩溃&#xff1…

2026/9/2 0:10:22 阅读更多 →

最新新闻

中国城市 PM2.5较高浓度风险识别

中国城市 PM2.5较高浓度风险识别

1 研究背景与分析目标空气质量监测数据通常同时包含颗粒物和多种气态污染物。不同污染物之间既存在共同排放来源,也会受到气象扩散、化学转化和地理位置影响。通过机器学习识别PM2.5较高浓度风险,可以展示环境数据的清洗、长宽表转换、类别不平衡处理和模…

2026/9/2 22:08:03 阅读更多 →
酒吧点餐小程序开发实战:从0到1完整指南

酒吧点餐小程序开发实战:从0到1完整指南

酒吧点餐小程序开发实战:从0到1完整指南 酒吧点餐小程序开发与传统餐饮点餐系统的差异在于:酒吧场景天然包含桌台管理、酒水套餐、骰子互动、赛事投屏、搭子社交等复合需求,而不仅仅是“扫码-下单-支付”的简单链路。本文基于 Spring Boot M…

2026/9/2 22:08:03 阅读更多 →
经典ASP图片专题系统实战:从Access到SQL Server迁移全解析

经典ASP图片专题系统实战:从Access到SQL Server迁移全解析

简介:这是一套齐齐性感美女专题图库ASP版v1.0体验版源码包,面向ASP初学者、网站二次开发者和图片站站长。程序采用ASPAccess架构,图片内容可自动更新,URL采用伪静态设计,有利于搜索引擎收录,能省去频繁手动…

2026/9/2 22:08:03 阅读更多 →
DeepSeek API接入Codex CLI:最小调用与高频报错排查

DeepSeek API接入Codex CLI:最小调用与高频报错排查

DeepSeek API 已经是很多 AI 应用接入大模型时的优先选项之一。它的价值不只是模型效果,更重要的是接口格式兼容 OpenAI,这意味着现有工具链、SDK 和命令行工具都可以低成本切换。实际项目里最常遇到的一类需求,就是把 Codex CLI 这类终端编程…

2026/9/2 22:08:03 阅读更多 →
源码服务端验收全流程:体检、验证与可维护性改造

源码服务端验收全流程:体检、验证与可维护性改造

简介:千年源码服务端是一套经典国产游戏的服务端程序,面向私服运营者、服务端开发学习者,用于搭建千年游戏运行环境并深入理解其核心机制。压缩包以rar格式打包,大小约13.06MB,内部按db、BA、tgs1000、login等目录分散…

2026/9/2 22:08:03 阅读更多 →
世界模型不是终点:从 WAIC/WRC 2026 到 DW0.5,具身智能的闭环之路

世界模型不是终点:从 WAIC/WRC 2026 到 DW0.5,具身智能的闭环之路

0. 简介 最近几个月,“世界模型"这个词热得有些反常。从 NVIDIA 的 DreamGen、DreamZero 掀起第一波浪潮,到 Jim Fan 那句被反复引用的"VLA 已死,WAM(World Action Model)万岁”,再到智元、英伟…

2026/9/2 22:07:03 阅读更多 →

日新闻

QEMU为什么能模拟不同CPU?从ISA、CPU模型到指令翻译讲起

QEMU为什么能模拟不同CPU?从ISA、CPU模型到指令翻译讲起

1. 引言:一个软件为何能“伪装”成不同CPUQEMU 是一款广为人知的开源模拟器,它既能在一台 x86 电脑上运行 ARM 系统,也能在 ARM 开发板上启动 x86 的 Linux 发行版。很多人第一次接触 QEMU 时都会好奇:一个纯软件程序,…

2026/9/2 0:00:30 阅读更多 →
单片机计算机毕设之基于 STM32 或 51 单片机的感知式智能垃圾桶硬件控制系统设计 基于 STM32 或 51 单片机的安全防护型智能垃圾桶装置设计(025005)

单片机计算机毕设之基于 STM32 或 51 单片机的感知式智能垃圾桶硬件控制系统设计 基于 STM32 或 51 单片机的安全防护型智能垃圾桶装置设计(025005)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/9/2 0:00:30 阅读更多 →
单片机计算机毕设之基于 ESP8266 的智能垃圾分类桶 APP 监控系统设计与实现 基于单片机的超声波满溢检测垃圾分类装置设计(025105)

单片机计算机毕设之基于 ESP8266 的智能垃圾分类桶 APP 监控系统设计与实现 基于单片机的超声波满溢检测垃圾分类装置设计(025105)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/9/2 0:00:30 阅读更多 →

周新闻

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析

每年校招季我都会接触不少准备数据库方向笔试的同学,看到最多的状态就是:简历上写着“熟悉 MySQL”“了解索引优化”,一碰到数据库管理工程师的笔试卷,却在索引、事务、锁、备份恢复这些题目上翻车。网易这套 2018 校园招聘数据库…

2026/9/1 19:44:48 阅读更多 →
数字电路时序基石:深入理解建立时间与保持时间

数字电路时序基石:深入理解建立时间与保持时间

1. 这不是“背公式”的事:时间参数到底在约束什么你翻过数字电路教材,一定见过这两个词:建立时间(Setup Time)和保持时间(Hold Time)。它们常被并列写在触发器(Flip-Flop&#xff09…

2026/9/1 18:13:19 阅读更多 →
蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

1. 项目缘起:从赛题到超声波测距机的诞生第八届蓝桥杯单片机设计与开发国赛的题目,我至今记忆犹新。它没有直接给出一个花哨的名字,而是用“超声波测距机”这个朴实无华的功能描述,精准地勾勒出了考核的核心。对于当时备赛的我而言…

2026/9/2 1:01:37 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/2 1:06:24 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/2 1:01:13 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/2 2:01:56 阅读更多 →