简介:这是一套基于Vue与SpringCloud的前后端分离博客系统完整源码,面向具备Java与前端基础、希望深入微服务架构与分布式部署的开发者,可用于课程设计、毕业设计或全栈项目实战参考。压缩包共1025个文件,约91.44MB,以360个js、265个java、70个vue为核心,辅以xml、json、yml等配置与依赖文件,另有scss、less样式及图片、文档等资源,结构完整。项目采用高可用Eureka与Zuul,结合Feign实现负载均衡并配置回退避免级联故障,同时以Elasticsearch作为Zipkin存储跟踪微服务Api指标。技术栈覆盖Mybatis分页、Redis缓存、Redisson分布式锁、RabbitMQ延迟队列、WebSocket实时通信及支付宝支付,前端使用Vue、axios、UI组件与v-charts图表,代码注释齐全、扩展方便,最终以Docker方式部署。目前已有648人学习,适合系统掌握微服务全栈开发与中间件整合的读者。
1. 从单体到微服务:一套博客系统为什么要拆成 Vue + SpringCloud
很多开发者第一次听到「基于 Vue + SpringCloud 博客的设计与实现」,第一反应是:一个博客而已,用得着上微服务吗?我一开始也这么想。直到某次帮一个技术社区做内容平台重构,单体应用在文章发布高峰期把数据库连接池打满,首页接口响应从 200ms 飙到 4s,运维重启了三次才扛过去。那次之后我才真正理解,博客系统一旦涉及用户、文章、评论、搜索、文件、通知这些模块,单体架构的耦合就会变成运维的噩梦。
这套方案的核心思路是:前端用 Vue 做组件化渲染和路由管理,后端用 SpringCloud 把博客拆成用户服务、文章服务、评论服务、搜索服务、文件服务等独立单元,通过注册中心、网关、配置中心把它们串起来。它适合有一定 Java 和 Vue 基础、想从单体 CRUD 过渡到微服务架构的开发者,也适合需要支撑多端内容分发的团队。接下来我会把选型理由、搭建步骤、参数配置和踩过的坑一条条讲清楚。
2. 服务拆分与注册发现:Nacos 怎么配才不翻车
2.1 博客微服务的拆分粒度怎么定
拆分的第一个问题是粒度。拆得太粗,等于没拆;拆得太细,一个博客搞出十几个服务,运维成本反而更高。我一般按「业务边界 + 数据独立性」来切,博客场景下推荐拆成五个核心服务:
| 服务名 | 职责 | 独立数据表 |
|---|---|---|
| user-service | 注册、登录、JWT 签发、用户信息 | t_user |
| article-service | 文章 CRUD、分类标签、草稿 | t_article、t_category |
| comment-service | 评论、回复、审核状态 | t_comment |
| search-service | 全文检索、关键词高亮 | Elasticsearch 索引 |
| file-service | 封面图、附件上传下载 | 对象存储 |
网关和注册中心不承载业务数据,单独部署。这样拆的好处是:文章服务压力大时可以单独扩容,评论服务出问题不会拖垮登录。注意不要按「controller 层一个服务、service 层一个服务」这种技术分层去拆,那是典型的伪微服务,后面调用链会乱成一团。
2.2 Nacos 注册中心的配置与启动
注册中心选 Nacos 而不是 Eureka,主要原因是 Nacos 同时提供注册发现和配置管理,少维护一个组件。下面是我常用的 Nacos 单机启动命令和 SpringCloud 服务端配置。
# 启动 Nacos 单机模式,指定独立存储避免重启丢配置 sh startup.sh -m standalone # 默认端口 8848,控制台地址 http://localhost:8848/nacos# 每个业务服务的 bootstrap.yml 公共部分 spring: application: name: article-service # 服务名,必须与调用方一致 cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: blog-dev # 多环境隔离,生产用 blog-prod group: DEFAULT_GROUP config: server-addr: 127.0.0.1:8848 file-extension: yaml namespace: blog-dev逻辑说明:spring.application.name是服务在注册中心里的唯一标识,调用方通过这个名字做负载均衡。namespace用来隔离开发、测试、生产环境,避免本地调试时把请求打到生产节点。file-extension决定配置中心拉取的文件格式,要和 Nacos 里创建的配置类型一致。
参数说明:server-addr如果 Nacos 部署在另一台机器,改成对应 IP;group默认 DEFAULT_GROUP 即可,除非你要做灰度分组。启动顺序上,先起 Nacos,再起各业务服务,最后起网关。如果服务启动时报「no available server」,九成是 Nacos 没起来或者 namespace 写错了。
2.3 服务间调用的两种方式与选择
服务间调用常见做法有两种:RestTemplate + LoadBalancer,或者 OpenFeign。我一般用 OpenFeign,因为声明式接口写起来更接近本地方法调用,可读性好。
// article-service 中声明调用 comment-service @FeignClient(name = "comment-service", fallback = CommentFallback.class) public interface CommentClient { @GetMapping("/comment/count/{articleId}") Result<Integer> countByArticleId(@PathVariable("articleId") Long articleId); }逻辑说明:@FeignClient的name必须和 comment-service 注册到 Nacos 的名字完全一致,大小写敏感。fallback指定降级类,当评论服务不可用时返回兜底数据,避免文章详情页整个挂掉。
参数说明:@PathVariable里的值要和路径占位符一致,否则运行时报参数绑定失败。Feign 默认超时较短,建议在配置里把feign.client.config.default.connectTimeout设为 5000、readTimeout设为 10000,否则评论服务稍慢就会触发熔断。
3. 网关与鉴权:JWT 在 SpringCloud 里怎么串起来
3.1 网关路由配置与跨域处理
网关是整个系统的入口,所有前端请求先到网关,再由网关转发到具体服务。Vue 前端开发时跑在 5173 或 8080 端口,和后端不同源,跨域必须在这里解决。
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowedOriginPatterns: "*" # 生产环境替换为具体域名 allowedMethods: "*" allowedHeaders: "*" allowCredentials: true routes: - id: article_route uri: lb://article-service predicates: - Path=/api/article/** filters: - StripPrefix=1逻辑说明:lb://表示走负载均衡到注册中心里的服务,而不是写死 IP。StripPrefix=1会去掉路径的第一段,比如/api/article/list转发后变成/article/list,这样后端 controller 不用带/api前缀。
参数说明:allowedOriginPatterns用通配符时不能和allowCredentials: true同时用allowedOrigins: "*",这是 Spring 的限制,必须用 patterns。生产环境一定要把来源收窄到自己的域名,否则等于把接口暴露给任何网站。
3.2 JWT 鉴权过滤器与 Vue 端的配合
鉴权放在网关做,业务服务只信任网关传来的用户标识,这样每个服务不用重复写登录校验。
// 网关全局过滤器核心片段 public class AuthFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst("Authorization"); if (token == null || !token.startsWith("Bearer ")) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } try { Claims claims = JwtUtil.parse(token.substring(7)); // 把用户 id 透传到下游服务 ServerHttpRequest req = exchange.getRequest().mutate() .header("X-User-Id", claims.getSubject()).build(); return chain.filter(exchange.mutate().request(req).build()); } catch (Exception e) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } } @Override public int getOrder() { return -100; } // 优先级高,先执行 }逻辑说明:过滤器从请求头取 token,解析成功后把用户 id 塞进X-User-Id请求头传给下游。下游服务直接从请求头拿用户 id,不再解析 token,减少重复计算。
参数说明:getOrder返回 -100 表示在大多数内置过滤器之前执行。Vue 端用 axios 拦截器统一加 token:
// Vue 端 axios 请求拦截器 axios.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) config.headers.Authorization = `Bearer ${token}` return config })注意 token 过期时间不要设太长,博客系统一般 2 小时足够,配合刷新 token 机制。如果直接把 token 存 localStorage,要防 XSS,富文本内容渲染时务必做转义。
4. 数据一致性与缓存:文章详情页的读写分离实践
4.1 文章详情的多级缓存设计
文章详情是读多写少的典型场景,我一般用「本地缓存 + Redis」两级。本地缓存扛热点文章,Redis 扛全量,数据库兜底。
// article-service 中查询文章详情 public ArticleVO getArticleDetail(Long id) { String localKey = "article:local:" + id; ArticleVO vo = localCache.getIfPresent(localKey); if (vo != null) return vo; String redisKey = "article:detail:" + id; vo = (ArticleVO) redisTemplate.opsForValue().get(redisKey); if (vo != null) { localCache.put(localKey, vo); // 回填本地缓存 return vo; } vo = articleMapper.selectDetailById(id); if (vo != null) { redisTemplate.opsForValue().set(redisKey, vo, 30, TimeUnit.MINUTES); localCache.put(localKey, vo); } return vo; }逻辑说明:先查本地缓存,命中直接返回;未命中查 Redis,命中后回填本地;再未命中查数据库,写回两级缓存。本地缓存用 Caffeine,设置最大条目和过期时间,避免内存无限增长。
参数说明:Redis 过期时间设 30 分钟,本地缓存设 5 分钟,这样即使文章更新,最坏情况 5 分钟后本地缓存失效,不会长期脏读。更新文章时要主动删除 Redis key,本地缓存靠短过期时间自然失效,这是权衡后的做法。
4.2 缓存与数据库的一致性处理
更新文章时,先更新数据库还是先删缓存,是个老问题。我的做法是:先更新数据库,再删除 Redis 缓存,本地缓存不主动删,靠短过期兜底。
@Transactional public void updateArticle(ArticleDTO dto) { articleMapper.updateById(dto); // 删除 Redis 缓存,下次查询重新加载 redisTemplate.delete("article:detail:" + dto.getId()); // 发送延迟消息,二次删除,防止并发读回填旧值 rocketMQTemplate.syncSend("article-cache-delete", dto.getId(), 1000); }逻辑说明:先落库再删缓存,配合延迟二次删除,能覆盖大部分并发场景。延迟消息 1 秒后再删一次,解决「读请求在删缓存后、更新前回填旧值」的窗口问题。
参数说明:延迟时间根据业务 QPS 调整,一般 500ms 到 1s。如果没有消息队列,可以用定时任务扫描变更表补偿,但实时性差一些。注意不要用「先删缓存再更新数据库」,那样并发下脏数据概率更高。
5. 避坑与排查:这套架构最容易翻车的五个地方
5.1 服务启动报「No spring.config.import property has been defined」
现象:SpringCloud 2021 之后版本启动服务,直接报错退出,提示缺少 config import。
原因:新版 SpringCloud 要求显式声明配置中心导入方式,不再自动读取 bootstrap.yml。
解决:在 application.yml 里加spring.config.import: optional:nacos:${spring.application.name}.yaml,或者引入spring-cloud-starter-bootstrap依赖恢复旧行为。我一般用前者,更符合新版本规范。
5.2 Feign 调用超时导致文章详情页空白
现象:文章详情页偶尔空白,日志里看到 Feign 超时异常,但评论服务本身正常。
原因:Feign 默认连接超时 1 秒、读超时 1 秒,评论服务在数据量大时响应超过 1 秒就触发熔断。
解决:在配置里调大超时,并给 Feign 配重试。feign.client.config.default.connectTimeout: 5000、readTimeout: 10000,同时确认熔断降级类返回了合理兜底数据,而不是抛异常。
5.3 Nacos 命名空间写错导致本地调不到测试服务
现象:本地启动服务后,网关转发 404,但服务在 Nacos 控制台能看到。
原因:本地服务注册到了 public 命名空间,而网关配置的是 blog-dev 命名空间,两者隔离,互相发现不了。
解决:检查每个服务的spring.cloud.nacos.discovery.namespace是否一致。建议把 namespace 抽到公共配置里,避免每个服务手写。本地调试时统一用同一个 namespace,不要混用。
5.4 Vue 打包后刷新页面 404
现象:开发环境正常,打包部署后刷新非首页路由,直接 404。
原因:Vue Router 默认 history 模式,刷新时浏览器向服务器请求真实路径,服务器没有对应资源。
解决:Nginx 配置try_files $uri $uri/ /index.html;,把所有未匹配路径回退到 index.html。如果用 hash 模式则没这个问题,但 URL 带 # 不好看。我一般用 history 模式加 Nginx 回退。
5.5 文章内容里的 HTML 被转义或 XSS
现象:富文本编辑器保存的文章,展示时标签被转义成文本,或者反过来被注入脚本。
原因:前端渲染时用了错误的转义方式,或者后端没有做白名单过滤。
解决:后端存储时保留原始 HTML,输出时用白名单过滤(如 Jsoup 的 Safelist),前端用v-html渲染过滤后的内容。不要简单用textContent,那样富文本全变纯文本;也不要直接v-html未过滤内容,那是 XSS 重灾区。
6. 从能跑到好用:接口压测与链路追踪的落地技巧
系统跑起来只是第一步,能不能扛住流量、出问题能不能快速定位,才是微服务博客和单体博客的真正差距。我一般在上线前做两件事:接口压测和链路追踪。
压测用 JMeter 或 wrk,重点压文章列表和详情两个接口。下面是我常用的 wrk 命令,配合 Lua 脚本模拟带 token 的请求:
# 压测文章详情接口,12 线程 400 连接,持续 60 秒 wrk -t12 -c400 -d60s --latency \ -s auth.lua \ http://localhost:8080/api/article/detail/1-- auth.lua:给每个请求加 JWT 头 wrk.method = "GET" wrk.headers["Authorization"] = "Bearer 你的测试token"逻辑说明:-t是线程数,-c是连接数,-d是持续时间,--latency输出延迟分布。auth.lua负责注入鉴权头,否则网关直接返回 401,压测没意义。
参数说明:线程数不要超过 CPU 核数太多,连接数根据目标 QPS 调整。看结果时重点关注 P99 延迟和错误率,如果 P99 超过 500ms,就要查是数据库慢查询还是缓存没命中。压测环境要和生产配置接近,否则数据没参考价值。
链路追踪用 SkyWalking 或 Sleuth + Zipkin。我倾向 SkyWalking,对代码零侵入,探针挂上就能看调用链。部署时注意采样率,生产环境设 10% 到 20% 即可,全采样会拖慢服务。看链路时重点找耗时最长的 span,通常是数据库查询或跨服务调用。
最后一个习惯:每次改完配置或加完服务,先本地跑一遍完整链路——注册、登录、发文章、看详情、发评论、搜索,六个动作走通再提交。微服务的坑大多不在单个服务里,而在服务之间的连接处。我踩过最惨的一次是网关路由配错,本地测试全过,上线后所有请求 404,回滚花了二十分钟。从那以后,我坚持每次上线前用脚本跑一遍端到端冒烟测试,这个习惯帮我省了无数次后悔药。
希望帮到你。
本文还有配套的精品资源,点击获取