在线教育系统微服务架构设计与实践:拆分、事务与网关鉴权全解析

📅 2026/8/27 4:10:53
在线教育系统微服务架构设计与实践:拆分、事务与网关鉴权全解析
简介在业务复杂度与高并发场景驱动下单体架构逐渐难以满足在线教育系统的弹性伸缩与独立交付需求。微服务架构通过服务拆分实现模块解耦结合Spring Cloud Alibaba生态中的Nacos注册配置中心、OpenFeign服务通信与网关统一鉴权构建稳定可控的分布式基础设施。分布式事务是微服务落地的核心难点Seata AT模式保障资金链路强一致消息队列实现最终一致。本文以在线教育业务为实例剖析服务边界划分原则、订单支付状态机、视频点播追踪等关键模块并给出本地开发环境搭建与问题排查的实战经验为开发者提供一套可复用的微服务设计与工程实践参考。 做在线教育系统我第一个踩的坑就是**一上来就拆微服务**。单体架构其实能撑住前几千个用户但课程、订单、用户、视频、题库全揉在一个应用里改一处就牵一发动全身。后来项目确实经历过一次拆了后悔、不拆更后悔的尴尬期我才真正意识到微服务的拆分不是技术炫技而是业务复杂度倒逼出来的必然选择。这篇博文就围绕我实际设计的这套基于微服务的在线教育系统把架构设计思路、技术选型、核心模块实现、分布式事务处理、以及本地搭建和排查问题的全过程完整记录下来。不管你是准备做课程设计、毕业设计还是想了解微服务怎么落地到真实业务场景这套东西应该都能帮你少走不少弯路。1. 项目整体设计与微服务拆分思路1.1 为什么在线教育系统要选微服务架构先说业务画像。在线教育平台的用户量通常不是均匀分布选课季、直播课开课、限时优惠活动这些节点流量能瞬间冲到平时的几十倍。单体应用遇到这种场景扩容只能整包复制浪费资源不说热点模块和非热点模块互相拖累系统很容易整个崩掉。微服务架构天然支持按需伸缩把负载最高的课程服务和订单服务单独扩上几个实例其他服务保持原样资源利用率和稳定性都高得多。再一个原因是业务模块的边界相对清晰。用户、课程、订单、支付、消息、学习记录、视频管理这些模块本来就属于不同的业务域彼此之间的交互方式可以用明确的接口定义。这种天然的业务边界恰恰是微服务拆分最适合的土壤。如果业务本身就纠缠不清拆微服务只会把复杂性翻倍这一点在选型的时候要特别想清楚。最后是团队协作的现实考虑。一个在线教育系统通常会分成前端、后端、内容运营、财务对账等多个角色配合如果代码全堆在同一个工程里合并冲突、发布互相影响都是日常。拆成独立的微服务之后每个服务由小团队甚至单人独立维护、独立发布版本节奏灵活很多。这是我实际开发中感受最深的一点微服务解决的往往不是技术问题而是协作和交付效率问题。1.2 核心微服务模块划分与职责边界我设计的在线教育系统拆成了7个核心微服务外加一个网关和若干基础设施组件。每个服务独立部署、独立数据库通过OpenFeign进行同步调用通过消息队列进行异步解耦。服务名称核心职责独立数据存储gatewayservice统一入口、路由转发、JWT鉴权、限流无纯转发层user-service用户注册登录、学生/教师/管理员角色管理、个人信息用户库、角色权限库course-service课程创建、章节管理、课程上下架、课程搜索课程库、章节库order-service购物车、订单创建、订单状态流转、订单查询订单库pay-service对接第三方支付、支付回调处理、退款支付流水库learning-service学习进度、视频断点记录、收藏、笔记学习记录库message-service邮件/短信通知、站内信、系统公告消息记录库这里要强调一个原则服务边界不等于业务动作的简单切分。比如下订单涉及order-service创建订单、pay-service发起支付、course-service锁定课程名额这个完整的业务动作需要跨服务协作但不意味着订单数据要复制到每个服务里。数据属于哪个服务就只存在哪个服务其他服务通过接口获取数据。关于边界的粒度我见过有人把上传视频单独拆一个服务把评论和点赞也各拆一个——这种拆法用不了多久就会被服务间调用搞疯。合并优先拆分次之判断标准就是如果两个模块的变动频率和业务方向高度一致就合并成一个服务。比如视频上传和转码虽然技术上复杂但业务上始终跟着课程走放在course-service里或者作为media-service独立存在都是合理的选择我最终选择了独立成media-service的做法后面细说。1.3 服务拆分中的陷阱与应对策略拆分容易拆对难。我在这部分整理了几个踩过的坑和反复调整后的原则。第一个坑是跨服务事务的一刀切。初期我把创建订单后扣减库存做成了同步调用order-service先落单再调course-service的接口扣库存如果第二个调用失败了整个流程就需要回滚。跨服务的分布式事务比单库事务复杂得多处理不好就是数据不一致的噩梦。后来我改为基于消息队列的可靠异步方案订单创建成功后发送消息course-service消费消息扣减库存通过消息确认和重试机制保证最终一致。关于这部分第四章会详细展开。第二个坑是数据库跟着服务的边界切得太碎。比如学习进度表记录用户看到了视频的哪一秒它既依赖用户ID又依赖课程ID和章节ID。拆库的时候我一度纠结要不要独立成库后来想明白了学习进度只被learning-service读写其他服务最多通过接口查询汇总数据独立成库是合理的。但如果把用户收藏课程表也单独拆库就过度设计了。判断标准很简单这张表有没有被多个服务同时直接读写如果没有就跟着主服务走。第三个坑是接口版本的兼容管理。服务A升级了接口服务B还在用旧版本一上线就报错。后来我强制要求所有对外接口都带版本号/api/v1/course/list并且新版本上线后保留旧版本至少一个迭代周期给调用方留足升级时间。这套机制在高频变动的业务场景下救过我很多次。2. 核心技术选型与基础设施搭建2.1 技术栈全景与选型理由在技术选型上我走的路线是Spring Cloud Alibaba全家桶。原因很实际Nacos既做注册中心又做配置中心相比EurekaSpring Cloud Config的组合少维护一套组件Sentinel直接支持网关和各微服务的熔断限流Seata对分布式事务的支持成熟而且这套技术栈在中文社区的资料极其丰富踩坑后能搜到的解决方案多对项目开发效率影响很大。完整的技术栈如下组件版本选型在系统中的用途JDK17长期支持版基础运行时Spring Boot2.7.x微服务基础框架Spring Cloud Alibaba2021.0.5.0微服务生态整合Nacos2.2.3注册中心 配置中心Spring Cloud Gateway3.1.xAPI网关统一入口OpenFeign3.1.x服务间声明式HTTP调用Sentinel1.8.6熔断降级与限流Seata1.7.0分布式事务AT模式MyBatis-Plus3.5.3ORM框架Redis6.x缓存、分布式Session、验证码存储RabbitMQ3.12.x异步消息解耦MinIO8.5.x视频文件存储Elasticsearch8.10.x课程搜索这里特别提一下JDK版本的选择。现在网上还有大量教程停留在JDK8但新项目直接用JDK17特别是Spring Boot 2.7对JDK17的支持已经非常稳定Lambda表达式、Stream API这些写起来清爽得多未来升级Spring Boot 3.x的迁移成本也更低。如果是从零搭环境没必要再抱着JDK8不放。2.2 Nacos注册中心与配置中心的使用细节Nacos在系统里承担了双重角色。先说注册中心所有微服务启动后把自己注册到Nacos上网关知道每个服务有哪些实例才能做负载均衡转发。服务实例的注册信息包括IP、端口、服务名、元数据如版本号、环境标识这些信息会在服务间调用时被OpenFeign动态获取。我当时在Nacos里规划了三个命名空间dev本地开发、test联调测试、prod生产环境。每个微服务的配置都放在对应的命名空间下通过spring.cloud.nacos.config.namespace指定。这样做的核心价值是环境隔离本地开发时连接的是dev环境的配置和注册中心绝对不会影响生产。下面展示一个典型的bootstrap.yml配置spring: application: name: course-service cloud: nacos: server-addr: 127.0.0.1:8848 username: nacos password: nacos discovery: namespace: dev group: DEFAULT_GROUP config: namespace: dev group: DEFAULT_GROUP file-extension: yaml shared-configs: ->spring: cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path/api/user/** filters: - StripPrefix1 - id: course-service uri: lb://course-service predicates: - Path/api/course/** filters: - StripPrefix1 - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix1 - id: pay-service uri: lb://pay-service predicates: - Path/api/pay/** filters: - StripPrefix1 - id: learning-service uri: lb://learning-service predicates: - Path/api/learning/** filters: - StripPrefix1 globalcors: cors-configurations: [/**]: allowed-origins: http://localhost:8081 allowed-methods: * allowed-headers: * allow-credentials: true路由配置看起来简单但有几个细节值得注意。lb://前缀表示通过Nacos的服务发现拿到实例列表然后由Ribbon负载均衡策略选择一个实例转发这是网关和服务注册中心联动的关键。StripPrefix1的作用是去掉/api/user这类前缀把请求转给user-service时只保留/xxx的真实路径如果后端接口设计了不同的前缀规则这里要灵活调整。请求转发到user-service之后如果它返回了401或403网关需要把响应头里的token刷新信息带回给前端。我是通过全局过滤器统一设置响应头来实现的这样前端axios拦截器就能统一处理token刷新逻辑不用每个服务自己处理。2.4 服务间通信与OpenFeign踩坑服务间通信我用的是OpenFeign声明式HTTP客户端写起来和调用本地方法一样方便。这是我的一个Feign接口示例FeignClient(name course-service, path /api/course, fallback CourseClientFallback.class) public interface CourseClient { GetMapping(/inner/detail/{courseId}) CourseDetailDTO getCourseDetail(PathVariable(courseId) Long courseId); PostMapping(/inner/stock/deduct) Boolean deductStock(RequestBody StockDeductRequest request); }这里有个关键设计思路所有Feign接口的路径统一使用/inner/前缀配合网关的路径匹配规则让/inner/开头的请求在网关层面被拦截只允许内部服务间调用禁止外部直接访问。这个设计相当于给内部接口加了一道防火墙防止敏感接口暴露在公网。OpenFeign踩过的坑主要集中在三个地方第一个是超时时间。OpenFeign默认的readTimeout是60s但在实际调用中比如课程详情接口内部还要查Redis缓存、查数据库、组装返回结果碰上第一次查询没有缓存的情况耗时可能超过默认超时导致调用失败。我统一配置了5秒连接超时和10秒读取超时并且根据实际接口耗时动态调整。第二个是异常处理。Feign调用失败时默认抛出FeignException如果不做处理会直接透传到上层。我通过实现FallbackFactory为每个Feign接口编写降级逻辑返回一个兜底空对象或默认值避免微服务异常波及调用链。第三个是请求头传递。用户登录信息通过JWT Token放在请求头里当user-service调用course-service获取课程信息时需要把当前登录用户的ID传给对方。我在Feign的RequestInterceptor里自动把透传的header从上游请求复制到下游请求这样下游服务也能拿到用户上下文。这个机制不处理好排查权限问题会非常痛苦。2.5 统一鉴权机制网关JWT的完整链路在线教育系统的权限角色有学生、教师、管理员三种不同角色访问的接口范围不同。我采用的方案是JWT Token 网关统一鉴权 服务内二次鉴权三层配合。用户登录成功后user-service生成一个JWT Token里面包含userId、角色、过期时间等信息Token返回给前端前端存储后每次请求都放在Authorization头里。网关的全局过滤器会拦截所有请求先校验Token是否合法签名、有效期然后解析出userId和角色通过X-User-Id和X-User-Role请求头传递给下游服务。这样设计的核心价值在于鉴权的校验集中在网关层做一层服务内部不需要关心Token怎么解析只需要信任网关传递的请求头。服务内部再根据当前用户的角色做细粒度的权限控制比如教师只能编辑自己的课程管理员可以操作所有数据。Token的过期策略也值得细说我设置了短期Token2小时和长期RefreshToken7天。当短期Token快过期时前端可以拿RefreshToken调用刷新接口后端校验RefreshToken有效后重新签发短期Token。这个机制比单Token方案更安全也避免了用户频繁重新登录的糟糕体验。3. 核心业务功能实现要点3.1 课程服务从表结构设计到课程搜索course-service是整个系统的内容核心负责课程和章节的管理。课程表设计我并没有做得很复杂核心表包括课程表course、章节表chapter、课程分类表category。课程表的关键字段包括课程标题、讲师ID、简介、封面图URL、价格免费课程价格为0、上下架状态、总时长、学习人数等。课程搜索这个功能初期我用的是MySQL的LIKE %keyword%数据量几千条没问题但课程数上去之后这种模糊查询的性能和体验都不行。后来我引入了Elasticsearch通过消息队列监听MySQL的binlog变更增量同步课程数据到ES索引中搜索接口直接查ES。这里我用了RabbitMQ做异步解耦课程上新或者信息修改时course-service发送一条消息索引同步服务消费消息后更新ES。关于课程上下架状态我设计了status字段0表示草稿、1表示已上架、2表示已下架。只有已上架的课程才会进入ES索引并在用户端可见。涉及价格变更或库存调整的操作必须走专门的后台接口配合操作日志和审核流程避免运营误操作。3.2 订单与支付流程状态机设计与回调安全订单模块是典型的高并发写 强一致性要求场景。我设计的订单状态机包含待支付 → 已支付 → 已发货这里即课程已开通→ 已完成另外还有已取消和退款中/已退款两个分支状态。订单状态机的核心思路是状态只能按箭头方向流转不能跳级每个流转都对应一个明确业务动作比如待支付→已支付必须由支付成功回调触发不能由前端手工改状态。支付流程是这么走的用户在前端点击立即支付前端调order-service创建订单订单状态为待支付前端跳转到支付页面调pay-service发起支付pay-service生成第三方支付平台的支付链接用户在第三方支付平台完成付款支付平台回调pay-service的支付结果接口pay-service校验回调签名、校验订单金额和商户订单号确认无误后更新本地支付流水状态pay-service通过消息队列发送支付成功消息order-service消费后更新订单状态为已支付同时给learning-service发消息开通课程访问权限用户端通过轮询接口实时获取支付结果这里最关键的是支付回调的安全校验。第三方支付平台的回调接口是公网可访问的必须严格校验签名、金额和订单号防止伪造回调。我踩过一个坑回调接口没有做幂等处理支付平台重试回调时把同一个订单的支付流水重复插了两条导致订单数据错乱。后来增加了数据库唯一索引merchant_order_no transaction_id才彻底解决。3.3 视频点播与学习进度追踪在线教育系统的视频模块我独立成了media-service。视频文件上传到MinIO也可以用阿里云OSS或腾讯云COS返回一个objectId存到章节表。播放时前端拿到视频URL通过media-service签发一个带时效的临时访问凭证比如有效期10分钟然后重定向到MinIO的预签名URL播放。这个方案的优点是视频文件不经过我们的应用服务器带宽压力全在对象存储上应用服务器只负责签发凭证和记录日志。学习进度追踪是个看似简单实则细节很多的功能。用户暂停/结束后前端定时上报当前播放进度精确到秒learning-service把这秒数存下来。用户下次进入课程前端调接口获取上次进度直接跳转到对应时间点这就是断点续播。为了减轻数据库压力进度上报我先写Rediskey为user:course:chapter:{userId}:{chapterId}每隔一段时间批量刷新到MySQL保证崩溃时最多丢失几秒的进度数据。关于视频防盗链除了临时URL凭证外在MinIO层面还可以做Referer白名单限制。不过最有效的还是短期凭证机制URL有效期一过自动失效即使被人抓包拿到URL也看不了。3.4 学习服务笔记、收藏与学习统计learning-service除了记录学习进度还负责笔记和收藏。笔记功能我认为特别容易踩过度设计的坑最开始我想把笔记做成富文本编辑支持图片插入、Markdown渲染后来评估发现MVP阶段只需要纯文本时间戳就够用了。笔记关联到具体章节用户可以按课程维度看自己写过的所有笔记。学习统计这一块我用定时任务基于Spring的Scheduled统计每天的学习人数、学习时长和完课率数据写入统计表每天凌晨生成一张报表数据。报表数据用于运营侧分析课程质量。这里有一个优化点统计逻辑不能直接查业务表而是通过消息队列接收学习行为事件异步落库避免统计任务拖垮在线业务。4. 数据一致性与分布式事务处理4.1 拆分后遇到的数据一致性问题微服务拆分后最头疼的问题就是数据一致性。举一个实际遇到的问题用户下单购买课程order-service创建了订单pay-service完成了扣款但course-service因为网络抖动没有成功执行开通课程权限的逻辑。如果没有任何补偿机制用户花了钱却看不了课这就是典型的数据不一致。我在这套系统中主要面临三类一致性问题支付成功后的订单状态更新这是最需要强一致的环节涉及钱绝不能出现已扣款但订单没更新或订单已支付但金额不对订单支付后开通课程权限可以接受短暂延迟但最终必须完成课程库存的扣减需要防止超卖允许一定的并发控制针对不同级别的问题我选择了不同的解决思路核心原则是非核心链路的强一致可以放宽核心资金链路必须强一致。这个取舍非常重要在实际开发中不需要面面俱到地All in Seata。4.2 Seata AT模式在订单支付场景的落地对于最关键的下单→扣款→开通课程链路我引入了Seata的AT模式。AT模式是侵入性最小的分布式事务方案业务代码只需要加GlobalTransactional注解Seata框架自动拦截SQL在事务协调者TC的协调下完成分支事务的提交或回滚。我的核心代码逻辑是这样的GlobalTransactional(name create-order-tx, rollbackFor Exception.class) public void createOrderWithPay(OrderCreateRequest request) { // 1. 创建订单order-service本地事务 orderService.createOrder(request); // 2. 远程调用库存服务扣减库存course-service本地事务 courseClient.deductStock(request.getCourseId()); // 3. 远程调用支付服务发起支付pay-service本地事务 payClient.initiatePayment(request.getOrderNo()); }当其中任何一个步骤抛出异常Seata会反向执行补偿SQL把已提交的本地事务回滚保证所有服务的状态恢复到操作前的状态。但我要说的是AT模式不是万能的它对性能有一定损耗因为每个事务都要和TC交互记录undo_log高并发场景下反而会成为瓶颈。我的经验是只在核心链路使用Seata其他场景能异步就异步。比如课程开通后续的通知推送完全没有必要参与分布式事务。4.3 基于消息队列的最终一致性方案对于支付成功后的通知、学习进度更新、课程数据同步到ES这类非核心链路我采用的是本地消息表 RabbitMQ的最终一致性方案。核心流程是执行本地业务操作更新订单状态为已支付同时插入一条待发送的消息记录到本地消息表这两个操作在同一个本地事务里要么都成功要么都失败启动一个定时任务扫描本地消息表中待发送状态的消息发送到RabbitMQ消息发出后更新消息状态为已发送消费者比如learning-service收到消息后执行自己的业务逻辑执行成功后手动ack如果消费者执行失败消息进入死信队列由死信监听器重新投递或人工介入处理这套方案的关键价值在于消息一定不会丢即使某个服务挂了重启后定时任务会把积压的消息重新发送。付出的代价是增加了一张消息表和相应的重试逻辑但对在线教育这种对实时性要求没那么苛刻的场景完全够用。开发过程中我把Seata和MQ最终一致性配合使用的经验建议记住这句话能用异步解决的别同步阻塞能用最终一致性解决的别强一致。这是微服务事务设计里性价比最高的原则。5. 本地开发环境搭建与问题排查实战5.1 从零搭建本地开发环境的完整步骤我现在把这套系统的本地搭建过程完整走一遍跟着操作能少踩很多环境问题的坑。第一步是环境准备。我使用的版本是JDK 17Temurin发行版、Maven 3.8配置阿里云镜像加速依赖下载、IDEA 2023.1、Docker Desktop用于跑MySQL、Redis、RabbitMQ、Nacos中间件。特别提醒一下Nacos 2.x直接跑在Docker里最省事不用本地装Java再启动脚本Docker一条命令就能把单机版Nacos跑起来docker run -d \ --name nacos-server \ -p 8848:8848 \ -p 9848:9848 \ -e MODEstandalone \ -e SPRING_DATASOURCE_PLATFORMmysql \ -e MYSQL_SERVICE_HOST127.0.0.1 \ -e MYSQL_SERVICE_DB_NAMEnacos_config \ -e MYSQL_SERVICE_PORT3306 \ -e MYSQL_SERVICE_USERroot \ -e MYSQL_SERVICE_PASSWORDyour_password \ -e MYSQL_SERVICE_DB_PARAMcharacterEncodingutf8\connectTimeout1000\socketTimeout3000\autoReconnecttrue\useSSLfalse \ nacos/nacos-server:v2.2.3启动Nacos后浏览器访问http://localhost:8848/nacos默认账号密码是nacos/nacos。第二步是创建父工程。我习惯先建一个Maven父工程通过dependencyManagement统一管理所有依赖版本避免各个子模块版本冲突dependencyManagement dependencies dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-alibaba-dependencies/artifactId version2021.0.5.0/version typepom/type scopeimport/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version2.7.18/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement父工程统一管理依赖版本各子模块只需要引入具体的spring-boot-starter-web、nacos-discovery等依赖不用写版本号。这是Java多模块工程的基本规范也是避免版本冲突最有效的手段。第三步是依次创建各个微服务模块的Spring Boot启动类每个服务在启动类上打上SpringBootApplication注解服务类上打上EnableDiscoveryClient。注意在Spring Cloud Alibaba 2021版本中客户端通过引入spring-cloud-starter-alibaba-nacos-discovery后就已经自动开启服务注册了EnableDiscoveryClient其实可加可不加但加上更保险方便后续排查。第四步是数据库初始化。每个服务一个独立数据库我在线上环境用自动化脚本创建本地开发则手动建库建表。建议把SQL脚本单独维护在sql/目录下按服务区分。用MyBatis-Plus的自动代码生成器生成实体类、Mapper接口和Service比手写快得多也很适合快速开发。第五步是启动顺序。必须先启动Nacos再启动网关和各微服务。如果服务启动时报nacos registry register failed大概率是Nacos没起好。各服务的启动顺序没严格要求因为服务注册是主动行为但建议先启动不依赖其他服务的服务比如user-service再启动网关照例。第六步是验证环境。启动网关后直接访问http://localhost:8080/api/course/list看能否通过网关路由到course-service。如果返回404而不是网关超时说明路由配置生效了只是接口路径没匹配上这时候需要检查网关路由配置和接口的实际路径。5.2 服务注册不上、Feign调用失败的排查套路本地开发中最常见的问题我分门别类整理一下。问题一服务注册不到Nacos或注册了但下线了排查步骤先看Nacos控制台的服务列表里有没有对应服务如果服务列表有但实例显示不健康很可能是心跳超时检查服务所在机器的防火墙是否放行了Nacos的9848端口Nacos 2.x的gRPC端口如果是Spark本地启动检查spring.cloud.nacos.discovery.ip配置是否被设置为乱七八糟的IP。问题二OpenFeign调用报Connection refused或UnknownHostException核心原因是服务间无法正常进行服务发现。先确认被调用的服务确实注册到了Nacos然后确认Feign接口上的name属性与目标服务在Nacos注册的服务名完全一致。这里经常有人把服务名的-和大小写搞混导致服务解析不到IP。还有一种情况是多实例环境下某个实例宕机了但还没被Nacos摘除Feign轮询会随机选到宕机实例这个问题可以通过Feign的maxAttempts重试机制缓解但要设置合理重试次数避免接口拖慢。问题三通过网关访问接口出现404先确认路径匹配规则。网关配置的Path/api/course/**配合StripPrefix1如果course-service的接口真实路径是/course/list那么外部访问/api/course/list时网关会转发成/course/list此时course-service的Controller映射必须匹配/course/list。如果Controller映射的是/api/course/list就会404。这是我初学网关时最容易踩的坑本质上是路径前缀没算明白。问题四数据库连接报Public Key Retrieval is not allowed这是MySQL 8版本常见的连接串问题JDBC连接串加上allowPublicKeyRetrievaltrueuseSSLfalse即可解决。问题五配置动态刷新不生效Nacos配置中心加载的配置要动态生效需要在对应类上标注RefreshScope。动态数据源、动态线程池这些组件尤其依赖这个注解。如果遇到改了配置没效果先确认是不是漏了这个注解。5.3 性能优化与监控体系系统跑起来之后我陆续做了一轮性能优化。最明显的瓶颈是课程详情接口每次展示课程信息都要查课程表、查讲师信息、查章节列表、查学习人数统计串行调用接口耗时最高到600ms。我的优化方案是把热点课程详情缓存到Rediskey为course:detail:{courseId}缓存时间设置为一小时课程信息变更时主动清空相关Redis缓存保证短时间内数据一致性引入Caffeine本地缓存做二级缓存配合Redis减少跨网络开销详情接口改为并行调用利用CompletableFuture同时查询多个数据源耗时降到200ms以内监控这边由于系统本身规模不大没有一上来就上特别重的方案而是先在Nacos控制台看服务健康状态再通过Spring Boot Actuator的/actuator/health接口做基础健康检查。后续如果要深入优化可以引入SkyWalking做分布式链路追踪把每次请求在网关、各微服务间的调用耗时和异常还原出来这是排查线上疑难杂症的最好工具。5.4 微服务鉴权的演进与反思最后聊一下鉴权的演进过程。初期我直接用JWT一个Token打天下后来发现用户角色权限变化、Token被截获等安全问题逐步在网关层做了Token校验和角色透传但仍然有权限控制不细的问题。后来我调研了Sa-Token和Spring Security OAuth2两套方案最终这个项目保持了自己实现JWT网关鉴权的方案因为业务权限模型简单三种角色引入一套完整的安全框架反而增加复杂度。如果你需要更复杂的权限控制比如课程级别的数据权限、按钮级别的权限我建议在网关层基础之上服务内部集成Spring Security定义方法级注解PreAuthorize(hasRole(TEACHER))实现更细粒度的权限管理。这个方案比在网关层拼权限字符串干净得多。写在最后的一些体会做完整个在线教育系统的微服务设计之后我最大的感受是微服务的核心难题从来不是那些框架怎么配置、组件怎么启动而是怎么把业务边界想清楚。很多人在刚接触微服务时热衷于引入新技术、把系统拆得越碎越显得专业但我在这个项目里吃的亏告诉我拆出来的每一个服务都意味着额外的运维成本、分布式事务复杂度、跨服务调用开销。只有当业务复杂度确实到了单一应用无法承载、团队协作确实需要独立发布单元的时候微服务才真正发挥价值。如果你也要做类似的在线教育系统我的建议是先用单体把核心业务流程跑通梳理清楚各个业务域的边界和接口依赖再逐步把高并发模块拆出来独立部署。这个过程最痛苦也最有收获的地方不是代码怎么写而是理清楚什么该拆、什么不该拆、拆完之后怎么保证数据不出错。这套设计里的接口规范、事务处理原则、网关鉴权思路很多都可以直接移植到其他微服务项目里复用。最后再分享一个小技巧项目的所有SQL建表脚本、接口文档、启动顺序说明一定要放在项目的docs目录里配一个README不然几个月后你自己看着代码都头疼。本文还有配套的精品资源点击获取