前段时间把一个“SpringBoot+Vue+SpringCloud学生荣誉证书管理系统”从零搭完,联调上线后回看整个项目,最值得沉淀的不是某个炫技接口,而是“为什么一个证书系统要上微服务”以及“上了微服务之后那些绕不开的坑”这两件事。这篇文章就把我的拆分思路、组件选型、核心链路实现、前端配合方案和踩坑记录完整写出来,给准备做类似项目的同学一份可以直接参考的实战笔记。
如果你正卡在“单体够用但想写微服务”“不知道服务怎么拆”“前后端联调时被跨域和权限搞崩”这些阶段,这篇应该能帮你省下不少时间。
1. 先泼盆冷水:一个证书管理系统真的需要微服务吗?
先说结论:不一定。但在动手前把这个问题想清楚,比直接敲代码重要得多。我见过太多人把微服务当成简历装饰品,结果把自己坑进分布式事务和链路排查的泥潭里。
1.1 什么时候微服务是伪需求
如果一个证书管理系统的实际使用场景是“单学校、几千学生、证书量一年几百份”,那我会明确告诉你:单体应用就是最优解。一个SpringBoot项目挂一台MySQL,前端Vue打包扔进Nginx,开发和运维成本都极低。这种场景硬上SpringCloud,只会带来三个后果:
- 每个服务都要单独配置、打包、部署、排查日志,开发效率肉眼可见地下降。
- 服务间调用引入的网络延迟和分布式事务问题,会让原本简单的“保存一条证书记录”变得复杂。
- 团队里只有一两个人的话,微服务的维护成本会直接吃掉技术红利。
很多人把“用了微服务”等同于“项目有含金量”,这是个误区。面试官更愿意看到的是“你清楚微服务的适用边界,并且能讲出取舍逻辑”。
1.2 哪些需求让这个系统值得走微服务
我做的这个系统之所以选择微服务,是因为需求里有几条硬性约束:
第一,证书业务的IO密度很高。教师批量上传证书文件、学生批量下载电子版证书、系统批量生成证书模板,这些操作集中在文件读写上,波动很大。如果和用户权限、业务审核混在一个服务里,一次大并发文件操作就可能拖垮整个应用的登录和业务接口。
第二,角色模型天然适合独立服务。学生、教师、管理员、教务审核人员,这四类角色的权限边界和业务域差异很大。学生只查自己的证书,教师负责上传和初审,管理员做模板配置和全局管理,审核人员做最终复核。把账号权限拆成独立服务,后续任何一个角色的逻辑变更都不会影响其他业务。
第三,将来对接外部系统的可能性很高。教务系统要同步获奖名单,学工系统要推送证书数据,档案系统要归档电子证书。以服务化的方式暴露接口,比在一坨单体代码里找入口要干净得多。
第四,证书编号需要全局唯一且不能错。这个点在单体里一个数据库自增就能解决,但拆了服务之后就必须用分布式锁或者号段模式来处理,这本身就是一个很好的技术深度展示点。
1.3 我的最终架构决策
综合以上,我最终确定的技术架构是:SpringBoot做业务服务底座,SpringCloud生态做服务治理,前端用Vue3全家桶,数据存储按服务拆分数据库实例。
服务规模控制得很克制:四个业务服务加一个网关,不上消息队列,不强行引入Seata分布式事务框架。能用最终一致性解决的,绝不上强事务;能用一个Redis锁解决的,绝不引入分布式协调中间件。这个“克制”本身也是项目经验的一部分。
提示:如果你的项目目的是毕业设计或简历项目,技术栈不必追求多,追求的是“每个组件都有它明确要解决的问题”。把这一点想清楚,后期答辩会非常从容。
2. 服务拆分与基础设施选型:不盲从主流清单
很多微服务教程一上来就是“注册中心、网关、配置中心、链路追踪”全家桶,但落到具体项目里,每一步都要做减法。下面是我的实际拆分和选型过程。
2.1 四张核心表撑起四个服务:服务边界到底怎么切
我的服务拆分完全围绕业务域来做,一共四个服务:
| 服务名 | 核心职责 | 关键数据表 | 独立数据库实例 |
|---|---|---|---|
| user-service | 账号、登录、角色权限、用户信息 | sys_user、sys_role、sys_user_role、sys_permission | user_db |
| certificate-service | 证书申请、审核、模板、编号发放、证书查询 | cert_apply、cert_record、cert_template、cert_review_log | cert_db |
| file-service | 文件上传、下载、格式校验、存储、访问地址生成 | file_record | file_db |
| gateway-service | 路由转发、统一鉴权、跨域处理、限流 | 不建业务表 | 无 |
每个服务都遵循一个原则:只操作自己数据库里的表。如果某个业务需要别的服务的数据,走接口调用,不直接跨库查。这个约束在前期会显得有点啰嗦,但后期好处非常明显——任何一个服务崩溃了,不会因为共享数据库而把整个系统拖死。
以证书申请为例,一次完整业务涉及三个服务:
- 教师在user-service里完成登录,拿到JWT。
- 教师调用certificate-service提交证书申请,certificate-service内部去调file-service,把已经上传的证书文件的信息一起关联起来。
- 审核人员复核通过后,certificate-service生成证书编号,把状态和编号写入cert_apply表,同时调用file-service生成电子证书预览地址。
三个服务各干各的事,数据流清晰,出了问题也能很快定位。
2.2 注册中心、网关、配置中心:我的选型落地表
SpringCloud生态的组件很多,但选型不能用“哪个火选哪个”,要看社区活跃度、维护状态和本地学习的难易程度。
我做的这套系统选型如下:
| 组件类型 | 我用的方案 | 淘汰的备选方案 | 选型理由 |
|---|---|---|---|
| 注册中心 | Nacos | Eureka、Consul、Zookeeper | Eureka已停止新功能开发维护,Consul的中文资料少,Nacos同时支持注册中心和配置中心,一套组件省一个运维节点 |
| 配置中心 | Nacos Config | Spring Cloud Config + Bus | 和注册中心用同一套,配置更新推送到服务,不用额外搭消息总线 |
| 网关 | Spring Cloud Gateway | Zuul 1.x | Zuul已经停止维护,Gateway基于WebFlux,性能更好,且和SpringCloud生态兼容性最佳 |
| 服务间调用 | OpenFeign | RestTemplate + LoadBalancer | Feign天然集成负载均衡,声明式调用代码可读性好 |
| 负载均衡 | Spring Cloud LoadBalancer | Ribbon | Ribbon进入维护模式,LoadBalancer是官方推荐替代 |
| 限流熔断 | Sentinel | Hystrix | Hystrix已停止维护,Sentinel的规则配置和Dashboard可视化都更好 |
这里面重点说一下Nacos。很多老教程还在用Eureka做注册中心,代码照着敲完也能跑,但版本一升级问题就来了。Eureka的旧版本在SpringCloud新版依赖管理里已经不再兼容,你在pom里硬加依赖会导致一堆类冲突。直接用Nacos,注册中心和配置中心一起解决,是当前最稳的路线。
网关我选用Spring Cloud Gateway而不是Zuul,还有一个原因是Gateway支持WebSocket转发,后续如果要加实时通知功能(比如证书审核通过后推送给学生),不需要改架构。
2.3 服务间通信的约定:统一封装比功能实现更重要
服务拆完之后,第一个要统一的是接口返回结构。如果几个服务各自返回不同的JSON格式,前端联调就是一场灾难。我在公共模块common-service里定义了一个统一的返回体:
@Data public class R<T> { private Integer code; private String message; private T data; public static <T> R<T> ok(T data) { R<T> r = new R<>(); r.code = 200; r.message = "success"; r.data = data; return r; } public static <T> R<T> error(Integer code, String message) { R<T> r = new R<>(); r.code = code; r.message = message; return r; } }所有服务接口都返回这个R对象,网关不做数据转换,前端Axios统一处理code字段。遇到404、500等异常,各服务用全局异常处理器转换成统一结构,避免前端一会儿拿到{code:500}一会儿拿到{timestamp:..., error:...}这种乱七八糟的格式。
Feign调用时的接口定义我也有一个约定:服务提供方除了提供Controller,还要在公共模块里暴露一个Client接口给调用方。比如certificate-service要调file-service时,会依赖file-service在公共模块里放的一个客户端接口:
@FeignClient(name = "file-service", path = "/api/file") public interface FileClient { @GetMapping("/info/{fileId}") R<FileInfoDTO> getFileInfo(@PathVariable("fileId") Long fileId); }这样做的最大好处是:调用方拿到的是声明式接口,内部走HTTP还是走别的协议都被Feign封装掉了,代码里完全看不出来“这是一个远程调用”,写起来和调本地方法没有区别。
3. 核心链路实测:从教师上传证书到学生在线查看
架构选型完成后,最核心的是把一条完整业务链路跑通。这条链路就是“教师上传证书 -> 提交申请 -> 审核人员复核 -> 系统生成编号 -> 学生查看和下载”。我在实测过程中遇到不少意外情况,下面把完整链路和关键处理逐一说明。
3.1 一次完整请求走过了哪些节点
假设教师上传一份“校级优秀学生干部”荣誉证书,实际请求经过的路径是:
- 浏览器请求
/api/user/login,user-service校验账号密码,签发JWT返回前端。 - 前端把JWT存到本地,之后每次Axios请求都在Header里带
Authorization: Bearer <token>。 - 请求
/api/cert/apply时,先到gateway-service,网关解析JWT校验身份和有效期,然后根据路由规则转发到certificate-service。 - certificate-service收到申请数据,里面包含学生ID、证书类型、图片文件ID、说明文字等。它先调用FileClient确认文件存在且格式正确,然后在cert_apply表插入一条状态为“待审核”的记录。
- 审核人员登录后,请求
/api/cert/review/list,同样通过网关转发到certificate-service,查询状态为“待审核”的申请。 - 审核人员点击“通过”,certificate-service执行一个包含“生成唯一证书编号 + 更新申请状态 + 写入证书记录表”的操作,这里就是我要重点讲的分布式锁场景。
- 学生登录后请求
/api/cert/my/list,certificate-service返回证书列表,同时附带file-service生成的临时访问URL。 - 前端拿到URL后展示证书图片或PDF预览,学生可以下载。
这条链路在单体应用里其实就是几个方法调用,但在微服务下每一步都涉及网络通信、权限校验、数据一致性。为了不让链路混乱,我给每个服务都加了统一的请求来源校验:网关放行之后,服务内部还会检查调用方身份是否合法,防止别人绕过网关直接打服务端口。
3.2 证书编号生成的分布式锁策略
证书编号是整个系统里最敏感的数据之一。教务处要求编号格式为“学校代码+年份+序号”,比如10001-2025-00001,序号每天从1开始递增,而且绝对不能重复。
在单体应用里,用数据库的自增ID或者唯一索引就能解决。但证书编号是按天重置的流水号,数据库自增做不到“按天重置”,所以我需要自己控制序号的生成逻辑。如果并发下两个教师同时提交证书,都去读取当前最大序号,就可能导致两个证书拿到同一个编号。这就是典型的并发竞争问题。
我的方案是用Redis分布式锁包裹“取当前序号 -> 加一 -> 写回”这个临界区:
public String generateCertNo(String schoolCode, LocalDate date) { String dateStr = date.toString().replace("-", ""); String lockKey = "cert:no:lock:" + schoolCode + ":" + dateStr; String seqKey = "cert:no:seq:" + schoolCode + ":" + dateStr; // 尝试获取锁,等待3秒,锁自动释放时间10秒 boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", Duration.ofSeconds(10)); if (!locked) { // 获取失败,休眠100ms后重试,最多重试30次 for (int i = 0; i < 30; i++) { Thread.sleep(100); if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(10))) { locked = true; break; } } if (!locked) { throw new BusinessException("系统繁忙,请稍后重试"); } } try { Long nextSeq = redisTemplate.opsForValue().increment(seqKey); // 每天首次生成时,需要把前一天的历史序号重置为1 // 简单做法:key里直接带日期,每天都用新的key,天然从1开始 return schoolCode + "-" + dateStr + "-" + String.format("%05d", nextSeq); } finally { // 释放锁前先确认锁还是自己的,防止误删别人的锁 String lockValue = redisTemplate.opsForValue().get(lockKey); if (lockValue != null) { redisTemplate.delete(lockKey); } } }这段代码有几个细节值得注意。第一,setIfAbsent加过期时间必须是原子的,不能先setnx再expire,否则服务器宕机就会造成死锁。第二,锁key里带上日期,天然做到了“按天重置”,不需要额外维护一个重置任务。第三,释放锁之前判断是不是自己的锁,这个在极端场景下很重要——如果一次请求执行时间超过了锁的自动过期时间,别人的请求可能已经拿到了同一把锁,此时你直接删除锁,就会把别人的锁给误删掉。
因为证书编号生成频率不高,我用的是最简单的Redis锁,没有引入Redisson。如果你追求更严谨的锁机制,可以在项目中加入Redisson,它内置了看门狗机制自动续期,能避免锁过期问题。但引入Redisson也意味着多一个依赖,需要权衡。
3.3 分布式事务能不上就不上
说到分布式事务,我见过不少人一上来就提Seata、可靠消息最终一致性。但在证书系统里,我认为多数场景都应该用“业务层面最终一致”来解决。
举一个实际例子:教师上传证书文件到file-service,file-service在file_record表里保存文件元数据并返回fileId。然后certificate-service收到申请,把fileId关联到cert_apply表。如果certificate-service保存失败,那么file-service里的文件就变成了“孤儿文件”,没有被任何业务记录引用。
这种情况要不要上分布式事务?我的答案是不用。因为“孤儿文件”不会对用户造成数据错误,只是占了缓存或存储空间。处理方案是定时任务兜底:每天凌晨扫描file_record表里超过24小时且没有被证书记录引用的文件,自动清理即可。这个方案简单、可靠、不引入额外的复杂框架。
真正强一致的场景其实只有一个:生成证书编号和插入证书记录必须同时成功或同时失败。但这两个操作都在certificate-service内部,使用的是同一数据库的同一事务管理器,所以这本质上是一个本地事务,根本不涉及分布式事务。
注意:判断是否需要分布式事务,不是看“涉及了几个服务”,而是看“一个业务操作跨了几个独立的数据库事务”。如果多个操作都在同一个服务内使用同一个数据库连接,那就只是一个普通事务,别给自己加戏。
4. Vue前端的微服务适配:不是换个后端那么简单
前端在整个微服务架构里承担着“统一入口体验”的角色。我前端选用的是Vue3 + Vite + Element Plus + Pinia,用Axios做HTTP请求。相比单体项目,微服务架构给前端带来的最大变化是:多个服务对应一套前端,路由、状态、权限、文件预览都需要做相应的架构适配。
4.1 登录认证在前后端之间的完整闭环
登录流程走的是JWT方案。用户提交账号密码到网关转发的登录接口,user-service校验通过后返回token。前端拿到token之后的处理逻辑如下:
- 把token存进Pinia和一个持久化存储(localStorage)。
- 创建Axios实例,在请求拦截器里统一加入token头。
- 在响应拦截器里判断HTTP 401状态,说明token失效或未登录,清空本地状态并跳转到登录页。
// axios实例的核心拦截逻辑 const service = axios.create({ baseURL: '/api', timeout: 15000 }) service.interceptors.request.use(config => { const token = useUserStore().token if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response?.status === 401) { const userStore = useUserStore() userStore.clearToken() window.location.href = '/login' } ElMessage.error(error.response?.data?.message || '网络异常') return Promise.reject(error) } )这里有一个微服务架构下的特殊细节:前端请求的baseURL不是任意一个服务的地址,而是统一指向网关。比如/api/user/login会由网关路由到user-service,/api/cert/my/list会由网关路由到certificate-service。前端根本不需要知道某个功能部署在哪个服务实例上,网关层把这件事全部处理掉了。
4.2 动态路由与权限菜单:不同角色看到不同界面
证书系统有四类角色,每类角色的菜单和操作权限都不一样。如果前端把所有路由都写死,只是通过按钮级权限来隐藏,那不同角色登录后依然可能通过修改前端代码来访问未授权页面,安全上有隐患。动态路由是更合理的方式。
实现方案是:登录后拿到该用户的角色权限码列表,然后根据权限码动态注册路由。
后端在user-service的登录接口里返回一个权限标识列表,比如:
{ "token": "xxx.yyy.zzz", "userInfo": { "id": 1001, "name": "张老师", "roles": ["teacher"], "permissions": ["cert:apply:add", "cert:review:list", "file:upload"] } }前端维护一张“权限码 -> 路由组件”的映射表,在全局路由守卫里动态添加路由:
function generateRoutes(permissions) { const routes = [] const asyncRouteMap = { 'cert:apply:add': () => import('@/views/cert/Apply.vue'), 'cert:review:list': () => import('@/views/cert/ReviewList.vue'), 'file:upload': () => import('@/views/file/Upload.vue') } for (const perm of permissions) { if (asyncRouteMap[perm]) { routes.push({ path: '/' + perm, name: perm, component: asyncRouteMap[perm], meta: { permission: perm } }) } } return routes }路由守卫里在每次进入路由前检查当前用户是否已经注册了动态路由,如果没有就先动态添加再继续跳转,这样刷新页面也不会丢失路由。后端权限校验才是真正的安全边界,前端动态路由更多是提升体验和避免无权限用户看到不该看到的菜单。
4.3 文件预览的那些坑:图片、PDF和视频
证书系统的核心资产是证书文件,格式通常有图片(JPG、PNG)和PDF两种,有些学校还会上传荣誉答辩或成果展示的视频。文件预览这块我踩了不少坑。
图片预览最简单,直接<img>标签加上file-service返回的临时访问URL就行。但PDF预览要特别注意:如果你直接用一个iframe指向PDF地址,浏览器可能会直接触发下载而不是预览,尤其在低版本浏览器里行为不一致。我自己用的是基于pdfjs-dist的预览方案,在后端返回文件流时设置正确的Content-Type: application/pdf和Content-Disposition: inline响应头,前端组件里再调用pdfjs把PDF渲染成Canvas展示,体验稳定很多。
视频预览的话,很多证书系统关联了现场颁奖视频,格式五花八门,尤其是使用M3U8切片格式的流媒体,前端不能用普通的<video>标签直接播放。比较省事的方式是使用开源播放器video.js配合对应的videojs-contrib-hls插件,在后端配置好M3U8地址后就能正常播放。这块配置的繁琐点在于CORS和鉴权——如果M3U8切片地址需要带token,播放器需要在请求头里动态添加凭证,不同播放器插件的写法差异很大。
经验:前端做文件预览前,一定要先搞清楚后端返回的Content-Type和缓存策略。我在联调时遇到过一次“明明换了一个PDF文件,但浏览器还是显示旧版本”的情况,原因就是file-service响应头没设置
Cache-Control: no-cache。加上这个响应头之后问题立刻消失。
5. 联调阶段踩得最深的四个坑
微服务项目联调和单体完全不是一个体感。单体项目只要本地起了后端,前端基本能直接对接;微服务要保证注册中心、网关、多个服务实例之间默契配合,任何一个环节出了问题,表现都是前端一片报错或者白屏。下面的四个问题是我实际踩坑后总结出来的,按遇到频率排序。
5.1 跨域问题:明明网关统一处理了,还是报跨域
我最初只在网关层配置了跨域规则,以为万事大吉,但前端联调时仍然报跨域错误。排查后发现问题出在“服务自己也响应了CORS头”导致的冲突。
具体表现是:前端请求先经过网关,网关加了CORS头成功转发;但服务端Filter又加了一组CORS头,结果浏览器收到两套Access-Control-Allow-Origin,直接判定冲突报错。
解决方式是统一在网关处理CORS,并取消各服务的跨域配置。网关里使用Spring Cloud Gateway自带的全局CORS配置即可:
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowed-origin-patterns: - "*" allowed-methods: - GET - POST - PUT - DELETE - OPTIONS allowed-headers: - "*" allow-credentials: true另外要注意:如果网关开了allow-credentials: true,allowed-origin-patterns就不能写成*,必须写明确的域名或使用allowed-origin-patterns,这两者不能同时乱配,很多人在这一步把配置抄错了。
5.2 Feign调用的超时与重试
Feign默认的超时时间很短,而file-service处理大文件时会比较慢,导致certificate-service调用file-service时频繁超时。更麻烦的是,Feign默认在连接失败时可能会触发重试,而重试的请求如果打到了处理进度不同的服务实例上,就可能导致重复操作。
我的调整方案是:
feign: client: config: default: connect-timeout: 5000 read-timeout: 15000同时关闭对GET请求以外的重试机制,因为POST请求的重试在服务端没做幂等处理时很容易产生重复数据。这一点在微服务项目里非常重要——超时重试都是隐形的风险,宁可把超时时间调大一点,也不要靠着“重试”来解决问题。
5.3 文件服务的内存溢出问题
file-service用的框架是SpringBoot默认的Tomcat容器。上传大文件时,如果直接调用file.transferTo()并配合CommonsMultipartResolver,在高并发下很容易遇到内存溢出。原因在于默认配置会把上传文件先读到内存,再写磁盘。我调整了上传配置,把文件先落临时目录再转存到对象存储:
spring: servlet: multipart: max-file-size: 512MB max-request-size: 512MB file-size-threshold: 10MB location: /data/tmp关键是file-size-threshold: 10MB这个配置:超过10MB的文件直接写入磁盘临时文件,而不是压在内存里。这样上传512MB的大视频也不会撑爆堆内存。后来我又把文件的最终存储地方改成了MinIO对象存储,与本地磁盘解耦,扩展性更好,file-service只负责文件流的中转和元数据管理。
5.4 Nacos配置更新后服务不生效
我把文件上传大小、超时时间、数据库连接池参数等配置都放进了Nacos。第一次修改Nacos里的配置后,服务端日志显示配置已发布,但服务跑了半天还是老配置。后来发现原因很简单:配置没有配置自动刷新。
Spring Cloud Nacos Config支持配置动态刷新,但前提是配置类里要加@RefreshScope注解,或者使用@ConfigurationProperties配合配置文件里的refresh: true。我一开始用@Value读取配置,这种注入方式默认不会自动刷新。改成下面的写法后再发布配置,服务才真正热更新:
@RefreshScope @Component @ConfigurationProperties(prefix = "cert") public class CertProperties { private String schoolCode; private Integer maxUploadSize; // getter/setter... }这是微服务开发中很容易被忽略的细节。如果不在类上标注@RefreshScope,Nacos管得再好也没有用。
6. 部署、测试与项目答辩必问点
项目开发完成只是开始,部署上线和答辩准备同样重要。微服务项目的部署方式和单体差异很大,这里把我实际用的部署流程和常见问题整理出来。
6.1 微服务项目的启动顺序与打包细节
微服务不能“一个脚本全启动”,服务启动有严格的依赖顺序。我之前图省事把四个服务一起启动,结果user-service还没注册到Nacos,gateway就尝试转发请求,直接报“找不到实例”。正确的启动顺序是:
- 先启动基础设施:MySQL、Redis、Nacos、MinIO。
- 启动Nacos后,确认Nacos控制台能看到服务列表,再启动业务服务。
- 业务服务启动顺序:user-service先启动(因为其他服务都依赖它的鉴权),再启动file-service,再启动certificate-service。
- 最后启动gateway-service。
打包时每个服务独立打成jar包,命令示例:
mvn clean package -DskipTests启动服务时注意指定环境配置:
java -jar gateway-service.jar --spring.profiles.active=prod如果服务器内存有限(比如学生项目的云服务器只有2G内存),启动全部服务会非常吃力。这种情况下可以做两件事:给各服务设置JVM内存参数,不要默认最大堆内存;或者在同一台机器上用Docker Compose管理服务,每个服务限制内存上限。我实际使用下来,一台4G内存的云服务器可以稳定跑完这一整套微服务。
6.2 项目答辩和面试经常被追问的问题
这段时间被问得最多的问题集中在这几类,提前准备好,答辩时能省掉很多临场“呃”。
第一个问题:为什么用微服务,只有几个功能而已?
我的回答重点是讲业务约束,而不是讲技术时髦。证书系统涉及文件IO高并发、多角色权限隔离、未来对接教务和学工系统,这些需求的本质决定了单体架构在后续维护和扩展中会越来越痛苦。同时要承认微服务带来的复杂度,说明自己在哪些地方做了克制性设计(没有把事务框架和消息队列全部引进来)。
第二个问题:分布式事务怎么处理的?
我的回答分三层:先说明系统里大部分操作封装在服务内部,是本地事务;再说明跨服务的文件留痕问题采用定时核对和清理机制,保证最终一致;最后说明确实需要强一致的场景(证书编号和记录插入)在同一个服务内,用数据库本地事务解决。没有盲目上Seata这套重型方案。
第三个问题:网关做了哪些事情?
路由转发、统一鉴权(解析和校验JWT)、跨域处理、请求限流(Sentinel)、部分接口的灰度策略。回答时要给具体细节,比如JWT在网关解析后会把用户ID放入请求头传给下游,服务内部不再解析token,减少重复计算。
第四个问题:服务挂了怎么办?
注册中心配合服务实例的心跳机制,某个实例掉线后Nacos会自动摘除不可用实例,网关转发时会自动避开它。配合Sentinel的熔断降级,file-service异常时,certificate-service可以快速失败返回提示,不会一直阻塞等待拖垮自己。
第五个问题:怎么做测试和监控?
单测每个服务独立编写,接口联调用Postman。监控方面项目上线初期用了SkyWalking做链路追踪,能清晰看到一次证书申请请求在网关、certificate-service、file-service之间的耗时分配。这个工具对微服务排查问题帮助很大,强烈建议接入。
最后再分享一个我自己的体会:微服务项目真正难的不是把SpringCloud的注解用起来,而是在拆服务之后还能保持业务逻辑的完整性和排查问题的能力。每次遇到Bug,先看“请求到了哪一层”,再想“这一层需要谁来配合”,远比一头扎进日志里大海捞针更高效。如果这篇笔记里的任何一个坑能帮你少加一次班,我就觉得把这些过程写下来值了。