
去年我们团队接到一个需求要在微博上实时监测指定关键词的舆情动态及时发现负面苗头。一开始想买商用舆情系统一打听价格确实不便宜而且数据都在别人平台上很多定制化功能根本实现不了。后来我们决定自己动手基于 SpringBoot Vue SpringCloud 微服务分布式架构配合大数据处理链路搭了一套基于大数据的微博舆情监测分析系统。这篇文章就把当时的完整思路写出来模块怎么拆、微服务怎么搭、数据怎么采、文本怎么分析、前端大屏怎么呈现、最后部署时踩了哪些坑。如果你也在做毕业设计或者公司想自建一套可私有化部署的舆情平台这篇应该能给你省不少时间。1. 落地之前先想清楚舆情系统要拆成哪几个业务模块1.1 舆情监测的本质是“采集—分析—展示”三层很多第一次做舆情系统的人上来就想写爬虫、调模型结果做了半个月还在纠结某一个小功能。我的建议是先把系统看成一个管道从数据进入到数据产出价值一共只有三层。第一层是采集层。负责按关键词、时间范围、用户类型等条件从微博获取公开信息。它解决的是“数据从哪里来”的问题。第二层是分析层。把采集到的原始文本做清洗、分词、情感判断、热点聚类解决的是“数据意味着什么”的问题。第三层是展示层。把分析结果通过列表、图表、大屏、告警推送给最终用户解决的是“数据怎么用”的问题。这三层逻辑上天然解耦所以我们的后端也按这个思路拆成了独立的微服务而不是糊在同一个工程里。1.2 从用户视角倒推功能清单做系统最忌讳闭门造车。我建议你先把自己当成最终用户拿着手机问自己如果我每天要看微博舆情我最需要哪些信息我梳理下来核心功能其实就四个功能需求具体描述后端落点前端落点实时监测按关键词订阅定时采集新增微博采集服务 定时任务关键词管理页情感分析判断每条微博的正面、负面、中性倾向分析服务 情感模型情感分布图表热点聚合将相似微博聚类识别热点事件分析服务 聚类算法热点列表 / 词云负面告警负面内容触发预警通知运营人员规则引擎 消息通知告警中心除了这四块还有一个容易被忽略的点系统管理。用户登录、角色权限、操作日志、数据字典这些基础能力看起来不起眼但决定系统能不能真正交给业务方使用。我们用的是若依微服务版的权限设计思路把用户和权限做成了独立的认证服务后面的服务通过 Token 校验身份清爽很多。1.3 为什么技术栈锁定 SpringBoot Vue SpringCloud这套组合放到今天已经不算新潮但我仍然觉得它是构建这类业务系统最稳的选择。SpringBoot 解决了“快速写业务接口”的问题内嵌 Tomcat、自动配置、起步依赖让团队能把精力放在业务而不是环境配置上。Vue 的开发效率同样很高组件化、响应式、生态完善尤其适合做数据可视化类页面。SpringCloud 的核心价值在于微服务治理——服务注册、配置管理、网关路由、熔断限流这些在单体架构后期会非常头疼的问题SpringCloud 体系里都有成熟方案。当然也要说句公道话如果你们团队只有两三个人数据量也压不到瓶颈单体架构可能更快。但既然标题定位是“微服务分布式”说明你有横向扩容、独立部署、数据分片的潜在需求。从长期维护角度SpringCloud 的投入是值得的。2. 微服务拆分与 SpringCloud 基础设施搭建2.1 按领域拆分而不是按技术层拆分微服务拆分最早期的错误是有人会按“Controller 服务”“Service 服务”“DAO 服务”来拆。这样拆完一次请求要跨三四个服务网络开销巨大事务管理基本失控最后变成分布式灾难。正确做法是按业务领域拆分。我们这个系统最终拆成了五个服务服务名职责关键依赖gateway-service统一入口、路由转发、Token 鉴权Spring Cloud Gatewayauth-service用户认证、权限管理Spring Security JWTcollector-service微博数据采集、定时调度、清洗落库Quartz MySQL Redisanalysis-service分词、情感分析、热点聚类HanLP ESvisual-service查询聚合、看板数据接口MySQL ES Redis服务之间通过 Feign 声明式调用。比如 visual-service 要展示舆情列表它不需要直接连数据库而是通过 Feign 调 analysis-service 的接口拿数据。这样每个服务的数据边界很清晰后期哪个服务压力大就单独给哪个服务加实例互不影响。2.2 Nacos 做注册中心与配置中心服务注册这一块我们直接选了 Nacos。对比 EurekaNacos 的优势是一并解决了注册中心和配置中心两个问题。Eureka 只做服务发现配置管理还得再搭 Spring Cloud Config多一套维护成本。实际接入很简单。服务端的 pom 里引入dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-config/artifactId /dependency然后在 bootstrap.yml 里指定 Nacos 地址和文件后缀spring: application: name: collector-service cloud: nacos: discovery: server-addr: 192.168.1.100:8848 config: server-addr: 192.168.1.100:8848 file-extension: yamlNacos 的配置管理有个好处配置修改后可以动态刷新不需要重启服务。比如我们调整采集频率、修改敏感词库、切换情感模型的阈值在 Nacos 配置中心一改服务自动感知。这让上线后的运营调整变得非常轻量。2.3 Gateway 统一路由与鉴权所有前端请求都先打到 gateway-service再由网关转发到具体服务。网关层面做两件事一是路由转发。根据请求路径的前缀把请求分发到对应服务。二是统一鉴权。我们自定义了一个 GlobalFilter从请求头里取出 Token调用 auth-service 校验。校验通过就把用户信息写进请求头后端各服务不需要再关心“这个用户是谁”只需要信任网关传来的身份信息。Component public class AuthFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token exchange.getRequest().getHeaders().getFirst(Authorization); // 调用 auth-service 校验 token这里省略远程调用细节 if (token null || !authService.verify(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } Override public int getOrder() { return -100; } }提示不要为了省事跳过网关层直接让各服务暴露公网地址。没有统一入口后续加鉴权、加限流、加日志都会变成逐个服务改一遍的痛苦工程。2.4 服务间通信Feign Sentinel 的降级方案服务间调用我们统一用 OpenFeign。Feign 的好处是写起来像调用本地方法接口逻辑清晰再加上内置的负载均衡能力服务多实例部署时自动分发请求。但微服务之间跨网络调用注定会有失败。所以每个 Feign 接口都必须考虑容错。我们接入了 Sentinel配置了线程池隔离和熔断规则。举个例子当 collector-service 频繁调用 analysis-service 而后者响应超时Sentinel 会打开熔断器后续请求快速失败并返回降级数据而不是让线程全部阻塞在等待上。降级返回什么我们返回一条默认的分析结果情感倾向为“中性”、关键词为原文保证前端页面不至于空白。业务方看到的是“该条暂未分析”但系统整体没有宕机这就达到了容错的目的。3. 数据采集与预处理微博数据从哪里来、怎么清洗3.1 采集通道的取舍API 与网页解析微博数据的获取行业常见方案有两个官方开放 API 和网页解析。官方 API 的优点是稳定、合规、数据结构规范但限制比较多能拿到的字段有限且有严格的频率限制。网页解析的优点是数据全、定制性强但需要维护模拟登录、Cookie、反爬策略工作量明显更大。作为技术分享我必须强调合规边界系统只采集公开信息不碰用户私信、不越权访问、严格遵守目标平台的访问频率限制这是底线。我们当时采用的是混合方案。核心数据源走官方搜索类接口按关键词和分页拉取公开微博对部分接口覆盖不到的字段再用可控频率的网页解析做补充。采集模块本身做成独立服务未来如果数据源要扩展成贴吧、新闻站点只需要在 collector-service 里新增一个数据源适配器即可。3.2 关键词订阅与定时任务设计采集引擎的工作方式不是无脑全量爬而是基于“订阅任务”。用户在管理后台配置一个关键词比如“某品牌”系统会生成一个采集任务随后按固定频率增量拉取新增微博。定时调度我们用了 Quartz。每个关键词任务对应一个 Job触发规则用 Cron 表达式控制// 示例每 5 分钟执行一次增量采集 CronTrigger trigger TriggerBuilder.newTrigger() .withIdentity(keyword-job-001, collect-group) .withSchedule(CronScheduleBuilder.cronSchedule(0 */5 * * * ?)) .build();这里有个实用细节高频关键词明星、热点事件和低频关键词冷门行业词的采集频率不能一样。我们对任务做了分级A 类词每 1 分钟一次B 类词每 5 分钟一次C 类词每 30 分钟一次。这样避免了对所有词一视同仁导致的计算资源浪费。3.3 清洗、去重、过滤的完整链路原始数据不能直接进库必须清洗。我们要走四步第一步是去噪。去掉微博文本里的 HTML 标签、多余空白、特殊字符和 URL。表情符保留但转成文本描述比如“[哈哈]”转为“哈哈”。第二步是去重。同一关键词下很容易采到重复微博尤其是热门内容被多次转发。我们在 Redis 里维护一个已见微博 ID 的布隆过滤器判断新数据是否已经处理过。布隆过滤器允许小概率误判但内存占用比存全量 ID 小一个数量级。第三步是过滤。按配置过滤掉广告类账号、营销号、灌水内容。判断依据包括账号等级、历史发帖频率、内容中是否含明显营销词。第四步是数据落地。结构化元数据微博 ID、用户、时间、内容、链接存 MySQL便于事务管理。频繁查询的热点数据放 Redis 缓存。全文检索需求走 Elasticsearch为后面分析和展示层的搜索功能做准备。public class WeiboCleaner { public WeiboDO clean(RawWeibo raw) { WeiboDO result new WeiboDO(); result.setWeiboId(raw.getId()); result.setUserId(raw.getUserId()); result.setContent(Jsoup.clean(raw.getContent(), Safelist.none())); result.setPublishTime(raw.getPublishTime()); result.setEmotionScore(-1); // 留待分析阶段填充 return result; } }注意清洗这一步看似笨重但如果你跳过它直接做分析分词准确率会被噪声文本拖累情感模型的预测结果也会明显失真。数据质量决定分析质量这是大数据项目的铁律。4. 分析引擎分词、情感判别与热点聚合的实现细节4.1 HanLP 分词接入 SpringBoot中文文本不像英文有天然空格分隔所以分词是所有下游分析的第一步。我们选了 HanLP它在工业界的口碑、社区活跃度和开箱即用的程度都更合适。pom 引入dependency groupIdcom.hankcs/groupId artifactIdhanlp/artifactId versionportable-1.8.4/version /dependency调用代码非常简洁ListString words HanLP.newSegment().seg(某品牌发布了新款手机用户反馈充电速度有明显提升) .stream() .map(Term::toString) .collect(Collectors.toList());HanLP 自带中文分词、词性标注、命名实体识别能识别“微博”“充电”“提升”这些词。分词结果里名动词往往是关键词提取的重要原料而形容词、副词的情感色彩又是情感分析的重要特征。实践中我们做了一个小改造把行业自定义词典放进 Nacos 配置服务启动时加载。比如手机行业的“骁龙”“OLED”“快充协议”通用分词器往往拆得不对加上自定义词典后准确率提升明显。4.2 情感分析怎么落地不调大模型也能跑大型语言模型虽然火但部署成本和推理延迟对舆情系统来说往往不划算。更务实的路线是小模型 情感词典 业务规则。我们采用了两层方案。第一层是情感词典打分。把微博分词结果和正向/负向词典比对统计情感得分。第二层是训练一个轻量分类器把词典得分、词向量均值、文本长度等作特征输出正面、中性、负面三类标签。这里我必须提一个很多人踩过的坑直接用通用情感模型跑舆情数据效果往往很差。原因很简单微博语境里有大量口语、反讽、表情符号通用模型没有针对这个领域调过。我们当时从历史标注数据里抽了一部分微博人工打好标签再对模型做了增量训练负面判别的准确率才从六成多提升到八成以上。情感得分不只看单条。我们还在分析层按时间窗口聚合输出“负面指数”曲线。负面指数由负面微博占比、负面情感强度、传播速度三部分加权计算超过阈值就会触发告警。4.3 热点话题聚类从 TF-IDF 到相似度归并舆情系统不能只做单条分析它得告诉用户“现在正在吵什么”。这就依赖热点聚合。我们当前数据量的处理策略是先对清洗后的文本做 TF-IDF 关键词提取再用关键词做文本相似度聚类最后把聚类内文本按时间排序提取出代表微博汇总成热点事件。算法上小数据量用 scikit-learn 的 KMeans 或 DBSCAN 就能跑。但到百万级以上单机算法就开始吃力了。数据量再往上走就需要把分析链路迁到 Spark 上跑分布式计算用 Spark MLlib 做词频统计和聚类。分享一个调参经验相似度阈值不要拍脑袋定先抽几千条数据人工聚类找到“人工认为同一事件”的相似度区间再去定工程阈值。我们反复调整后定的阈值是余弦相似度不低于 0.55超过这个阈值才归并低于则视为不同话题。5. Vue 前端与数据大屏把分析结果直观呈现出来5.1 前端技术选型Vue3 Vite Element Plus ECharts前端我们选了 Vue3 配合 Vite组件库用 Element Plus图表用 ECharts。这套组合在数据管理后台和可视化大屏这两个场景里都算顶配。先提醒一个环境问题Vue3 Vite 对 Node 版本有要求建议直接用 Node 18 以上版本否则安装依赖时会遇到各种版本报错。npm 镜像建议切换成国内镜像源否则下载慢到怀疑人生npm config set registry https://registry.npmmirror.com5.2 核心页面与组件设计前端最有价值的页面是数据大屏。我们设计了三块核心区域。顶部是核心指标区展示监测总数、负面微博数、今日新增、当前热点事件数。中间是趋势区左半边放情感趋势折线图右边放关键词词云。底部是实时列表滚动展示最新采集到的高危微博每条附带情感标签和传播路径摘要。ECharts 的图表配置相对琐碎我建议把图表封装成通用组件。比如情感趋势图组件接收一个 timeSeries 数组内部负责渲染之后哪个页面需要直接引用不需要每个页面重复写 option。template div refchartRef classchart-container / /template script setup import * as echarts from echarts; import { onMounted, ref, watch } from vue; const props defineProps({ seriesData: { type: Array, required: true } }); const chartRef ref(null); let chartInstance null; onMounted(() { chartInstance echarts.init(chartRef.value); renderChart(); }); watch(() props.seriesData, () renderChart()); function renderChart() { chartInstance.setOption({ xAxis: { type: time }, yAxis: { type: value }, series: [{ type: line, data: props.seriesData }] }); } /script5.3 实时推送WebSocket 在看板中的实践舆情数据是持续滚动进来的如果前端靠手动刷新页面体验很差。我们从两种方案里选了 WebSocket。最初想用轮询实现简单但每 5 秒一次请求用户开十个页面就是十倍的无效请求。后来用 Spring Boot 内置的 WebSocket 端点由 analysis-service 在产生新分析结果时主动推送到网关网关再转发给前端。Vue3 端用原生 WebSocket 接收消息然后更新组件内的响应式数据即可。const socket new WebSocket(ws://your-domain/ws/emotion-feed); socket.onmessage (event) { const data JSON.parse(event.data); emotionTrendSeries.value.push({ time: data.timestamp, value: data.negativeIndex }); };注意WebSocket 后端连接是长连接配合网关时要注意网关层的 idle timeout 配置否则空闲连接被断开前端会时不时离线重连表现为“明明在线却收不到推送”。5.4 Vue 打包放进 SpringBoot 的部署技巧项目是前后端分离开发但交付时我们希望打成一套包后端部署人员不用单独配 Nginx。解决方案是Vue 构建后的 dist 目录直接放进 visual-service 的 src/main/resources/static 下由 SpringBoot 托管静态资源。这一步有两个关键配置。第一是前端路由必须用 hash 模式而不是 history 模式否则刷新页面时会出现 404因为 SpringBoot 不认识前端的路由路径。第二是后端接口要统一加 /api 前缀避免和后端静态资源映射冲突。这样打出来的 jar 包自带页面部署只需要一个 Java 环境。6. 部署、性能与踩坑记录6.1 Docker Compose 一键编排服务多起来之后手工启动是不可接受的。我们用了 Docker Compose 做环境编排一套 docker-compose.yml 把 MySQL、Redis、Elasticsearch、Nacos 和各个业务服务全部定义清楚任何一台新服务器上直接 docker compose up -d 就能拉起整套环境。资源分配上有个经验数据Nacos 占 1G 内存ES 默认堆内存 2GMySQL 1G再加五个业务服务整套系统最低至少需要 8G 内存。如果服务器只有 4GES 要调小堆内存并且用 boot 模式部署否则服务启动到一半就可能 OOM。6.2 三个值得记录的坑第一个坑是 Spring Boot 版本太高导致的 Spring Cloud 兼容问题。我们最初用了 Spring Boot 3.x结果和 Spring Cloud 2022 的某些组件存在不兼容启动直接报错。如果你的团队对这套体系不够熟悉建议保守一点用 Spring Boot 2.7.x 搭配 Spring Cloud 2021.0.x资料多、踩坑少项目能跑通比版本新更重要。第二个坑是 Elasticsearch 的堆内存锁定问题。ES 默认根据系统可用内存自动分配但如果在容器里跑容器分配的内存和 JVM 感知的内存不一致会出现内存锁定失败。方案是在启动参数里用 -Xms2g -Xmx2g 固定堆内存并设置 ES_JAVA_OPTS 环境变量避免 JVM 动态伸缩。第三个坑是 Feign 的超时时间设置。默认超时时间很短而分析服务的分词和情感计算接口本身耗时相对偏高经常触发超时重试重试导致重复计算又拖慢服务。后来把 Feign 的读超时时间从默认值调大到 10 秒并关闭了重试机制问题才消失。6.3 从单机到分布式集群的演进思路做完这套系统还有一个工程化问题值得聊聊当数据量继续增长怎么演进当前阶段的做法是MySQL 存元数据Redis 做热点缓存ES 做全文索引与分析。大多数业务场景这种“伪分布式”组合已经能撑住百万级的日采集量。但如果你面对的是亿级数据就需要引入真正的大数据组件。原始采集数据直接入 HDFS离线分析任务用 Hive 和 Spark 定期跑流式数据交给 Flink 实时处理聚合分析结果再写回 MySQL 和 ES。这也就是行业里常见的大数据集群部署策略批流分离、冷热分层、计算存储分离。演进的核心原则是“按需引入”。不要在数据量还小的时候就上一套 Hadoop 全家桶那会让运维成本直接翻倍。先跑通小而美的闭环监控好自己的数据增长曲线到瓶颈了再引入对应组件这样系统的整体性价比最高。我在整个项目的最后有个挺深的体会舆情监测系统的难点从来不在代码本身而在于业务理解和数据质量。代码框架可以靠熟练度快速搭起来但采集通道稳不稳、清洗链路全不全、情感模型对你的业务语料有没有针对性这些才是决定系统好不好用的关键。踩过几次坑之后我更推荐你先跑通最小闭环——采集、分析、展示一天内打通再逐步扩展微服务、优化算法。这样系统每一步演进都有真实数据验证不容易出现“架构很华丽、业务用不起来”的尴尬局面。