ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+SpringCloud+Vue:公考知识学习平台微服务架构实践

SpringBoot+SpringCloud+Vue:公考知识学习平台微服务架构实践 这些年一直有人在问教育类项目能不能上微服务尤其是像公务员公考知识学习平台这种看起来就是个刷题网站、视频网站搞那么复杂干嘛。但从我实际做过的项目来看这个领域恰恰是微服务分布式架构最适合施展的场景之一。公考用户群体大、访问时段集中公告一出、考试前几天流量直接顶满、业务模块天然独立题库、课程、社区、资讯、统计分析再加上题目和资料本身是强数据性内容后期还有智能组卷、学情分析、AI批改这些延展需求。用SpringBoot SpringCloud打底配前端Vue做一套微服务分布式系统不是炫技是真的能扛压、好扩展。这篇文章就结合我之前做这套系统的实际经验把从架构设计、核心模块实现到部署排坑的完整链路原原本本拆给你看。不管你是刚接触微服务想找个完整项目参考还是已经上手SpringCloud但卡在某个细节上这篇都能给你一些实打实的启发。1. 平台整体设计与微服务拆分思路1.1 为什么公考学习平台一定要走微服务先聊一个很多同学会纠结的问题一个公考学习平台单体应用用SpringBoot加个MySQL再加个Redis前端Vue写个后台管理系统能不能跑起来说实话能跑但分人跑。你要只是做个课程展示加刷题demo单体肯定够甚至开发效率更快。但一旦你希望这个平台真正商业化运营、要应对公考报名高峰期的高并发、要同时维护多端用户端App、PC端、后台管理端、要持续叠加功能模块单体就会陷入一个很尴尬的局面——改一个题库的bug全服务都得重新打包上线某个模块流量暴涨只能把整个应用水平扩容浪费资源不说扩容出来的实例还会互相争抢数据库连接。微服务拆分的核心逻辑不是按代码分包拆而是按业务边界和变化频率拆。公考学习平台我拆成了六个服务用户服务管登录、注册、会员权限、题库服务管题目的增删改查、组卷、刷题记录、课程服务管视频、课件、直播回放、资讯服务管公告、政策信息、备考文章、订单支付服务管课程购买和会员开通、学习数据分析服务管做题记录收集、正确率统计、学情报告生成。每个服务自己持有独立的数据库和缓存通过服务间接口通信互不直接操作对方的表。从团队协作角度讲这六个服务天然适合分成六个开发组或者六个开发阶段来做。从运维角度看哪个服务压力大就只扩哪个服务题库服务扛不住刷题流量了直接给题库服务加两个实例而不需要把整个系统都扩大三倍。从系统演进角度看后续就算要把AI智能批改单独抽出来做成独立平台也只是再多起一个服务骑在现有的注册中心上就够了。1.2 技术选型SpringBoot版本、SpringCloud组件和Vue版本的取舍技术选型这块我先给出一套可以照抄的版本配置都是我实测过的搭配SpringBoot用2.7.xSpringCloud用2021.0.x也就是之前的Jubilee版本对应的SpringCloud Alibaba用2021.0.5.0前端用Vue2如果你团队熟悉Vue3用Vue3也没问题但要注意和现有组件的兼容性下面会细聊。这里有个很关键的坑SpringBoot和SpringCloud的版本是强绑定的不是随便找最新的就能用。网上很多教程直接拿SpringBoot 3.x配SpringCloud 2022.x起步结果发现原来很多老的第三方组件还不适配报一堆乱七八糟的错。稳妥做生产项目我还是建议用SpringBoot 2.7.x这是2.x系列最成熟的版本生态兼容性最好。服务发现注册中心用Nacos配置中心也用Nacos不用Eureka和Config原因很简单——Nacos同时解决了服务注册发现和配置动态刷新两件事而且是中文生态文档和社区经验都比较丰富。前端这块Vue2 ElementUI是后台管理系统最成熟的组合用户端可以用Vue2 Vant移动端适配比较省事。如果你非要用Vue3那UI库建议换成ElementPlus和Vant4。但从实际团队招聘和开发速度考虑Vue2的存量代码和网上资料都更多踩坑成本低。Vue端的路由用Vue Router状态管理用Vuex打包工具用Webpack不要自己去折腾Vite和Vue2的组合有兼容性问题白费时间。前端还有一个重点视频播放。公考课程的视频一般都在阿里云OSS、腾讯云COS或者自建的服务上而且很多是m3u8格式。Vue播放m3u8最稳的方案是用video.js加videojs-contrib-hls插件或者在hls.js基础上自己包一层播放器。这里我强烈建议用hls.js因为videojs-contrib-hls已经处于维护停滞状态了而且对HLS加密流的兼容性不如hls.js干净。后面章节我会给出一套hls.js播放m3u8的完整代码。1.3 整体架构中数据通信与分布式的基础设施分布式架构最核心的基建就四样服务注册与发现、服务间通信、分布式缓存、分布式锁。注册与发现交给Nacos服务间通信走OpenFeign加SpringCloud LoadBalancer做客户端负载均衡。这里提醒一下从SpringCloud 2020.x之后Ribbon被移除了替代方案就是LoadBalancer你在网上查资料的时候看到老的Ribbon配置基本都可以跳过了那些配置在新版本里会直接报错。数据通信这块同步调用用OpenFeign但像用户刷题后要异步更新答题统计、学习时长这类场景同步调用就不合适了。因为一次刷题请求你不可能等统计分析服务算完正确率再给用户响应那延迟太高了。这时候就需要引入消息队列做异步削峰解耦。我用的是RabbitMQ最简单也够用不需要上Kafka除非你的日活真的到了百万级。刷题提交后题库服务把答题记录和题目ID发给MQ数据分析服务监听队列后台慢慢去统计正确率、薄弱知识点排行用户几乎感知不到延迟。分布式下的缓存也是一门学问。本地缓存Caffeine只能缓存本服务内的热点数据比如题型字典这类几乎不变的配置。业务数据缓存一定要放在Redis里而且要注意加命名空间隔离避免不同服务之间key撞车。比如题库服务的题目缓存统一加前缀qbank:question:{id}用户服务用user:session:{token}一目了然也方便后续做清理。分布式锁是分布式系统中绕不开的一个环节。举一个我们实际遇到的场景公考报名季课程运营同学会批量上架“秒杀课”一个课程同一时段只有50个限量名额。放在单机里用synchronized就行但拆成微服务后多个实例同时处理同一个课程的购买请求不加分布式锁的话50个名额能被卖出80份。我们用Redis的setnx命令实现分布式锁同时设置了过期时间防止拿到锁的服务崩溃导致死锁。后面会专门写一段完整的锁工具类这里先把场景留着。2. 后端SpringBoot服务核心细节解析2.1 用户服务JWT Token、SpringSecurity和Redis会话用户服务是所有服务里最基础也最容易写烂的一个。我们直接抛弃了传统的Session配合Cookie的会话保持方案因为微服务下用户完全有可能因为负载均衡被分发到不同实例上Session复制那套很笨重Session粘滞又会造成单实例压力。我们用JWT作为Token载体配合Redis做一次性校验。整个流程是这样的用户登录时用户服务校验手机号加验证码这里验证码的存储和校验走的就是Rediskey存的是预先分配的验证码IDvalue是验证码内容有效期五分钟。登录成功后生成JWT把用户ID、角色信息、过期时间放进claims里用配置好的私钥签名。然后写一条UserToken:{userId}:{token}到Redis作为服务端对Token的兜底校验。为什么JWT已经包含了用户信息还非得再存Redis因为JWT无法主动失效用户改密码、被踢下线、管理员封号这些场景光靠JWT本身实现不了。Redis里的这条记录就是一个吊销名单查询的时候实时比对逻辑简单而且可靠。SpringSecurity在这一版的配置值得说一下。我们用的是SpringSecurity加JWT的经典过滤器链自定义一个JwtAuthenticationFilter继承OncePerRequestFilter在每来一个请求时先从Header里解析Authorization字段拿到Token后做了三件事——解析签名、查Redis看Token是否有效、把当前用户信息塞进SecurityContextHolder。放行规则上登录接口、验证码接口、课程试看接口是匿名可访问的其余全部要求认证。微服务里要注意用户服务只管自身接口的权限校验像创建订单这种操作不在用户服务里做所以订单服务也要自己配一套资源服务器的配置不能图省事只在一个服务里做认证。2.2 题库服务Hanlp分词、缓存和刷题组件的设计题库服务是整个平台里逻辑最重的一个服务。公考题目的字段结构比想象中复杂题干纯文本、材料大段文字、选项A到D有的题还有不定项、答案、解析、所属学科行测文科、行测理科、申论等、知识点标签、难度系数、历年真题来源年份、省份、考试名称。为了后期做智能推荐题目表还要冗余一个知识点路径字段比如“行测-判断推理-图形推理-空间重构”。这里有一个有意思的技术点搜索题目的时候我们直接用Hanlp分词做了带知识点的全文匹配。很多人听到分词就觉得要做搜索引擎要上ES其实根据我们的数据量十几万道题还没必要上ES。我们的方案是给题目表的关键字段题干、解析、知识点名称建全文索引然后每隔一段时间把题目同步到索引表里。搜索时先用Hanlp从关键词里抽出核心词再用这些词去MySQL的全文索引或者是like匹配。Hanlp在SpringBoot里的整合非常简单——引入依赖然后把自定义词典公考专属词汇例如“行测”“申论”“图形推理”放到resources目录下加载时用CustomDictionary.add方法。组卷这块也是题库服务的高频接口。我设计的组卷引擎支持三种模式按知识点随机组卷比如“考图形推理的15道题”、按历史错题组卷把用户做错的题目按错误率重新凑一套、模拟真题组卷按国考真题的题量和难度分布生成。为了避免每次组卷都实时去查数据库我们把不同组卷规则的结果先用Redis存起来20分钟内有效。同一套试卷题目顺序不能变不然用户分享出去的题目和好友打开看到的对不上所以组卷结果里要存一份题目ID的有序列表按列表回查题目保证了稳定性。2.3 分布式事务扣库存、下订单和更新学习记录分布式事务是每个微服务开发者都会遇到的大山。拿课程秒杀场景举例这个过程至少涉及到订单服务和课程服务两个服务订单服务要创建一条支付订单课程服务要把秒杀课程的库存减掉。如果两个操作不是原子的——订单建了库存没减就会出现超卖库存减了订单没建成功用户支付后找不到订单更麻烦。我们最终选用的是消息事务加本地消息表的最终一致性方案没有上Seata。原因很直接公考课程秒杀场景并没有强到每笔都要求毫秒级强一致而Seata的AT模式下全局锁对数据库性能影响较大尤其在高并发秒杀时会放大热点。我们的实现思路是这样的把“扣库存”这个动作转化为一条本地消息存到同一个事务里。具体一点订单服务在创建订单时同时往本地消息表insert一条“待扣减库存”记录这两个操作在同一个本地事务里保证要么都成功、要么都失败。然后通过一个定时任务扫描本地消息表把“待扣减库存”的消息投递给RabbitMQ课程服务监听队列完成真正的库存扣减扣减成功后回调修改消息状态为“已完成”。有人说这不就是异步消息吗对但核心区别在于把消息写入和业务操作放在同一个数据库事务里解决了“先发消息事务回滚了”的问题。这套方案在日常流量下的稳定性非常高前提是消息表里要记录足够多的上下文信息包括订单ID、课程ID、用户ID、扣减数量方便后续重试和排查。定时任务扫描频率设为30秒一次重试次数上限设为3次超过次数标记为死信人工介入。我们在生产环境跑了半年一个月下来出现的死信消息一般不超过5条而且都能快速定位。2.4 学习数据服务SpringBoot整合Flink做实时学情计算你可能会问一个学习平台有必要上Flink吗先别急着否定。公考学习平台有一个很典型的实时计算场景用户刷题以后需要立刻看到自己的正确率变化曲线、知识点薄弱雷达图、同岗位竞争对手的平均水平对比。如果每次展示都现算题库服务会被实时统计请求拖垮而用普通的定时任务去算延迟又太大。所以我们在学习数据分析服务里用SpringBoot整合Flink做了一条轻量级实时计算链路。我们的架构是用户每做完一道题题库服务发送一条做题事件到MQ消息里包含用户ID、题目ID、是否答对、用时、所属知识点。Fl总结在SpringBoot里的是Flink的DataStream API我们把MQ数据源接入Kafka其实没用Kafka还是接RabbitMQ因为RabbitMQ的吞吐量足够支撑现有的刷题峰值没必要再引入Kafka增加运维负担。这里有个SpringBoot整合Flink的细节很多人容易踩坑Flink的类加载器和SpringBoot的类加载器有冲突直接在SpringBoot里new一个StreamExecutionEnvironment会导致部分依赖加载失败。我当时的处理方式是给Flink任务单独开一个线程池用自定义的ClassLoader去加载Flink相关的jar包这样和SpringBoot主业务隔离开问题就消失了。实时计算的结果写入到Redis里以有序集合存的key是userId:knowledge:{知识点ID}分数是正确率。前端展示学情报告的时候直接查Redis就能拿到一系列数据聚合速度非常快。像“最近7天正确率变化趋势”这类指标在Redis里用多个时间分片计数展示时做合并也够用了。3. 前端Vue的架构实现与资源处理3.1 Vue环境的搭建和工程化规范前端这块我先把搭建步骤捋一遍。不管你用的是Vue2还是Vue3环境准备都差不多Node.js最好用16.x或18.x LTS版本npm或者pnpm二选一不要混用。如果你已经有Node环境注意版本不要太新Node20配合老一些的Vue2项目在安装依赖时偶尔会碰到node-sass编译失败的坑——解决办法很简单用sass这个新包替代node-sass或者干脆退回Node16。工程化规范方面我强烈建议项目一开始就接上ESLint加Prettier并且开启husky的pre-commit钩子。这不是装样子因为公考平台的前端界面有不少管理后台的长表单和列表页多人协作时格式混乱会让代码review效率直线下降。我们在实际开发中用的是Vue CLI创建的项目加上了vue.config.js的代理配置开发环境运行npm run serve时把/api前缀的请求代理到后端的SpringCloud网关地址地址http://localhost:8080。这样前端写代码的时候不用关心后端服务的真实地址是哪个全部走统一网关的入口。组件库的选择上管理后台和用户端要分开。管理后台用ElementUIVue2或者ElementPlusVue3用户端的社区域用Vant。如果你们是小团队做一个PC端加移动端复用的自适应网站也可以直接用ElementUI加响应式布局搞定只是用户端在手机上的体验会稍微差点。我给当时的方案是拆成两个独立前端工程一个pc-web管理后台一个mobile-h5用户端共用同一套API网关开发效率也高。3.2 视频播放模块Vue中播放m3u8的几种有效方案公考课程平台除了刷题大头是看课。课程视频基本都是录播课存在对象存储上转码输出为m3u8格式。前端播放m3u8的方案我实战后排序是原生hls.js、video.js加hls插件、ckplayer组件。hls.js是花了大价钱验证过的对iOS Safari和安卓WebView的兼容性都很好还能自己处理播放过程中的网络切换。直接给一段hls.js播放m3u8的核心代码思路安装hls.js依赖后判断当前浏览器是否原生支持HLS也就是看video.canPlayType(application/vnd.apple.mpegurl)是否返回maybe或probably。如果支持直接把video的src设置为m3u8地址就行主要覆盖的是iOS Safari。如果不支持就用new Hls()创建实例加载地址然后挂载到video元素。这里有个必须注意的点Flutter或者原生App内嵌WebView播放时安卓上必须开硬件解码而且要设置playsinline属性否则视频会弹出全屏播放器用户返回页面就断了。还有一个常见的坑是跨域。m3u8的地址如果在阿里云OSS上而你的OSS没开启CORS规则浏览器去请求m3u8文件的时候会直接报跨域错误视频黑屏。解决的办法是去对象存储控制台给对应的Bucket配置CORS规则允许来源填写你的前端域名允许方法填GET和HEAD这样播放器才能正常拉流。这段音视频经验在项目上线初期帮我们省了很大一块技术债。3.3 前端路由、动态路由与权限控制前端路由设计直接影响用户权限控制的好坏。我们的平台有三种角色普通用户、课程讲师、管理员。后端接口有SpringCloud网关做权限校验但前端如果不配合做路由控制用户还是能通过改URL绕过页面级别的拦截。管理后台的路由分成两类静态路由和动态路由。静态路由是登录页、404页面等不需要权限就能访问的。动态路由是从后端拉取的菜单权限数据以树状结构返回前端根据用户的角色ID去拼接出一份当前用户可访问的路由表在Vue Router的beforeEach守卫里动态添加。这个方案的细节在于你需要在登录成功后先请求一次“当前用户菜单权限”接口拿到菜单数据后调用router.addRoutes方法再放行到目标路由。如果不做这一步刷新页面时路由表会清空用户停留在一个动态路由下就会白屏。用户端的动态路由也类似但少一些。用户端主要控制的是课程列表、个人中心、刷题记录这些模块管理员角色登录用户端时额外出现一个“进入管理后台”的入口按钮不需要单独配一排路由。总之前端路由的核心理念是“不显示未授权的内容”而不是仅仅在页面里藏按钮。3.4 前端打包与部署Vue打好的包怎么塞进SpringBoot这个话题在热词里出现得非常高频Vue打包放进SpringBoot。很多人第一次做前后端不分离部署的时候会搞不清其实逻辑很简单。SpringBoot能直接部署静态资源因为它本身内嵌了Tomcat你把Vue打包后的dist目录里的内容复制到SpringBoot项目的src/main/resources/static目录下重新打包成jar前端页面就跟后端服务一起通过同一个端口访问了。但注意这样做有一个大前提——前端和后台API的访问路径不能冲突。我推荐的做法是前端所有API请求都带一个统一前缀比如/api然后在SpringBoot的application.yml里配置一个mvc的add-mappings让Tomcat把/api开头的请求统统转发到接口层而/static、/ js、/css等路径直接映射到static下的静态文件。还有一种做法是单独起一个Nginx由Nginx来代理静态文件和API请求这种比塞进SpringBoot更专业一点适合整体架构稍微复杂的时候用。如果你真的要把Vue打进SpringBoot里建议前端在vue.config.js中设置publicPath为相对路径也就是直接写成./而不是默认的夸张/开头带域名形式。不然打包出来的index.html里引用的js和css文件路径都是根目录的而你放到static下之后路径就对不上了。这个坑我见过多次代码酸痛部署时一脸懵。4. SpringCloud微服务治理与分布式基础设施深入4.1 注册中心与网关配置实践SpringCloud微服务开源项目里最基础也最关键的组件是注册中心和网关。我们用Nacos做注册中心配置了三个服务实例用户服务、题库服务、课程服务全部注册到Nacos上服务名分别为user-service、question-service、course-service。网关用SpringCloud Gateway主要承担路由转发、统一鉴权、跨域处理、限流。网关配置的核心就是路由规则。举个例子前端访问/api/user/login的请求会先到达Gateway网关根据配置把路径映射到user-service服务。这段路由配置用YAML写是比较直观的spring.cloud.gateway.routes下面id为user-service-routeuri是lb://user-servicepredicates是Path/api/user/**。lb前缀是LoadBalancer的缩写它和注册中心配合把请求按照负载均衡策略转发到某个具体的服务实例。在网关层做鉴权最省心的方式是写一个GlobalFilter在所有请求过滤前统一处理。我们这个过滤器做的事先放行白名单路径比如登录接口、验证码接口、首页资讯接口然后从请求头里拿Token调用用户服务提供的“校验Token”远程接口这个接口内网暴露只允许网关调用返回用户信息后放入请求头再向下游服务传递。这样的好处是下游服务不用自己再解析JWT拿到Header里的userId直接可用业务代码干净很多。限流在公考平台非常关键。公告发布当天全国几十万应届生同时刷公告如果有提供按职位表匹配查询的接口这个接口不限制的话后端数据库瞬间就被打爆。我在网关层给热点接口配置了基于Redis的令牌桶限流每秒最多放行100个请求超过直接返回一个JSON格式的“系统繁忙请稍后再试”。网关限流的实现是RequestRateLimiter过滤器工厂要用到Redis配合配置好的keyResolver按用户ID做维度限流这样同一个用户在一秒内最多发10个请求别人不误伤。4.2 Redis分布式锁的使用场景与正确写法既然热词里反复出现Redis分布式锁和分布式锁面试题这里我就把公考平台实际用到的几种场景和代码细节掰开讲。第一个场景是秒杀扣库存第二个场景是防止用户重复提交学习计划第三个场景是定时任务的多实例互斥。Redis分布式锁的正确写法不是简单地使用setnx命令。因为单纯setnx加expire两步操作不原子会出现一个尴尬情况线程A设置锁成功后还没设置过期时间就挂了key一直存在其他线程永远拿不到锁。正确姿势是用一条命令把set和expire合并起来Redis的set命令本身就支持扩展参数直接在set key value nx ex 30这同一个命令里完成“不存在才设置”和“过期时间30秒”两个动作原子性就不成问题了。还有一个高频面试点是锁续期问题。如果业务执行时间超过了过期时间另一个线程拿到了锁原线程执行完释放锁就释放了别人的锁。解决方案很多常见的有使用Redisson的看门狗机制它能在锁未释放时自动续期也可以自己写一个Handler在Redis接口里定时比较value是否还是自己的线程ID是的话就重置过期时间。我们用的方式是给锁的value设置成UUID释放锁时先get判断当前value是否等于自己的UUID相等才执行del操作用Lua脚本保证判断和删除是一条原子操作。这套写法在面试里能拿高分在实战里也确实稳。分布式锁还有一个容易忽略的细节锁粒度。刚开始我们图省事给整个操作课程秒杀的Service方法加了一把全局锁结果不同课程的并发购买互相阻塞。后来改成把锁的key设计成courseStockLock:{courseId}不同课程各自锁各自的库存并发能力直线上升。这里要提醒大家锁的粒度越细系统吞吐量越高。4.3 微服务架构中的分布式IO与消息异步化分布式IO这个概念听起来玄其实拆开就是文件上传下载、客户端长连接、以及服务间的文件传输。在公考平台里用户上传头像、讲师上传课程课件、导出成绩单这些文件流的操作如果全部走普通的HTTP请求大文件时会造成网络阻塞。我们做了动静分离文件一律直传OSS不走应用服务器。前端从后端拿一个STS临时凭证然后把文件直接传到OSS的指定Bucket上传完成后再回调通知后端记录文件Url。这个模式比通过后端转发的方案爽快很多能减少后端带宽压力也避免应用服务器在文件流传时延迟飙高。消息异步化在SpringBoot里的落地方案用ActiveMQ还是RabbitMQ争议不大我们最后选的是RabbitMQ。SpringBoot整合RabbitMQ非常方便主要就三件事配连接参数、声明交换机队列、写生产者消费者。在实际项目里我习惯用延迟消息来处理一些经验性逻辑。比如用户连续学习打卡一天后要发一张“今日打卡成功”的提醒通知我们就可以让打卡事件消息延迟1分钟再投递给通知服务从而给用户一个缓冲降低用户的刷屏感。RabbitMQ的延迟消息需要安装延迟插件配合SpringBoot的转换成消息后设置消息的x-delay属性来实现这个用法比固定死等的定时任务灵活多了。4.4 分布式架构下的数据一致性与跨服务查询策略微服务拆完之后最让人头疼的其实是“怎么查询跨服务的数据”。举个例子首页要展示一条课程报名记录里的用户昵称和课程名如果数据库拆成用户库和课程库就不能像单体一样连表join了。我们的方案是在设计数据库时就提前做好数据冗余。报名表里除了存userId还冗余存了一份用户手机号脱敏昵称、课程名、课程封面图。用户改名或者课程名变更时通过消息通知到报名服务异步更新冗余字段。这样查询首页时报名服务本身就是一个完整的宽表不用远程调用其他服务。确实这会造成一定程度的数据不一致。比如用户改名一瞬间报名记录里还是旧昵称但在一两分钟之后异步消息就把它更新过来了。大多数业务场景都能接受这种短暂不一致。如果你的业务场景要求严格一致那就要考虑用分布式事务的方案但性能上肯定会有损耗。所以我的习惯是——默认允许最终一致性只有涉及支付、库存这种窗口期很短的核心场景才上强一致的解决方案。跨服务查询的还有一个策略是引入CQRS把查询请求和写请求分离查询走独立的数据副本。这类例子就像公考平台里的大数据统计用户学习报告需要聚合用户信息、题目信息、课程信息如果每次都实时跨服务去拉接口会慢到无法接受。所以我们在数据分析库里建立了一份“用户答题明细宽表”每天定时从各个服务同步数据落库报表导出和首页学情展示全部走宽表速度快且稳定。5. 项目实操过程与Maven多模块工程构建5.1 Maven多模块项目的搭建方法回头说说工程结构。工欲善其事必先利其器微服务项目的工程结构如果不预先设计好后面是灾难。用Maven构建多模块项目比用IDEA里乱建几个Module要严谨得多。我们的项目父POM是platform-parent下面聚合了common模块、各个服务模块、网关模块。common模块非常关键里面放的是所有服务都会用到的统一返回结果类Result、全局异常处理器、JWT工具类、Redis工具类、统一常量类。每个服务模块都依赖common而不是各自造一个返回结构这样前端对接API的时候体验统一不会出现用户服务返回的是{code:0,data:{}}题库服务却返回自己的结构。父POM里定义了所有依赖的版本号子模块只写依赖坐标不写版本号统一升级依赖版本时直接从父POM改效率极高。这里要注意Maven多模块在IDEA中导入时要确保每个子模块是通过父POM引用的而不是给每个子模块都建了独立的maven project否则你会看到它们之间互相引用时一直报找不到类。5.2 SpringBoot的配置管理开发、测试、生产环境分离SpringBoot配置管理的核心思想是“一份代码多个环境”。我们在resources目录下放了application.yml、application-dev.yml、application-test.yml、application-prod.yml。application.yml里只放通用的配置比如应用名、日志格式后面的环境配置文件里放对应的数据库地址、Redis地址、Nacos地址。启动时用spring.profiles.activeprod来激活生产环境。这就必须小心一件事千万不要把生产环境的数据库密码提交到代码仓库里。后端同学交接项目时要约定流程让部署的人用环境变量覆盖比如在启动脚本里设SPRING_DATASOURCE_PASSWORD然后在application-prod.yml里引用${SPRING_DATASOURCE_PASSWORD}。这样就算代码泄露了数据库密码也是安全的。配置中心选择Nacos时又有一个细节。application.yml里要配置spring.cloud.nacos.config.server-addr同时也要配置spring.config.importnacos:xxx.yaml这个语法在SpringCloud 2021.x里是个硬性要求否则配置怎么拉不下来。很多同学在这个地方卡一整天就是少一个import导包配置。5.3 一个实际客户端请求的完整调用链演示拿用户在首页查看一个课程详情并开始播放视频来举例PC端Vue先发起GET请求到网关地址是/api/course/detail/123。网关的GlobalFilter先校验Token拿到用户身份后转发到课程服务的Controller。课程服务先查Redis缓存key是course:detail:123如果命中直接返回数据如果没命中就去课程数据库查询详情查完写入缓存然后返回。在返回之前课程服务还要调用学习记录服务的接口查询一下这个用户对该课程的已学时长拼装到详情里。这样一个接口就涉及了三个服务也展示了Nacos注册中心、网关、Redis缓存、OpenFeign远程调用在链路里各自的作用。如果用户点的是“开始播放”前端直接请求的是视频文件的m3u8地址不再经过后端。播放过程中前端每30秒向后端上报一次学习进度这个上报接口是异步的不返回大量数据只存Redis等课程学完再批量落库。5.4 单体双击部署还是微服务集群部署云服务器配置参考微服务集群的部署至少需要一台4核8G的服务器跑Nacos网关和基础组件再加两台8核16G的服务器跑业务服务实例。如果没有多台服务器预算还有折中方案一台高配服务器上起多个实例用不同端口区分比如用户服务分别在8081和8082各起一个配合Nacos负载均衡也能享受到微服务的高可用。当然生产环境建议至少三台Nacos自己也要部署集群至少三个节点防止注册中心挂掉后服务全部不可信。每台服务器上最基础的中间件就是Docker。环境准备用Docker Compose直接拉起来MySQL主从、Redis主从、Nacos集群、RabbitMQ。MySQL就用主从复制模式一主一从至少Redis也是主从加哨兵RabbitMQ单机或集群看预算。这套基础环境大约需要2个Docker Compose文件一键启动省时省力。6. 常见问题与排查技巧实录6.1 SpringBoot启动报错排查版本冲突与配置缺失大家在代码里最容易碰到的一个问题就是SpringBoot启动失败各种一大堆错误日志让人眼花缭乱。我用一个“时间线法”来排查先看错误是什么时候发生的是在上下文加载阶段、Bean创建阶段、还是端口绑定阶段。如果是上下文加载阶段基本判断是依赖冲突或者配置类加载出错如果是Bean创建阶段看具体是哪个Bean创建失败最常见的就是Autowired注入的Bean为null因为对应的服务没有启动或配置了错误的名称。SpringBoot版本太高老项目里引入了一些老版本第三方依赖兼容性就会崩。比如SpringBoot 3.x要求JDK17而你用JDK8编译的直接报错UnsupportedClassVersionError。解决思路是别硬扛看第三方依赖的版本跟不跟得上新框架如果跟不上就乖乖降级SpringBoot版本。生产经验只要不是从零开始要用到最新特性就用多数第三方库兼容性最好的2.6或2.7版。6.2 SpringCloud网关跨域与路由转发常见坑SpringCloud Gateway自带跨域处理很多人在配置跨域时就粗暴地用CorsConfiguration加addAllowedOrigin但新版SpringCloud要求必须用allowedOriginPatterns否则前端请求能带Cookie请求就报错。还有个更隐蔽的坑我们在网关配置了跨域后又在业务服务单独配了跨域结果是请求先被网关放行再到业务服务时又过一次检查由于最后响应头被业务服务的跨域配置覆盖可能会导致前端再次误判为跨域。处理方式很干脆网关跨域处理统一全局策略业务服务里不要重复加跨域过滤器逻辑。路由转发的坑更多是路径重写。前端请求/api/question/list网关转发到题库服务的时候如果直接转发给question-service控制器控制器接收到的RequestMapping通常是在类上又加了一层接口前缀。我们处理得很干脆网关在转发时用RewritePath过滤器把/api/question/(? .*)重写为/${segment}这样下游服务的接口路径不会因为网关前缀而设计得很别扭控制器里也不用写疯狂的嵌套路径。6.3 Vue前端部署后的白屏问题排查前端部署后最常见的两个问题一个是打开页面白屏控制台报“Uncaught SyntaxError: Unexpected token ”另一个是刷新后404。第一种白屏往往是因为服务器把静态文件当做了接口处理返回的是HTML而非静态资源此时看下Nginx配置里location的root路径是否正确指向了打包dist目录。第二种刷新后404通常是因为前端路由是history模式Nginx没有配置try_files回退到index.html。解决方法是Nginx里加一句location / { try_files $uri $uri/ /index.html; }。如果你用的是Hash模式比如带/#/的URL就完全没有刷新404的问题代价是不太美观但够用就行。6.4 分布式环境下接口性能变慢的排查思路接口变慢的问题在分布式环境下排查起来比单机要复杂。我的习惯是从请求链路往下走先看着网关日志请求在哪一步耗时最长再打开服务提供方日志看那段时间有没有GC暂停有没有数据库慢查询。有一种容易出现的情况是数据量不大却一直在远程调用其他服务每个服务愿又几十毫秒加起来就超过3秒。遇到这种情况优先把远程调用改成批量接口或者是加一个缓冲合并处理器把单个请求合并成一个批量请求发出去。排查要用到链路追踪工具我们当时用了Sleuth加Zipkin。虽然不是每个微服务项目都一定要上但一旦出问题没有链路信息真的寸步难行。从投入产出比来说趁项目还在早期就接上链路追踪等到排查性能瓶颈时能节约大量的时间。个人实操经验总结这套公考知识学习平台从架构设计到最后上线我个人的体会是微服务不是银弹它解决的问题是业务复杂度、团队协作和高可用问题但随之而来的部署复杂度、分布式事务和监控运维难度都会成倍增加。最关键的是想清楚“拆出去是为了什么”——如果用不着弹性扩容、用不着多团队并行单体反而是更务实的选择但一旦业务确实到了需要拆的规模那就要把服务边界、数据一致性策略、基础组件选型一次性想明白再动手写代码。另外公考学习平台这种内容题库课程的综合业务是练习微服务落地的好载体业务边界清晰、数据敏感度高、并发场景真实。做一次这个项目你对SpringBoot、SpringCloud、Redis、MQ、Vue的理解和使用能力会有一个质的提升以后换到别的业务领域这套技术底座依然能打。
返回列表