
简介这份资源是面向Java后端学习者与微服务入门者的在线教育平台项目源码适合希望通过完整案例理解服务拆分、接口设计与分布式治理的开发者参考。项目围绕课程检索、视频学习、在线测评、作业批改与社区互动等教育业务将系统拆分为用户管理、课程资源、测评、作业处理及社区论坛等独立服务并涉及服务注册发现、负载均衡、断路器、链路追踪与统一配置等支撑机制技术实现以Spring Boot与Spring Cloud为主结合Docker容器化部署安全层面涵盖OAuth2与JWT认证授权。资源包共189个文件以128个java源码为核心辅以19个xml配置、9个properties参数文件及31个zbak备份文件整体约156KB结构紧凑便于按模块阅读。目前已有55人学习适合作为课程设计或毕业设计的架构参考帮助读者梳理微服务拆分思路、接口规范与工程组织方式。1. 从单体到微服务在线教育平台为什么必须拆做过在线教育平台的人都知道最开始的版本往往是一个 Spring Boot 单体应用打天下用户模块、课程模块、订单模块、直播模块全塞在一个 war 包里部署一台 4C8G 的机器就能跑。日活几千的时候一切正常直到某天晚上八点直播课开课选课接口突然把整个应用拖垮——线程池被直播推流请求占满连登录都进不去。这不是假设是我自己踩过的坑。基于微服务架构的 Java 在线教育平台设计与实现核心要解决的就是这种「一个模块出事、全站陪葬」的问题。它把用户、课程、订单、直播、题库这些业务边界拆成独立部署的服务每个服务有自己的数据库、自己的进程、自己的扩缩容策略。适合谁看如果你手里有一个正在从单体往微服务迁移的教育类项目或者准备从零搭一套能扛住选课高峰、直播并发的平台这篇笔记里的选型理由、代码骨架和踩坑记录可以直接拿去用。Java 生态在这件事上有天然优势Spring Boot Spring Cloud Alibaba 的组合足够成熟Nacos、Sentinel、Seata 这些组件把服务发现、限流、分布式事务的复杂度压到了可接受的范围。2. 服务拆分与 Spring Cloud Alibaba 骨架搭建2.1 按业务边界拆而不是按技术分层拆新手最容易犯的错是按 controller、service、dao 三层去拆服务结果拆出三个「分布式单体」改一个字段要同时动三个仓库。正确的做法是按业务能力拆用户服务只管认证、授权、个人信息课程服务管课程 CRUD、章节、分类订单服务管下单、支付回调、退款直播服务管房间、推流地址、回放。每个服务对应一个独立的 Maven 模块独立数据库 schema。拆分的判断标准很简单如果两个功能经常在同一个事务里改同一批数据它们就不该拆开。比如「选课」和「扣库存」必须在一个服务里否则就要引入分布式事务成本陡增。我一般会先画一张业务流程图把强一致的操作用框圈起来一个框就是一个服务边界。2.2 用 Spring Initializr 生成多模块骨架下面是我常用的父 POM 结构统一管理 Spring Cloud Alibaba 版本避免各子模块版本打架。!-- 父 pom.xml只保留关键部分 -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.0/version /parent properties java.version17/java.version spring-cloud-alibaba.version2023.0.1.0/spring-cloud-alibaba.version /properties dependencyManagement dependencies dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-alibaba-dependencies/artifactId version${spring-cloud-alibaba.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement modules moduleedu-common/module moduleedu-gateway/module moduleedu-user/module moduleedu-course/module moduleedu-order/module moduleedu-live/module /modules逻辑说明父 POM 只做版本仲裁和模块聚合不写业务代码。edu-common放公共的 DTO、工具类、统一返回体其他服务依赖它。参数上Java 17 是当前 LTSSpring Boot 3.2 要求最低 Java 17别再用 Java 8 硬撑否则 Nacos 客户端会有兼容问题。2.3 Nacos 做注册中心和配置中心的最小配置每个服务的bootstrap.yml里加 Nacos 地址注意 Spring Boot 3.x 需要额外引入spring-cloud-starter-bootstrap才能让 bootstrap 文件生效。spring: application: name: edu-course cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 file-extension: yaml启动 Nacos 用单机模式即可sh startup.sh -m standalone。启动后访问 8848 端口在配置列表里新建edu-course.yaml把数据库连接、Redis 地址这些环境相关配置放进去代码里用RefreshScope注解的 Bean 就能热更新。这一步的坑在于Nacos 默认的命名空间是 public如果你建了新的命名空间记得在discovery和config下都指定namespace否则服务注册上了但配置拉不到。3. 核心业务链路选课、订单与分布式事务3.1 选课接口的限流与库存扣减选课是教育平台最典型的秒杀场景。我的做法是在网关层用 Sentinel 做第一道限流在课程服务里用 Redis 预扣库存最后异步落库。// 课程服务中的选课接口使用 Redis 原子操作预扣库存 PostMapping(/course/select) public Result selectCourse(RequestBody SelectRequest req) { String stockKey course:stock: req.getCourseId(); // DECR 是原子操作返回值小于 0 说明库存不足 Long remain redisTemplate.opsForValue().decrement(stockKey); if (remain null || remain 0) { // 库存回滚避免负数持续累积 redisTemplate.opsForValue().increment(stockKey); return Result.fail(课程已满); } // 发送 MQ 消息异步创建订单记录 rocketMQTemplate.convertAndSend(course-select-topic, req); return Result.success(选课成功); }逻辑说明decrement返回的是扣减后的值如果小于 0 说明超卖立刻increment回滚。这里没有用数据库行锁因为教育平台的选课峰值可能到每秒几千次数据库扛不住。参数上stockKey的过期时间要设成课程结束时间加一天避免 Redis 内存无限增长。MQ 消费端做幂等用userId courseId做唯一索引重复消息直接丢弃。3.2 Seata AT 模式处理订单与支付的分布式事务订单服务创建订单后要调用支付服务两个库不在一个事务里。用 Seata 的 AT 模式对业务代码侵入最小。// 订单服务中发起分布式事务 GlobalTransactional(name create-order, rollbackFor Exception.class) public void createOrder(OrderDTO dto) { // 本地事务写入订单表 orderMapper.insert(dto.toOrder()); // 远程调用支付服务Seata 会自动生成 undo_log payFeignClient.preparePay(dto.getOrderId(), dto.getAmount()); }逻辑说明GlobalTransactional注解开启全局事务Seata 会在每个参与方的数据库里建undo_log表记录数据快照。如果支付服务抛异常Seata 根据 undo_log 回滚订单表的插入。参数上rollbackFor必须写Exception.class默认只回滚RuntimeException受检异常不会触发回滚这是血泪教训。另外每个参与方的数据库都要有undo_log表建表语句在 Seata 的 GitHub 仓库里有别自己手写。3.3 用 MyBatis-Plus 根据实体类生成建表 SQL热词里有人问 MyBatis-Plus 怎么根据 Java 实体类生成建表 SQL这在微服务初期快速搭库时很实用。引入mybatis-plus-generator后写一个单元测试就能输出 DDL。Test public void generateDdl() { // 配置数据源这里用 H2 内存库只为生成 SQL不连真实库 DataSourceConfig config new DataSourceConfig.Builder( jdbc:h2:mem:test, sa, ).build(); // 指定实体类所在包 FastAutoGenerator.create(config) .strategyConfig(builder - builder .entityBuilder().enableLombok() .addInclude(Course, Order)) // 只生成这两个表的 SQL .execute(); }逻辑说明MyBatis-Plus 的代码生成器默认生成 Entity、Mapper、Service开启enableLombok后实体类用Data注解。生成的 SQL 在控制台输出复制到数据库执行即可。注意这个方式生成的字段类型是保守估计比如String一律映射成varchar(255)实际业务里要手动改成长文本或text。4. 网关、鉴权与跨服务调用的避坑记录4.1 网关统一鉴权别在每个服务里重复写 JWT 解析常见做法是在edu-gateway里加一个GlobalFilter解析 JWT 后把userId放到请求头里往下传下游服务直接从 Header 取不再解析 Token。Component public class AuthFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token exchange.getRequest().getHeaders().getFirst(Authorization); if (token null || !jwtUtil.validate(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } // 把用户信息塞进请求头下游服务直接读 Long userId jwtUtil.getUserId(token); ServerHttpRequest mutated exchange.getRequest().mutate() .header(X-User-Id, String.valueOf(userId)).build(); return chain.filter(exchange.mutate().request(mutated).build()); } Override public int getOrder() { return -100; } // 优先级最高 }逻辑说明getOrder返回 -100 保证鉴权在路由转发之前执行。下游服务用RequestHeader(X-User-Id)接收不要再去解析 Token。注意网关本身要放行登录、注册、课程列表这些公开接口用AntPathMatcher做白名单匹配。4.2 避坑微服务落地时最容易翻车的 4 个点现象一服务注册上了但 Feign 调用报 503。原因Nacos 客户端默认注册的是容器内网 IP本地开发时如果开了多个网卡注册的 IP 不可达。 解决在bootstrap.yml里指定spring.cloud.nacos.discovery.ip为本机实际 IP或者用spring.cloud.inetutils.preferred-networks过滤网段。现象二Seata 全局事务不回滚订单数据脏了。原因参与方服务的数据库没有建undo_log表或者undo_log表的字段和 Seata 版本不匹配。 解决每个业务库都执行对应版本的undo_log建表语句版本号在 Seata 依赖的 jar 包里能找到。现象三Sentinel 限流规则配了但不生效。原因规则只存在内存里服务重启就丢或者资源名写的是接口路径但实际资源名是SentinelResource注解的 value。 解决把规则持久化到 Nacos用sentinel-datasource-nacos依赖资源名统一用注解 value别混用。现象四跨服务调用时X-User-Id丢失。原因Feign 默认不会传递自定义请求头需要手动加拦截器。 解决实现RequestInterceptor在apply方法里把当前请求的 Header 复制到 Feign 请求里。Configuration public class FeignConfig implements RequestInterceptor { Override public void apply(RequestTemplate template) { // 从当前线程的请求上下文取 Header ServletRequestAttributes attrs (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attrs ! null) { String userId attrs.getRequest().getHeader(X-User-Id); if (userId ! null) { template.header(X-User-Id, userId); } } } }逻辑说明RequestContextHolder拿到的是当前线程绑定的请求Feign 调用发生在同一线程里所以能取到。注意异步调用或线程池场景下RequestContextHolder会失效需要手动透传。5. 直播模块的异步化与最终一致性验证5.1 直播回放生成用 MQ 解耦转码任务直播服务收到推流结束的回调后不要同步等转码直接发消息给转码服务。// 直播服务推流结束回调 PostMapping(/live/callback/stop) public Result onStreamStop(RequestBody StreamStopEvent event) { // 只做一件事发消息 rocketMQTemplate.convertAndSend(live-transcode-topic, new TranscodeTask(event.getRoomId(), event.getRecordUrl())); return Result.success(); }逻辑说明转码是耗时操作同步等待会占满直播服务的线程。发到 MQ 后由独立的转码服务消费转码完成再回调课程服务更新回放地址。参数上消息要带roomId和原始录制文件地址转码服务根据roomId查课程信息生成不同清晰度的 m3u8 文件。5.2 验证最终一致性对账任务怎么写微服务里订单状态和支付状态可能短暂不一致需要一个定时对账任务兜底。Scheduled(cron 0 0/10 * * * ?) // 每 10 分钟跑一次 public void reconcile() { // 查最近 1 小时内状态为「支付中」的订单 ListOrder pending orderMapper.selectPending(Duration.ofHours(1)); for (Order order : pending) { // 主动查支付服务 PayStatus status payFeignClient.query(order.getOrderId()); if (status PayStatus.SUCCESS) { orderMapper.updateStatus(order.getId(), OrderStatus.PAID); } else if (status PayStatus.FAILED) { orderMapper.updateStatus(order.getId(), OrderStatus.CANCELLED); } } }逻辑说明对账任务只处理「支付中」的中间态订单已支付或已取消的不动。Duration.ofHours(1)限制扫描范围避免全表扫描。参数上cron 表达式按业务容忍度调整教育平台一般 10 分钟一次足够。注意对账任务本身要加分布式锁防止多实例重复执行。5.3 一个具体技巧用 SkyWalking 快速定位跨服务慢调用微服务链路长了之后一个接口慢 2 秒你根本不知道卡在哪个服务。我的习惯是每个服务启动时挂上 SkyWalking Agent在 UI 里看拓扑图和 Trace。# 启动服务时加 JVM 参数指向 SkyWalking OAP 地址 java -javaagent:/opt/skywalking/agent/skywalking-agent.jar \ -Dskywalking.agent.service_nameedu-order \ -Dskywalking.collector.backend_service127.0.0.1:11800 \ -jar edu-order.jar逻辑说明service_name要和 Nacos 里注册的服务名一致否则拓扑图对不上。backend_service指向 OAP 的 gRPC 端口默认 11800。启动后访问 SkyWalking UI 的 8080 端口在「拓扑图」里能看到服务间的调用关系和耗时点进 Trace 能看到每个 Span 的耗时哪个 SQL 慢、哪个 Feign 调用超时一目了然。我自己的习惯是每次上线新服务前先在预发环境跑一轮 SkyWalking确认没有超过 500ms 的 Span 再发生产。这个习惯帮我省掉了至少三次半夜被叫起来查慢接口的麻烦。希望帮到你。本文还有配套的精品资源点击获取