ARTICLE DETAIL

资讯详情

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

从单体到微服务:Vue+SpringCloud博客系统设计与实现

从单体到微服务:Vue+SpringCloud博客系统设计与实现 简介一套基于Vue与SpringCloud的微服务博客系统完整源码及项目文档面向具备Java基础、想系统掌握前后端分离与微服务架构的开发者可帮助解决分布式项目中服务治理、网关路由、链路追踪等落地难题。压缩包共1025个文件含265个java后端源码、360个js脚本、70个vue组件、55个xml配置及23个yml等类型涵盖前后端编码、构建配置、说明文档与图表样式资源总大小91.44MB。目前已有648人学习下载。项目覆盖Eureka高可用集群、Zuul网关、Feign负载均衡、Elasticsearch与Zipkin链路追踪等微服务核心组件并整合Redis缓存与Redisson分布式锁、RabbitMQ延迟队列、Elasticsearch高亮分页、WebSocket实时通信及支付宝支付支持Docker部署采用前后端分离工程结构从服务发现、网关路由到分布式协同均有可运行实现代码注释完整、扩展方便适合作为毕设、课程设计或全栈微服务学习的参考工程。1. 从单体到微服务这套 VueSpringCloud 博客系统的设计与实现个人博客用微服务乍一听是杀鸡用牛刀但把它当成一个学习样本价值就完全不一样。这套基于 Vue SpringCloud 的博客系统麻雀虽小五脏俱全前端是 Vue 单页应用后端按业务拆成用户、文章、评论三个服务中间有 Gateway 网关统一入口Redis 扛缓存Nacos 做注册与配置中心。它比电商、秒杀这类大型项目简洁得多但该有的微服务组件一个不少。如果你正在做毕设、从 SSM 单体往 SpringCloud 过渡或者前端想完整走一遍后端链路这套资源的架构思路和代码落地方案都值得照着敲一遍。我把拆解过程中涉及的服务划分、参数配置、跨域处理和缓存动手实践全部整理出来所有命令都是本机可复现的踩过的坑也一并交代清楚。2. 把博客拆成微服务SpringCloud 组件选型与四个模块划分2.1 为什么选 SpringCloud 而不是 SSM 单体技术栈对比先回答一个很多人会问的问题博客这种内容型网站单体能跑微服务也能跑凭什么用后者我看过不少 springcloud 微服务开源项目多数是抄来抄去的订单系统业务逻辑复杂但讲解混乱反而不适合入门。这套博客的好处是业务模型足够简单——文章、用户、评论天然适合按业务边界切分。从技术栈对比来看结论更清楚维度SSM 单体SpringCloud 微服务部署一个 WAR 包多个服务独立部署数据库单库单表按服务分库学习曲线平缓陡峭需要理解注册中心、网关、负载均衡适用场景内容简单、访问量可控需要横向扩展、服务拆分、团队协作踩坑成本低高网络、版本、配置都可能出问题博客确实不需要微服务但你需要通过一个熟悉的业务去理解微服务的协作方式。这类「入门简洁」的实践项目恰恰是最合适的跳板。2.2 模块怎么拆一次请求的完整链路这套博客系统拆了四个服务网关服务blog-gateway、用户服务blog-user、文章服务blog-article、评论服务blog-comment。每个服务独立一个端口数据库按服务拆分用户表、文章表、评论表各归各的库。一次请求在微服务环境下的完整链路是浏览器发起请求先打到 Gateway 网关网关做 JWT 校验和路由转发文章相关的请求被转发到文章服务文章服务查 Redis 缓存缓存未命中再查数据库页面需要显示评论时文章服务通过 Feign 调用评论服务拿到评论列表组装后返回前端。用户登录则是先到网关再转发到用户服务用户服务校验账号密码后签发 JWT。这样拆的好处是每个服务只关心自己的表代码边界清晰团队协作时互不干扰。坏处也很明显——你在本地要同时启动 Nacos、Gateway 和三个服务机器配置差一点就会卡。我一般建议至少 16G 内存再跑全套。2.3 基础设施搭建Nacos 注册中心与配置中心服务注册与发现用的是 Nacos既当注册中心又当配置中心这是目前 SpringCloud 微服务开源项目里最常见的选型。相比 EurekaNacos 自带控制台和配置管理对新手友好很多。先加入依赖dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId version2021.1/version /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-config/artifactId version2021.1/version /dependency版本号要和 SpringBoot 对应。我本地用的是 SpringBoot 2.6.x 配 SpringCloud Alibaba 2021.1这套组合经过大量项目验证坑最少。然后配置地址spring: application: name: blog-article cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 file-extension: yamlapplication name是注册到 Nacos 的服务名网关根据这个名字做路由转发。server-addr是 Nacos 的地址本地默认 8848 端口如果你改了端口这里要同步改。Nacos 启动用 standalone 模式startup.cmd -m standalone启动后访问http://localhost:8848/nacos默认账号密码都是 nacos记得第一次登录后改掉。很多人在注册中心翻车多半是 Nacos 没启动就把服务启动了或者 namespace 不一致导致服务找不到——这个问题后面避坑章节专门写。3. Vue 前端落地路由参数、代理配置与打包进 Spring Boot3.1 环境配置与项目初始化前端部分依赖 Node.js 环境。vue 安装及环境配置这一步最常见的问题不是不会装而是装完 node 之后 npm 命令不生效或者版本太新导致 Vue CLI 安装失败。我一般用 nvm 管理 Node 版本稳一点。node -v npm -v npm install -g vue/cli vue create blog-frontendvue create会进入交互式命令行选择手动配置勾选 Router 和 Vuex。如果网络不好npm 装依赖卡住可以切换淘宝镜像源但公司内网环境请确认是否允许使用第三方镜像。安装依赖常报错我遇到最多的是node-sass编译失败原因是 Node 版本与 node-sass 版本不匹配。现在的新项目建议直接用sass替代node-sass免编译省心很多。3.2 路由设计动态路由与参数传递博客前端路由只有四个核心页面首页文章列表、文章详情、登录页、个人中心。麻雀虽小但 vue 路由参数该有的细节一个不少。// router/index.js const routes [ { path: /, name: Home, component: () import(/views/Home.vue) }, { path: /article/:id, name: ArticleDetail, component: () import(/views/ArticleDetail.vue), props: true }, { path: /login, name: Login, component: () import(/views/Login.vue) }, { path: /user/:username, name: UserCenter, component: () import(/views/UserCenter.vue), props: true }, ]/article/:id是动态路由:id对应文章 ID页面里通过this.$route.params.id拿参数。props: true可以让路由参数直接作为组件 props 传入组件内部不用再依赖$route可测试性更好。从列表页跳到详情页时有个天然坑如果用户从/article/1点击推荐位跳到/article/2Vue 会复用同一个组件实例created钩子不会重新执行页面内容不更新。解决方法是给router-view加key或者监听$route变化重新拉取数据watch: { $route.params.id(newId) { this.fetchArticle(newId) } }3.3 跨域与代理vue.config.js 的 devServer开发阶段前端跑在 8080 端口后端网关跑在 8080 端口直接请求必然跨域。常规做法是在 vue.config.js 里配置 devServer 代理。// vue.config.js module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } }target指向网关地址changeOrigin必须为 true否则后端收到的请求头里Host还是前端域名。pathRewrite把前端的/api前缀去掉再转发网关侧就不用额外处理前缀。开发模式下前端代码里写axios.get(/api/article/list)实际请求会被代理转发到http://localhost:8080/article/list。上线后这段代理配置不生效需要靠 Nginx 做反向代理这是后面打包部署的重点。3.4 打包放进 Spring Bootdist 目录与其他路径的关系vue 打包放进 springboot 中这个操作看起来只是把目录复制过去实际操作坑不少。执行构建命令npm run build产物在dist目录下。把dist下的所有文件复制到网关服务或其他静态资源服务的src/main/resources/static目录里重新打包网关服务即可。注意三个前提。第一publicPath 必须是相对路径module.exports { publicPath: ./, outputDir: dist, assetsDir: static }publicPath写成./打包出来的资源引用才是相对路径否则部署到子路径下所有静态资源全部 404。第二路由使用 history 模式时Spring Boot 需要把未知路径转发到 index.html否则刷新页面直接 404。第三如果后端接口有 context-path前端代理和 Nginx 的 location 都要同步调整三处不一致就翻车。4. 后端服务细节Gateway 鉴权、Feign 调用与文章服务4.1 网关层JWT 校验与白名单网关是微服务的门面所有请求先进这里做统一鉴权。博客场景下用 JWT 无状态校验比 Session 更适合微服务因为用户服务不需要维护会话状态网关校验通过后把用户 ID 放进请求头转发到下游服务即可。Component public class AuthFilter implements GlobalFilter { private static final ListString WHITE_LIST Arrays.asList(/auth/login, /article/list, /article/detail); Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path exchange.getRequest().getURI().getPath(); if (WHITE_LIST.contains(path)) { return chain.filter(exchange); } String token exchange.getRequest().getHeaders().getFirst(Authorization); if (!JwtUtil.validate(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } }白名单放行登录、文章列表、文章详情这类不需要登录的接口其余请求强制校验 token。JWT 校验失败返回 401前端 axios 拦截到 401 后跳转登录页。注意 JWT 的密钥不要硬编码在代码里要放到 Nacos 配置中心环境不同用不同密钥。还有 token 过期时间博客场景一般设置 7 天但如果是练习项目2 小时更接近真实业务。过期时间写太长后面调试权限问题时你会怀疑人生。4.2 文章服务分页查询与统一返回结构文章服务是核心所有服务统一返回结构前端处理起来才不费劲。RestController RequestMapping(/article) public class ArticleController { Autowired private ArticleService articleService; GetMapping(/list) public RPageResultArticleVO list( RequestParam(defaultValue 1) Integer current, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String categoryId) { PageResultArticleVO page articleService.pageQuery(current, size, categoryId); return R.ok(page); } }pageQuery内部用 MyBatis-Plus 的分页插件底层是分页拦截器自动拼接LIMIT语句。current是页码size是每页条数这两个参数必须做上限控制否则用户传一个size10000会把数据库打爆。categoryId是可选参数用来做分类筛选。统一返回结构R里包含 code、message、data 三部分code 为 0 表示成功非 0 表示各种业务异常。前端 axios 响应拦截器统一判断 code省去每个请求单独处理的重复劳动。4.3 评论服务与 Feign跨服务拿用户名评论是拆出来的独立服务但前端展示评论时需要显示用户名用户名存在用户服务里。跨服务获取数据有几种做法这里选 Feign 调用。FeignClient(name blog-user, fallback UserClientFallback.class) public interface UserClient { GetMapping(/user/info/{id}) RUserInfoVO getUserInfo(PathVariable(id) Long userId); }name是目标服务的注册名Feign 内部通过负载均衡从 Nacos 找到服务实例。评论服务拿到评论列表后根据 userId 的集合调用一次这个接口批量换取用户名和头像然后组装返回。这里有个性能坑要提醒千万不要在 for 循环里一条一条调用 Feign评论有 100 条就发 100 次请求服务必然被拖垮。正确做法是接口设计成批量查询一次传多个 ID。另外 Feign 调用的超时时间默认很短本地调试稍慢就超时配置里要放宽连接超时和读取超时。feign: client: config: default: connectTimeout: 5000 readTimeout: 50004.4 登录与令牌JWT 无状态方案登录流程前端把用户名密码发给网关网关转发到用户服务用户服务校验通过后生成 JWT 返回前端前端存到 localStorage 或 cookie。后续请求在 header 里带上Authorization: Bearer token网关校验签名与过期时间。JWT 方案对比 Redis 存登录态优势是网关不需要回源查 Redis多实例部署也天然支持。劣势是 token 无法主动吊销用户改密码后旧 token 仍然有效直到过期。博客这种低安全敏感场景完全够用但做项目实践时可以思考如何用 Redis 黑名单列表解决这个缺陷——这也是面试官喜欢问的点。用户密码存储用 BCrypt 加密就算数据库泄露也不会直接暴露明文。注册接口写好后记得做密码强度校验全数字化密码在真实场景会被刷爆。5. 常见问题避坑跨域、注册、缓存与端口之争5.1 排错顺序与日志入口微服务排错比单体难因为一个请求跨多个服务问题可能发生在任意环节。我自己的排错顺序是固定的先看浏览器 Network 面板请求是否发出去、状态码多少然后看网关日志有没有进再看目标服务日志有没有打最后看数据库慢查询和 Redis 命中情况。这套顺序每次都能快速缩小范围省去乱猜的时间。网关日志里TraceId要打通到下游服务建议接一个日志链路追踪组件。如果暂时没接至少在各服务日志里统一打印请求路径和用户 ID否则定位跨服务问题就是在黑匣子里摸。5.2 前端请求 403CORS 与 OPTIONS 预检现象前端请求后端接口浏览器控制台报 CORS 错误请求根本没发出去就是 403。 原因浏览器同源策略拦截跨域请求会先发 OPTIONS 预检后端没正确处理预检请求。 解决在网关层写一个 CORS 过滤器允许来自前端域的跨域请求并对 OPTIONS 请求直接放行。Configuration public class CorsConfig { Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(http://localhost:8080); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsWebFilter(source); } }allowedOrigin不要写成*因为开启allowCredentials时浏览器不允许*作为来源。生产环境这个配置也建议按域名精确配置否则任何网站都能跨域调用你的接口。5.3 Nacos 注册不上namespace、版本与网络现象服务启动成功日志也打了 register但 Nacos 控制台看不到服务。 原因最常见的三个namespace 不一致、服务端口没对外开放、SpringCloud Alibaba 版本与 Nacos 服务端版本不兼容。 解决先确认三个服务的spring.cloud.nacos.discovery.namespace配置一致默认是 public再看防火墙是否放行 8848 端口最后检查 Nacos 服务端版本2.x 服务端配老版本客户端会出现注册成功但心跳失效的玄学问题建议服务端 2.2.0 以上配客户端 2021.1 以上。注册不上去是最费时间的坑因为服务启动看起来正常也没有异常堆栈就是列表里找不到。排查时打开 Nacos 客户端日志看到 heartbeat 正常才说明真的注册上了。5.4 Redis 缓存穿透并发请求打爆数据库现象首页文章列表接口慢得离谱数据库 CPU 飙升但 Redis 里明明有缓存。 原因缓存 key 设计不合理。比如查询不存在的文章 IDRedis 没有缓存请求全部穿透到数据库大量恶意请求直接压垮数据库。 解决对空值也做缓存缓存时间设置短一些同时做参数校验非法 ID 直接拒绝。更稳的方案是加布隆过滤器所有请求先过布隆过滤器判断 ID 是否可能存在不存在的直接返回。缓存空值虽然简单但要注意设置过期时间否则大量不存在的 key 堆积在 Redis 里内存迟早被耗光。5.5 Vue 部署后刷新 404history 路由的兜底现象前端打包放进 Spring Boot 后访问首页正常点击链接跳转也正常但刷新详情页变成 404。 原因前端用 history 模式浏览器直接请求/article/1后端没有这个路由返回 404。 解决网关层加一个转发规则把非 api 开头的路径都转发到 index.html交给前端路由处理。spring: cloud: gateway: routes: - id: frontend uri: no://local predicates: - Path/** filters: - RewritePath/.*, /index.html这个配置要放在最后作为兜底路由前面的 api 路由优先匹配。Nginx 部署也一样try_files $uri $uri/ /index.html;一行解决。6. 阅读量计数的优化Redis 计数与定时落库的完整闭环博客的文章详情页有阅读量最简单的实现是每次访问update article set view_count view_count 1 where id ?但并发一上来这条 update 会频繁锁行严重拖慢详情页响应。我的做法是 Redis 计数、定时落库。每次浏览详情时INCR article:view:{id}详情接口直接读 Redis 这个 key 返回阅读量。这样数据库一条 update 都不用发Redis 单线程处理 INCR天然原子并发再高也不会出现计数丢失。后台再挂一个定时任务每 5 分钟把这段时间的增量刷回数据库。Scheduled(cron 0 */5 * * * *) public void flushViewCount() { SetString keys redisTemplate.keys(article:view:*); for (String key : keys) { String articleId key.split(:)[2]; Integer increment redisTemplate.opsForValue().get(key); if (increment ! null increment 0) { articleMapper.increaseViewCount(articleId, increment); redisTemplate.delete(key); } } }这段逻辑的关键是INCR命令本身返回值是自增后的值我这里是每 5 分钟读取一次再清空重计。更稳妥的做法是用 ZSET 或者队列做增量持久化防止服务崩溃丢失 5 分钟内的计数但博客场景这个精确度足够了。验证这一步容易被忽略个人博客系统测试不能只测功能通不通并发行为必须验证。我一般 launch 一个 JMeter 压 100 个线程循环看详情页压完比对 Redis 总计数和数据库落库数据是否一致几次下来数据一致才算通过。这套方案我接手过三次前两次都在落库时机上栽过跟头——定时任务没加分布式锁多个实例同时跑就重复计数。从那以后我每次做这类缓存写回功能都强制走一遍并发压测和落库校验自己确认过数据无损再上线希望帮到你。本文还有配套的精品资源点击获取
返回列表