简介:采用前后端分离模式,基于若依框架的微服务权限管理系统完整代码,面向熟悉Spring Boot但需要落地微服务治理的Java后端与前端开发者。项目以Nacos作为注册与配置中心,整合Redis认证、Sentinel限流熔断与Seata分布式事务,并提供Vue3 + Element Plus + Vite版本前端,适合学习企业级微服务架构与权限模型。资源包共677个文件,约132.24MB,主体为261个Java源码与89个Vue页面组件,另含75个JS脚本、46个XML配置、7个SQL脚本及Dockerfile、yml等部署文件,可支撑从后端逻辑到前端页面再到容器化部署的全链路阅读。辅助脚本如bat、sh可快速启动gateway、auth、system等模块,便于本地运行调试。目前已有3578人学习下载,对需要借鉴若依微服务版设计思路、改造权限与网关模块的开发者有直接参考价值。
1. 若依SpringCloud微服务脚手架:一个字,先把基座调通再谈业务
微服务改造这件事,绝大多数团队卡住的不是业务代码,而是基座没打通:注册中心、配置中心、限流熔断、分布式事务、前端权限,每一个单独拎出来都有教程,一旦凑到一起,版本冲突和隐性配置能把人折磨到怀疑人生。这套基于若依SpringCloud的脚手架,把Nacos(注册+配置)、Sentinel(限流+熔断)、Seata(分布式事务)和Vue3前端预先调通,业务开发只需要关注自己的模块。适合两类人:一是从单体转微服务、想找个能直接跑的框架起步的团队;二是想看清这几个组件在真实项目里怎么协作的开发者。本文不聊理论,只讲落地。
2. Nacos注册+配置双中心:服务发现与动态刷新怎么落地
2.1 为什么选 Nacos 而不是 Eureka + Config
微服务基座里,注册中心和配置中心通常是两个独立组件:Eureka负责服务发现,Spring Cloud Config负责配置管理。这套脚手架选Nacos,核心原因是它把两件事合并了,省掉一套独立配置中心的部署和运维成本。另一个现实因素是Nacos原生支持服务端的配置持久化、命名空间隔离和权限控制,这些在Eureka + Config组合里要么没有,要么得自己用数据库或Git仓库去补。
Nacos的服务发现走的是AP模型下的注册表机制,客户端定时拉取服务实例列表,同时通过心跳维持实例健康状态。配置中心走的是服务端推送加客户端长轮询的组合,配置变更后客户端能在几秒内感知到。这套机制在中小团队规模下非常够用,而且部署简单,standalone模式一条命令就能起来,不需要额外依赖数据库。
2.2 接入Nacos:bootstrap.yml 与 namespace 隔离
接入Nacos的第一步是引入依赖。每个微服务模块的pom里需要加上注册中心和配置中心的starter,这是这套脚手架的基础依赖,版本跟随SpringCloud Alibaba的BOM统一管理,不需要手写版本号。
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>依赖引入后,配置写在bootstrap.yml里。这里有个容易踩的坑:Nacos配置中心的地址必须放在bootstrap阶段加载,如果写在application.yml里,服务启动时配置中心还没初始化,动态刷新就不会生效。
spring: application: name: system-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: ->@RefreshScope @RestController @RequestMapping("/api/gateway") public class GatewayConfigController { @Value("${gateway.routes.timeout:3000}") private Integer timeout; @GetMapping("/timeout") public Integer getTimeout() { return timeout; } }注意@RefreshScope只对加了它的Bean生效,不会自动刷新所有配置项。比如数据源连接池、线程池这类在启动时创建且不销毁重建的Bean,修改配置后不会自动重建,需要配合@ConfigurationProperties或者专门的刷新逻辑。这套脚手架里公共配置抽成了shared-configs,所有服务共享同一个common.yaml,改一处全部刷新,适合放日志级别、公共开关这类配置。每个服务的独有配置则放在自己的dataId里,dataId命名规则是${spring.application.name}.${file-extension},如果文件后缀配成yaml,dataId就是system-service.yaml。
3. Sentinel流量治理:限流规则与熔断兜底配置
3.1 资源定义与控制台接入
Sentinel的核心概念是「资源」,它可以是一个方法、一个接口、一段代码块。这套脚手架里网关层和服务层都接入了Sentinel,网关负责入口流量控制,服务层负责接口级别保护。接入Sentinel需要引入依赖并配置控制台地址,控制台用来查看实时流量和推送规则。
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>控制台地址配置在application.yml里。要注意Sentinel客户端默认是懒加载,服务启动后如果不访问任何接口,控制台是看不到服务节点的,先随意请求一次接口再刷新控制台就能看到。
spring: cloud: sentinel: transport: dashboard: 127.0.0.1:8858 port: 8719 eager: true参数说明:dashboard是控制台地址,port是客户端与控制台通信的本地端口,如果被占用会自动+1,为避免端口漂移问题建议固定。eager设为true可以让客户端启动时立即注册到控制台,不用等第一次请求。
3.2 限流规则:QPS阈值与流控效果
限流规则定义在控制台上配置后默认存储在内存里,这一点后面避坑章节会细说。先看规则本身。Sentinel支持QPS限流和线程数限流两种模式,实际项目里QPS限流用得最多。
[ { "resource": "GET:/api/system/user/list", "limitApp": "default", "grade": 1, "count": 100, "strategy": 0, "controlBehavior": 0 } ]参数说明:grade=1表示按QPS限流,count=100表示每秒最多100次请求;strategy=0表示直接按资源本身限流,不涉及调用关系;controlBehavior=0是直接拒绝,1是Warm Up预热模式,2是排队等待。接口类限流我一般默认用0,预热适合秒杀这类冷启动场景,排队等待适合容忍延迟的异步场景。
代码层面也可以硬编码规则,适合规则不容易变化的场景,但生产环境建议用规则持久化,后面避坑章会给出接Nacos数据源的具体配置方式。
3.3 熔断降级:慢调用比例与兜底逻辑
熔断和限流是两回事。限流管的是流量大小,熔断管的是下游服务是否健康。这套脚手架里用的是慢调用比例熔断:当一段时间内接口调用超过指定耗时的比例达到阈值,熔断器打开,后续请求快速失败,不再等待下游超时。
[ { "resource": "POST:/api/order/create", "grade": 0, "count": 500, "timeWindow": 10, "minRequestAmount": 5, "slowRatioThreshold": 0.5 } ]参数说明:grade=0表示按慢调用比例熔断,count=500是最大RT阈值(毫秒),slowRatioThreshold=0.5表示超过500ms的请求占比达到50%就触发熔断,timeWindow=10表示熔断后10秒内快速失败,minRequestAmount=5是触发熔断的最小请求数,避免流量太小时误判。
熔断触发后需要有兜底逻辑,这套脚手架里的做法是在被保护的方法上写降级函数。兜底方法必须与原方法签名一致,返回值类型也要一致,否则会启动报错。
@SentinelResource(value = "createOrder", fallback = "createOrderFallback") public Result createOrder(OrderDTO dto) { // 正常业务逻辑 } public Result createOrderFallback(OrderDTO dto, Throwable ex) { return Result.error("服务繁忙,请稍后重试"); }注意fallback方法可以接收Throwable参数,这样能拿到熔断或异常的具体原因。我遇到过一个坑:fallback方法返回值类型和原方法不一致时,异常被吞掉,接口直接返回500空指针,排查了很久才定位到是降级函数签名问题。
4. Seata分布式事务:AT模式的回滚机制与落地配置
4.1 三种模式怎么选:AT、TCC、SAGA
Seata支持AT、TCC、SAGA三种事务模式。这套脚手架默认用AT模式,原因是侵入性最小:业务代码里只需加一个@GlobalTransactional注解,不需要像TCC那样写confirm和cancel两套方法。AT模式的原理是代理数据源,在业务SQL执行前记录数据快照到undo_log表,事务提交时根据undo_log的内容反向补偿。
三模式选型参考:
| 模式 | 侵入性 | 适用场景 | 主要成本 |
|---|---|---|---|
| AT | 低 | 单体拆微服务,业务代码不想大改 | 每库需建undo_log表,性能有一定损耗 |
| TCC | 高 | 对一致性要求极高,需要精细控制 | 业务需写try/confirm/cancel三接口 |
| SAGA | 中 | 长事务,链路长且允许最终一致 | 需维护状态机,补偿逻辑复杂 |
中小团队从单体重构上来的,无脑选AT,先把分布式事务跑通,后续业务确实复杂到需要精细控制再考虑TCC。
4.2 AT模式落地:undo_log表与全局事务注解
AT模式落地前,每个参与事务的数据库都要建undo_log表。这张表是Seata回滚的数据源,没建这张表,事务执行时不会报错,但回滚时会静默失败,数据就出现部分提交的情况,这是数据不一致最常见的隐患。
CREATE TABLE `undo_log` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `branch_id` BIGINT NOT NULL, `xid` VARCHAR(100) NOT NULL, `context` VARCHAR(128) NOT NULL, `rollback_info` LONGTEXT NOT NULL, `log_status` INT NOT NULL, `log_created` DATETIME NOT NULL, `log_modified` DATETIME NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;建好表后,在业务入口方法上声明全局事务。这个方法需要是被外部调用的,不能是同类内部调用,否则Spring AOP代理失效,事务完全不生效。
@Service public class OrderServiceImpl implements OrderService { @GlobalTransactional(rollbackFor = Exception.class) public void createOrder(OrderDTO dto) { orderMapper.insert(dto); accountClient.decrease(dto.getAccountId(), dto.getAmount()); inventoryClient.decrease(dto.getProductId(), dto.getCount()); } }代码说明:@GlobalTransactional是Seata的全局事务注解,rollbackFor=Exception.class表示任何异常都触发回滚。createOrder方法内部调用远程的扣减账户和扣减库存服务,如果第二步或第三步抛异常,前面已执行的本地SQL也会通过undo_log反向补偿回滚。
4.3 全局事务超时与回滚验证
Seata默认全局事务超时时间是60秒,如果业务链路里有慢SQL或远程调用超时超过这个值,事务会被强制回滚,接口还会返回一个不太容易看懂的异常。我一般会在application.yml里显式调大超时时间,但也不建议调太大,太长会导致全局锁持有时间过久,并发性能下降。
seata: enabled: true application-id: system-service tx-service-group: my_tx_group registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: public config: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: public service: vgroup-mapping: my_tx_group: default client: tm: default-global-transaction-timeout: 120000部署完成后一定要做一次回滚验证:在createOrder方法第二步后面故意抛一个RuntimeException,然后查数据库,确认订单、账户、库存三张表的数据都没有变化。这个验证动作我在每次环境迁移后都会强制做一遍。
5. 常见问题排查:路由、鉴权、跨域与规则失效
5.1 现象:服务之间Feign调用报错,Nacos控制台却能看见实例
原因有两个:一是namespace不一致,服务A在dev命名空间,服务B在public命名空间,Feign查询时从当前服务所在命名空间查不到对方实例列表;二是Nacos 2.x版本客户端向服务端上报实例走的是gRPC端口9848,防火墙只放通了8848没放通9848,注册表数据能上报但实例列表拉取失败。
解决:检查所有服务的spring.cloud.nacos.discovery.namespace配置,确保指向同一个命名空间ID;同时放通Nacos服务器的8848和9848端口。这个坑在换服务器环境时特别常见,因为本机部署通常不涉及防火墙问题。
5.2 现象:Sentinel控制台配置的限流规则,服务重启后全部丢失
原因:Sentinel默认规则存储在客户端内存中,控制台推给客户端的规则不会持久化到任何存储介质,服务重启即失效。
解决:引入Sentinel的Nacos数据源扩展,让规则先从Nacos配置中心拉取,控制台的修改通过Nacos作为中间桥梁同步。
spring: cloud: sentinel: datasource: flow: nacos: server-addr: 127.0.0.1:8848 >service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) service.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(error) } )代码说明:请求拦截器从localStorage读取token并写入请求头,响应拦截器统一处理401,token失效时清空缓存并跳回登录页。注意这里的token键名要和登录接口存储时保持一致,不一致的话请求拦截器永远拿不到token,这个低级错误排查起来很浪费时间。
5.5 现象:前端调用后端接口报CORS跨域,网关和前端都配置了跨域
原因:网关层和下游服务都配置了CORS响应头,浏览器收到两套Access-Control-Allow-Origin,或者网关拦截了预检请求后下游的CORS头没有透传,导致浏览器判定跨域失败。
解决:跨域只保留网关层处理,下游服务关闭CORS配置。网关的CORS配置需要显式允许预检请求,否则OPTIONS请求会被拦截无法继续。
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowed-origin-patterns: - "http://localhost:8080" allowed-methods: - GET - POST - PUT - DELETE allowed-headers: "*" allow-credentials: true参数说明:allowed-origin-patterns可以用通配符匹配前端地址,注意allow-credentials和allowed-origins不能同时使用通配符,否则浏览器会拒绝响应,这是Spring Cloud Gateway一个比较隐蔽的约束。
6. 进阶:本地联调顺序与Sentinel阈值调优经验
本地联调时,启动顺序有讲究。正确顺序是先基础设施后应用服务,具体如下表:
| 顺序 | 组件 | 启动方式 | 关键检查点 |
|---|---|---|---|
| 1 | Nacos | sh startup.sh -m standalone | 访问8848控制台正常 |
| 2 | Seata Server | sh seata-server.sh -p 8091 | 8091端口监听正常 |
| 3 | Redis / MySQL | 依本机环境 | 确认连接配置可访问 |
| 4 | 网关服务 | java -jar gateway-service.jar | 路由表加载日志无报错 |
| 5 | 业务微服务 | java -jar system-service.jar | 控制台能看到注册实例 |
| 6 | Vue3前端 | npm run dev | 浏览器能正常登录 |
这套顺序我每次拉新环境都强制走一遍,跳过任何一步都可能出现“服务注册了但调用404”这种难以归因的怪问题。有次我图省事先启动了业务服务,结果Seata还没起来,全局事务注解全部报错,吓得以为代码有问题,排查半天才发现是启动顺序反了。
Sentinel阈值调优方面,我的经验是从小到大逐步加压,不要一上来就拍脑袋定1000 QPS。先用一个较低的阈值比如100跑压测,观察执行耗时曲线和控制台的实时流量,如果系统CPU和内存都很健康,再逐步上调,每次调完观察半小时。限流阈值没有普适公式,不同业务接口的RT和依赖开销差异很大,控制台实时监控比理论估算值可靠得多。
最后说一个实际教训:别在本地改Nacos的namespace来玩隔离,本地就用public命名空间,环境隔离交给dev、test、prod的环境配置文件去区分。我有个同事把服务注册到自定义命名空间后,又改回public,结果配置中心里的shared-configs没跟着迁,所有服务启动报错找不到配置,花了半天才排查出来。从那以后我每次改完命名空间配置,都强制核对一遍discovery和config的namespace是否一致。希望帮到你。
本文还有配套的精品资源,点击获取