在线教育微服务实战:课程/问答/文章三中心拆分与落地
简介:这是一套基于微服务架构的在线教育系统实战项目,面向Java后端与全栈开发者,聚焦教育类SaaS平台的设计与落地,涵盖课程管理、问答社区、内容运营等核心业务场景。资源为ZIP压缩包,共198KB,虽文件总数未提供,但根据技术描述可推知包含SpringCloud多模块微服务源码(如用户服务、课程服务、网关等)、Vue前端工程、Docker容器化配置及SQL初始化脚本等关键内容,各类文件协同支撑前后端分离与分布式部署实践。已有1155人学习下载,具备较强的学习参考价值。读者可直接获取完整可运行的教育平台骨架:含JWT单点登录实现、Redis缓存优化、ActiveMQ异步解耦、阿里云OSS与视频点播集成、ECharts数据看板、POI批量导入导出等企业级功能模块,代码结构清晰,Swagger接口文档完备,适合进阶学习微服务拆分、中间件整合与云服务对接。
1. 这不是又一个“SpringBoot+Vue”空壳模板:它真把课程发布、问答流转、文章归档三块业务用微服务切开了,且每个服务能独立部署、压测、扩缩容
你见过多少标着“在线教育系统”的 GitHub 项目?点开一看,user-service和course-service共用一个application.yml,MySQL 表全建在同一个库,前端路由懒加载写成注释,连 Redis 配置都硬编码在@Value("${redis.host}")里——这种项目跑不起来是常态,跑起来才叫玄学。而这个online_edu不同:它把「课程」、「问答」、「文章」三个高耦合模块,真正拆成三个可独立启动的 SpringBoot 微服务(course-center、qa-center、article-center),每个服务自带完整数据库迁移脚本、独立 Dockerfile、健康检查端点,且前台 Vue 项目通过 Nginx 反向代理明确指向各服务网关地址(非 localhost:8080 硬连)。它不解决“怎么学 SpringCloud”,它解决“上线前最后一周,运营突然要给问答模块加敏感词过滤,你能不能只重启qa-center而不影响课程购买流程”——这才是真实产线里微服务该干的事。适合正在从单体转向微服务、手上有真实课程/题库/内容类业务要落地的 Java 工程师,尤其适合需要快速验证服务拆分边界、接口契约、链路追踪落地方案的团队。
2. 拆解微服务边界:为什么课程、问答、文章必须分三个服务?看它们的数据一致性模型和事务边界
2.1 课程中心:强一致性读写 + 分布式事务兜底
课程模块的核心矛盾在于「上架状态变更」与「库存扣减」必须强一致。比如讲师点击“上架”,系统需同时更新课程表course.status=1、初始化库存表course_stock.init_quantity、并触发通知服务发消息。若用单体事务,@Transactional一把锁住三张表即可;但在微服务下,course-center和stock-center(虽未在标题列出,但源码中存在)是两个独立服务,MySQL 也分库。online_edu的解法是:本地消息表 + 定时补偿。course-center在更新课程状态后,不直接调用stock-center接口,而是往自己库的local_message表插入一条记录(含目标服务名、消息体、状态pending),再由MessageSenderJob每 3 秒扫描一次,成功调用stock-center/api/v1/stock/init后将状态改为success。失败则重试 3 次,第 4 次标记为failed并告警。这避免了 Seata 的复杂配置,也绕开了 RocketMQ 事务消息的依赖,对中小团队更可控。
-- course-center 数据库中的 local_message 表结构(关键字段) CREATE TABLE `local_message` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `target_service` VARCHAR(32) NOT NULL COMMENT '目标服务名,如 stock-center', `endpoint` VARCHAR(128) NOT NULL COMMENT '目标接口路径,如 /api/v1/stock/init', `payload` TEXT NOT NULL COMMENT 'JSON 消息体,含 course_id, init_quantity', `status` ENUM('pending','success','failed') DEFAULT 'pending', `retry_count` TINYINT DEFAULT 0, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );提示:
payload字段存储的是 JSON 字符串,不是二进制。这样设计是为了便于人工排查——当某条消息卡在pending,DBA 直接SELECT payload FROM local_message WHERE id=123就能看到原始参数,无需反序列化解析。这是我在某高校在线考试系统里踩过坑后养成的习惯:所有跨服务消息,宁可多占几 KB 存储,也要保证可读性。
2.2 问答中心:最终一致性 + 事件驱动架构
问答模块的典型场景是:用户提交问题 → 系统分配给讲师 → 讲师回复 → 用户收到站内信。这里没有强事务需求,但要求低延迟和高吞吐。online_edu采用ActiveMQ Topic + 消费者组实现。qa-center在保存问题后,向topic://question.created发送消息;notification-center(独立服务)和teacher-assigner(独立服务)均订阅此 Topic,各自处理通知和分配逻辑。关键点在于:qa-center不关心下游是否消费成功,只确保消息发出;而notification-center自带幂等校验(用question_id + event_type作唯一索引),防止重复推送。这种松耦合让问答模块扩容极简单——加机器只需部署新notification-center实例,ActiveMQ 自动负载均衡。
2.3 文章中心:读多写少 + 多级缓存穿透防护
文章模块是典型的 C端高频读场景(首页推荐、分类列表、详情页),写操作极少(仅后台运营发布)。online_edu的缓存策略是三层:
- CDN 缓存:Nginx 层对
/article/list/*、/article/detail/*设置Cache-Control: public, max-age=3600; - Redis 缓存:
article-center对GET /api/v1/articles?category=tech&page=1的响应做 JSON 序列化缓存,Key 为article:list:tech:1,TTL 30 分钟; - 本地缓存(Caffeine):对热点文章详情(如 ID=1001 的爆款课纲),在
ArticleService内存中缓存 5 分钟,避免频繁 Redis 查询。
最关键是防缓存穿透:当请求article/detail/999999(不存在的文章 ID),article-center不会查 DB,而是先查 Redis,命中null后立即写入article:detail:999999 -> "NULL"(带 2 分钟 TTL),后续请求直接返回空,杜绝打穿 DB。
3. 前后端分离落地细节:Vue 如何安全对接多个 SpringCloud 服务?Nginx 配置是核心
3.1 前端构建产物如何知道后端各服务地址?
很多项目把API_BASE_URL写死在vue.config.js里,导致开发环境用http://localhost:8081,生产环境却要改代码重新打包。online_edu的做法是:构建时注入环境变量,运行时由 Nginx 代理转发。Vue 项目中所有 API 调用均使用相对路径:
// src/api/course.js export function getCourseList(params) { return request({ url: '/api/v1/courses', // 注意:这里是 /api/v1/courses,不是 http://xxx.com/api/v1/courses method: 'get', params }) }而nginx.conf中定义了明确的 upstream 和 location 规则:
# nginx.conf 片段 upstream course_backend { server course-center:8080; # Docker Compose 中 service 名 } upstream qa_backend { server qa-center:8081; } upstream article_backend { server article-center:8082; } server { listen 80; server_name online-edu.com; # 前端静态资源 location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; } # 课程服务代理 location /api/v1/courses { proxy_pass http://course_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 问答服务代理 location /api/v1/questions { proxy_pass http://qa_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 文章服务代理 location /api/v1/articles { proxy_pass http://article_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这样,同一份 Vue 构建产物(dist/文件夹)可部署到任意环境,只需改 Nginx 配置里的upstream地址即可。我曾在一个模拟项目X中,用这套方案实现了开发、测试、预发、生产四套环境共用一套前端包,运维同学再也不用找我要“生产版 dist”。
3.2 登录态如何在多服务间透传?
前后端分离下,登录态管理极易出错。online_edu采用JWT + Cookie HttpOnly方案,且严格限定 Cookie 路径:
- 用户登录成功后,
auth-center(独立认证服务)生成 JWT,写入Set-Cookie: token=xxx; Path=/; HttpOnly; Secure; SameSite=Strict; - 所有后续请求(
/api/v1/courses,/api/v1/questions)自动携带此 Cookie; - 各业务服务(
course-center,qa-center)的 Spring Security 配置中,JwtAuthenticationFilter从 Cookie 读取token,解析并设置SecurityContextHolder; - 关键限制:Cookie 的
Path=/,确保所有/api/*请求都能携带;SameSite=Strict防止 CSRF;HttpOnly阻止 XSS 窃取。
注意:
online_edu没有用 Spring Cloud Gateway 做统一鉴权,因为其 JWT 解析逻辑简单(仅校验签名、过期时间、角色),各服务自行解析反而降低网关瓶颈,也方便单个服务灰度升级。
4. 避坑:部署与联调阶段五个血泪经验,每一条都来自真实翻车现场
4.1 现象:Docker 启动后course-center报Connection refused连不上 MySQL
原因:docker-compose.yml中course-center依赖mysql服务,但 SpringBoot 的spring.datasource.url=jdbc:mysql://mysql:3306/edu_course在容器启动时,MySQL 容器可能尚未完成初始化(特别是首次启动,InnoDB 恢复耗时)。course-center启动失败后退出,Docker 不会自动重试。
解决:在course-center的Dockerfile中,添加启动前健康检查脚本wait-for-mysql.sh,并在ENTRYPOINT中调用:
# Dockerfile 片段 COPY wait-for-mysql.sh /wait-for-mysql.sh RUN chmod +x /wait-for-mysql.sh ENTRYPOINT ["/wait-for-mysql.sh", "mysql:3306", "--", "java", "-jar", "/app.jar"]wait-for-mysql.sh内部用nc -z mysql 3306循环检测,超时 60 秒则报错退出。
4.2 现象:Vue 前端调用/api/v1/courses返回 404,但直连course-center:8080/api/v1/courses正常
原因:Nginx 的location /api/v1/courses配置中,proxy_pass末尾缺少斜杠。正确写法是proxy_pass http://course_backend/;(注意末尾/)。若写成proxy_pass http://course_backend;,Nginx 会将/api/v1/courses完整路径转发给后端,而course-center的 Controller 映射是@RequestMapping("/api/v1"),导致匹配失败。
解决:统一在proxy_pass末尾加/,并确保后端 Controller 的@RequestMapping不重复包含/api/v1。
4.3 现象:Redis 缓存击穿,大量请求打到article-centerDB,CPU 100%
原因:缓存穿透防护只针对article/detail/{id},但首页推荐接口/api/v1/articles/recommend未做任何缓存,且 SQL 查询无索引(ORDER BY created_at DESC LIMIT 10在千万级文章表上全表扫描)。
解决:
- 为推荐接口增加 Redis 缓存,Key 为
article:recommend:page1,TTL 5 分钟; - 在
articles表的created_at字段上建立联合索引:ALTER TABLE articles ADD INDEX idx_created_status (created_at, status);(status=1表示已发布)。
4.4 现象:ActiveMQ 消息积压,question.createdTopic 下游消费延迟高达 10 分钟
原因:notification-center的消费者线程池默认只有 1 个线程(DefaultMessageListenerContainer的concurrentConsumers=1),而teacher-assigner却配置了concurrentConsumers=5,导致通知服务成为瓶颈。
解决:在notification-center的application.yml中显式配置:
spring: activemq: pool: enabled: true max-concurrent-consumers: 5并确保@JmsListener方法内逻辑轻量(如只发短信,不查 DB)。
4.5 现象:ECharts 图表在 IE11 下白屏,控制台报Promise is not defined
原因:Vue CLI 4 默认不 polyfill Promise,而 IE11 原生不支持。online_edu的babel.config.js中@vue/apppreset 未启用useBuiltIns: 'entry'。
解决:修改babel.config.js:
module.exports = { presets: [ ['@vue/app', { useBuiltIns: 'entry' // 关键:启用按需 polyfill }] ] }并在src/main.js顶部添加:
import 'core-js/stable'; import 'regenerator-runtime/runtime';5. Docker Compose 多环境编排实战:一套 YAML 文件搞定开发、测试、生产三套隔离环境
5.1 环境变量分层管理:.env+docker-compose.override.yml
online_edu的docker-compose.yml是基础定义,不包含任何敏感配置。所有可变参数通过环境变量注入,并采用三层覆盖机制:
| 文件 | 作用 | 示例变量 |
|---|---|---|
.env | 全局默认值,Git 提交 | MYSQL_ROOT_PASSWORD=root123,REDIS_PORT=6379 |
docker-compose.override.yml | 开发环境覆盖,.gitignore | SPRING_PROFILES_ACTIVE=dev,LOG_LEVEL=DEBUG |
docker-compose.prod.yml | 生产环境覆盖,不提交 Git | SPRING_PROFILES_ACTIVE=prod,LOG_LEVEL=WARN,JVM_XMS=1g,JVM_XMX=2g |
启动命令即体现差异:
# 开发环境(自动加载 override.yml) docker-compose up -d # 生产环境(显式指定 prod 文件) docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -ddocker-compose.yml中的关键片段(展示变量注入):
version: '3.8' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: edu_course ports: - "${MYSQL_PORT:-3306}:3306" course-center: build: ./course-center environment: SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE} SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/edu_course REDIS_HOST: redis REDIS_PORT: ${REDIS_PORT} depends_on: - mysql - redis5.2 数据库初始化:Flyway 迁移脚本如何与 Docker 容器协同?
online_edu未用schema.sql初始化,而是集成 Flyway。每个服务(course-center,qa-center)的src/main/resources/db/migration下存放 V1__init.sql、V2__add_index.sql 等脚本。关键点在于:Flyway 必须在应用启动前执行,且不能与 MySQL 容器竞争。解决方案是:在course-center的Dockerfile中,将 Flyway CLI 打包进镜像,并在ENTRYPOINT中先执行迁移,再启动应用:
# course-center/Dockerfile 片段 FROM openjdk:11-jre-slim # ... 其他步骤 COPY --from=flyway/flyway:8.5.13 /flyway /flyway COPY src/main/resources/db/migration /flyway/sql/ ENTRYPOINT ["/bin/sh", "-c", "cd /flyway && ./flyway -url=jdbc:mysql://mysql:3306/edu_course -user=root -password=$MYSQL_ROOT_PASSWORD migrate && java -jar /app.jar"]这样,每次docker-compose up,course-center容器启动时,先连 MySQL 执行所有未执行的 Flyway 脚本,再启动 SpringBoot,彻底规避“表不存在”异常。
5.3 日志集中化:ELK 栈如何低成本接入?
online_edu的日志输出格式已统一为 JSON(Logback 配置logback-spring.xml中<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>),但未强制依赖 ELK。它提供两种轻量接入方式:
Filebeat + Logstash(推荐测试环境):
Filebeat 监控各容器的/app/logs/*.log,输出到 Logstash,Logstash 过滤后写入 Elasticsearch。filebeat.yml关键配置:filebeat.inputs: - type: log paths: - /app/logs/*.log fields: service: course-center # 每个服务不同 output.logstash: hosts: ["logstash:5044"]直接写入阿里云 SLS(生产首选):
修改logback-spring.xml,用aliyun-log-logback-appender替换 ConsoleAppender:<appender name="ALIYUN" class="com.aliyun.openservices.log.logback.LoghubAppender"> <endpoint>cn-shanghai-intranet.log.aliyuncs.com</endpoint> <project>online-edu-prod</project> <logstore>java-service-logs</logstore> <accessKeyId>${ALIYUN_ACCESS_KEY_ID}</accessKeyId> <accessKeySecret>${ALIYUN_ACCESS_KEY_SECRET}</accessKeySecret> <topic>course-center</topic> </appender>
从那以后我每次搭建新服务,都强制走一遍 Flyway 迁移 + JSON 日志 + Nginx 代理路径校验这三步。不是为了炫技,而是因为曾经在凌晨两点,为修一个因
proxy_pass少斜杠导致的 404,重启了六次线上容器——那种焦虑感,比写一百行业务代码还刻骨。希望帮到你。
本文还有配套的精品资源,点击获取