Spring Cloud微服务脚手架:Nacos、Sentinel、Seata落地与避坑指南 简介一套基于若依框架的微服务实战工程采用前后端分离模式后端整合Spring Cloud Alibaba生态中的注册与配置中心、限流熔断、分布式事务等组件前端则提供Vue3Element PlusVite技术栈版本工程。压缩包共677个文件大小约132.24MB以261个Java源文件、89个Vue组件、75个JavaScript脚本、7个SQL脚本为主另有较多XML/YAML配置、Dockerfile容器化文件及批处理启动脚本覆盖微服务业务代码、页面交互、数据库初始化和一键部署配置。已有3578人学习下载。读者可获得包含网关、认证、系统管理、文件监控等模块的完整前后端分离项目内置注册与配置管理、流量控制、分布式事务、权限认证等集成方案配套模块化目录结构和多个启动脚本便于快速搭建开发环境。适合具备Java与Spring Cloud基础的开发者用于学习微服务治理也可作为企业级项目的二次开发基座。1. 从单体后台到微服务全家桶这个脚手架替你踩掉了多少坑一个后台系统如果只是用户、角色、菜单加几个业务模块一般单体 Spring Boot 就够用。但一旦要有多个团队并行开发、要按服务独立扩缩容就得把单体拆成微服务。这个项目就是基于那套被大量二次开发的开源后台管理脚手架改造成的 Spring Cloud 微服务版服务注册与配置走 Nacos限流熔断走 Sentinel分布式事务走 Seata前端用 Vue3 重写。说白了它把后台管理里最常见的“用户-角色-菜单-字典-定时任务”整套业务从单体搬运到了微服务架构还补上了网关、认证、监控这些基础设施。适合谁适合正打算把单体后台拆成微服务或者想在一套真实工程里看 Nacos、Sentinel、Seata 怎么落地的开发者。拆完你会发现90% 的旧代码不用重写真正重做的其实是基础设施和事务边界。2. 基础设施拆解Nacos 双中心与网关统一鉴权的落地细节2.1 服务拆分六个子服务的边界怎么定我拿到这套工程第一件事就是数模块结构。它没有做成那种几十个服务的“微服务全家桶”而是只拆了六个核心服务再加一个网关网关服务只管路由和鉴权过滤认证服务负责登录、登出和 token 生成系统服务管用户、角色、菜单、字典这类基础数据文件服务管上传下载定时任务服务管 Job监控服务负责聚合日志和指标。拆分的思路很明确——按数据消费方式拆而不是按页面拆。用户数据会被订单、工作流、文件多个服务引用所以单独拆成系统服务认证是所有人的前置依赖所以单独拆出来定时任务是独立的执行周期和用户请求没有强耦合也单独拆。这种拆分边界对二次开发非常友好。新来的同学不需要理解几十个服务之间的关系只看系统服务就能找到权限相关的东西。目录结构大致是这样scloud-gateway/ 网关服务端口 8080 scloud-auth/ 认证服务端口 9200 scloud-system/ 系统管理服务端口 9201 scloud-file/ 文件服务端口 9300 scloud-job/ 定时任务服务端口 9301 scloud-monitor/ 监控服务端口 9400 scloud-common/ 公共模块包含 core、security、redis、log scloud-api/ Feign 接口定义模块 scloud-ui/ Vue3 前端工程注意它把 Feign 接口单独抽到了scloud-api模块而不是让服务之间直接互相依赖接口的 jar。这样做的目的是避免服务间强依赖A 服务调用 B 服务时只依赖 B 在scloud-api里定义的接口签名B 服务内部重构不影响 A 编译。这是很多微服务项目容易忽略的点等换了框架或者接口参数一变服务间的 jar 依赖就会变成一场噩梦。2.2 Nacos 双中心注册与配置的装载顺序Nacos 在这里同时承担注册中心和配置中心两个角色。注册中心比较直观服务启动时把实例信息注册上去网关和 Feign 都通过服务名来找实例。配置中心则要更谨慎整个团队的公共同步开关、数据库连接串、Redis 地址全部下发到 Nacos而不是写死在本地。本地只保留一份 bootstrap.yml里面只放“怎么找到 Nacos”的信息。# bootstrap.yml每个服务一份注意 application.name 不同 spring: application: name: scloud-system cloud: nacos: server-addr: 127.0.0.1:8848 username: nacos password: nacos namespace: dev group: DEFAULT_GROUP config: file-extension: yaml shared-configs: ->spring: cloud: gateway: routes: - id: scloud-auth uri: lb://scloud-auth predicates: - Path/auth/** filters: - StripPrefix1 - id: scloud-system uri: lb://scloud-system predicates: - Path/system/** filters: - StripPrefix1uri: lb://scloud-auth表示走 Nacos 负载均衡找到认证服务的实例lb前缀是 Spring Cloud LoadBalancer 的固定写法。StripPrefix1是去掉路径里的第一段再转发比如请求/auth/login转发到认证服务时实际路径变成/login服务端不用关心外部路由前缀。鉴权统一放在网关的全局过滤器里不散落到各个业务服务。代码长这样Component public class AuthGlobalFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); String token request.getHeaders().getFirst(Authorization); String path request.getURI().getPath(); // 白名单直接放行比如登录、验证码、注册 if (isWhitelist(path)) { return chain.filter(exchange); } // 解析 JWT从 token 中取出 userId 和 userKey Claims claims JwtUtil.parseToken(token.replace(Bearer , )); if (claims null) { return unauthorized(exchange); } // 把用户身份写进 header转发给下游服务 ServerHttpRequest mutated request.mutate() .header(scloud-user-id, claims.get(userId).toString()) .header(scloud-user-key, claims.get(userKey).toString()) .build(); return chain.filter(exchange.mutate().request(mutated).build()); } Override public int getOrder() { return -100; // 优先级最高最先执行 } }这里有两个设计细节值得关注。第一网关解析完 token 后通过 header 向下游传递用户身份下游服务里不用再解析 JWT直接从 header 取scloud-user-id就能拿到当前用户性能和代码简洁度都好很多。第二getOrder()返回负数保证它是第一批执行的过滤器如果还有验签、日志类的过滤器可以按 order 值排序值越小越先执行。3. 认证与权限链路从登录接口到按钮级权限的完整闭环3.1 登录接口token 生成不是单纯加密认证服务的登录接口是整个系统里被调用最多的接口。用户输入用户名、密码、验证码认证服务做四件事校验验证码、查用户表并比对 BCrypt 密码、生成 JWT、把登录用户的信息写入 Redis。这里的 token 不是简单的加密串而是带业务含义的凭证。public String login(LoginBody loginBody) { // 1. 校验验证码防止暴力破解失败直接抛异常 validateCaptcha(loginBody.getUuid(), loginBody.getCode()); // 2. 根据用户名查数据库密码用 BCrypt 比对不能明文存储 SysUser user userService.selectUserByUserName(loginBody.getUsername()); if (user null || !BCrypt.checkpw(loginBody.getPassword(), user.getPassword())) { throw new ServiceException(用户名或密码错误); } // 3. 生成 JWTclaims 里只放 userId、userKey、登录时间不放密码 String token JwtUtil.createToken( user.getUserId().toString(), user.getUserName(), UUID.randomUUID().toString().replace(-, ) ); // 4. 用户完整信息写入 Rediskey 为 login_tokens:userId后续权限判断直接读缓存 LoginUser loginUser buildLoginUser(user); redisCache.setCacheObject(login_tokens: user.getUserId(), loginUser, JwtUtil.getExpireTime(), TimeUnit.MINUTES); return token; }参数说明userKey是每次登录生成的唯一标识作用是让用户在多端登录时能有不同的 token踢人下线时只删对应 key。Redis 里存的是完整的LoginUser对象包含用户的权限标识集合这样后续每个接口的权限校验都不用回查数据库直接走缓存。token 过期时间在 Nacos 配置里统一管理生产环境一般设 30 分钟配合前端定时刷新机制使用。3.2 上下文透传网关把用户身份塞进 Header登录之后前端每次请求都会带上Authorization头。网关解析 token 后将用户信息透传到下游下游服务再把这个身份信息放到 ThreadLocal业务代码里随处可取。透传之后的下游拦截器是这样工作的public class SecurityInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 从网关传过来的 header 中取用户身份不再解析 JWT String userId request.getHeader(scloud-user-id); String userKey request.getHeader(scloud-user-key); // 构造 LoginUser 放入 ThreadLocal业务代码里用 SecurityUtils.getUserId() 获取 LoginUser loginUser new LoginUser(); loginUser.setUserId(Long.valueOf(userId)); loginUser.setUserKey(userKey); SecurityContextHolder.set(loginUser); return true; } Override public void afterCompletion(...) { // 请求结束必须清理 ThreadLocal避免线程池复用导致数据串号 SecurityContextHolder.clear(); } }这里有个必须重视的问题下游服务不能盲信任 header。如果某个服务直接暴露在内网外部请求绕过网关带着假 header 打到服务上就能伪造任意用户身份。常见的做法有两种一是把业务服务绑定内网 IP只允许网关 IP 访问二是在 header 值里加签名比如网关把 userId 和 userKey 拼接后用约定密钥做 HMAC下游验签通过才信任。我在拆解这个项目时发现它的做法是前者加了一层所有非网关来源直接拒绝Nacos 注册的实例全部走内网地址。3.3 权限模型数据库表设计与注解校验权限部分沿用了经典 RBAC 模型五张核心表表名作用关键字段sys_user用户表user_id, user_name, password, statussys_role角色表role_id, role_name, role_key, statussys_menu菜单/按钮表menu_id, parent_id, perms, menu_typesys_user_role用户-角色关联user_id, role_idsys_role_menu角色-菜单关联role_id, menu_id菜单表是这张模型里最灵活的表。menu_type区分目录、菜单、按钮三类节点按钮也作为一条菜单数据存在perms字段存权限标识字符串比如system:user:add。用户登录后系统服务一次性查出该用户所有角色的权限标识拼成集合放到 Redis。后端接口鉴权直接用注解PreAuthorize(ss.hasPermi(system:user:list)) GetMapping(/list) public TableDataInfo list(SysUserQuery query) { // 只有具备 system:user:list 权限的用户才能执行 return userService.selectUserPage(query); }ss.hasPermi是自定义的权限校验 Bean它做的事情很简单从当前 ThreadLocal 的 LoginUser 里取权限集合判断是否包含system:user:list。超管角色一般会配置*:*:*通配符所有权限校验直接放行。菜单变动时管理员只需要在系统服务里更新角色菜单关系再清掉对应用户的 Redis 缓存权限就能及时刷新生效不需要重新登录。4. 高可用三件套实战Sentinel 限流熔断与 Seata 分布式事务4.1 Sentinel 接入从监控到限流熔断Sentinel 在这套工程里管两件事一是接口限流防止突发流量把服务打挂二是服务间调用的熔断降级防止下游服务变慢后线程池被拖垮。接入配置比较直接spring: cloud: sentinel: transport: dashboard: 127.0.0.1:8858 port: 8719 eager: truedashboard是 Sentinel 控制台地址port是客户端与控制台通信的端口每台机器不能重复eager: true表示项目启动时就主动注册到控制台不需要等第一次被访问才出现。资源定义用注解方式写在业务方法上SentinelResource( value createOrder, blockHandler createOrderBlock, fallback createOrderFallback ) public Order createOrder(OrderCreateDTO dto) { // 创建订单的业务逻辑 return orderMapper.insert(dto); } public Order createOrderBlock(OrderCreateDTO dto, BlockException e) { // 限流触发时返回提示不抛异常给前端 throw new ServiceException(当前下单人数过多请稍后重试); } public Order createOrderFallback(OrderCreateDTO dto, Throwable e) { // 业务异常降级 throw new ServiceException(下单失败请稍后重试); }这里有两个反射层面的硬性要求blockHandler方法必须和原方法放在同一个类中且参数列表要完全一致只是多一个BlockException参数fallback同理多一个Throwable参数。写错一个参数类型运行时反射找不到方法Sentinel 会静默跳过限流逻辑接口看起来正常但规则完全不生效——这是接入 Sentinel 最常见也最坑的点。4.2 服务间调用限流Feign 场景的 resource 命名服务内部调用也要定义资源粒度。Feign 调用另一个微服务时不能只在被调方接口上定义资源调用方也要做一层保护。比如系统服务调用文件服务上传文件文件服务接口定义了file_upload资源但系统服务同时要限制“单个服务每秒最多调用多少次”避免定时任务批量处理时把文件服务冲垮。这套工程里对 Feign 接口的资源命名约定是服务名:接口路径比如scloud-file:/upload保证资源名全局唯一。这样在 Sentinel 控制台按资源名配置规则时一眼就能看出是哪个服务的哪个接口。热点参数限流也可以做成接口维度比如针对商品详情接口的userId参数做单用户限流防止某个恶意用户高频刷接口把缓存打穿。4.3 Seata AT 模式跨服务下单的事务边界分布式事务是微服务项目里最让人头大的部分。这套工程选的 Seata AT 模式核心特点是业务代码无侵入正常写本地事务Seata 通过代理数据源拦截 SQL自动生成回滚镜像。典型的场景是下单订单服务创建订单库存服务扣库存用户服务扣余额三个服务任何一步失败都要回滚。先看 Seata 的配置spring: cloud: alibaba: seata: tx-service-group: scloud-tx-group seata: registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: dev cluster: default config: type: nacostx-service-group是事务分组名所有参与分布式事务的服务必须配成同一个值Seata Server 端也要在配置里注册同一个分组才能正确找到事务协调器。业务入口加上一个注解GlobalTransactional(rollbackFor Exception.class, timeoutMills 30000) public void createOrderWithItems(Long userId, Long productId, Integer count) { // 1. 远程调用库存服务扣库存 inventoryFeignClient.deductStock(productId, count); // 2. 本地事务写订单表 orderMapper.insert(order); // 3. 远程调用用户服务扣余额 accountFeignClient.deductBalance(userId, order.getAmount()); }AT 模式保证回滚的关键在于三件事入口方法必须有GlobalTransactional注解它把整个调用链注册成一个全局事务被调用的库存服务、用户服务各自的数据源必须被 Seata 代理否则它们只执行本地事务不会注册分支事务全局事务也管不到它们每个服务的数据库都要有undo_log表回滚时 Seata 靠它恢复数据。timeoutMills参数要根据实际链路设置我一般会设置为远程调用总耗时的两倍以上否则全局事务先超时释放锁后面的调用再失败就回滚不了了。5. 避坑与排查配置不生效、规则未触发、事务不回滚的四个案例5.1 Nacos 配置改完不生效现象在 Nacos 控制台把数据库连接串改成了新地址点发布之后服务重启发现还连的旧库。或者同事说改了某个开关配置服务报了 “配置不存在” 却一切照常运行。原因八成是 namespace 没对上。同一个 Nacos 集群里 dev、test、prod 各占一个 namespace服务启动读的是 dev你把配置发到了 test自然读取不到。还有一部分情况是服务根本没走 Nacos 配置而是读了本地 application.yml 里的旧配置——bootstrap.yml 里没写spring.cloud.nacos.config.server-addr或者服务没引入spring-cloud-starter-alibaba-nacos-config依赖。解决先用 curl 直接请求 Nacos Open API确认配置是否真的存在curl -X GET http://127.0.0.1:8848/nacos/v1/cs/configs?dataIdscloud-system.yamlgroupDEFAULT_GROUPtenantdev返回内容是配置文本说明 dataId 和 tenant 配对没问题返回空则先检查配置发布了没有。确认配置存在后再看服务日志里启动时的Located property source列表有没有出现对应的 dataId。从这以后我都强制在备忘里记一条改配置之前先核对服务启动日志左下角加载的 namespace 和 dataId。5.2 Sentinel 规则重启即丢现象在 Sentinel 控制台手动配了限流规则刚配上还生效服务一重启规则就没了。或者压测时发现规则根本没触发流量监控一片空白。原因Sentinel 控制台手动配置的规则默认只保存在控制台内存里客户端通过 transport 模块定时拉取并没有做持久化。服务重启后客户端内存里的规则清零只能等控制台重新下发——如果控制台也是刚启动规则就会彻底消失。加上规则没触发的那部分多半是SentinelResource的 value 和控制台配置的资源名不匹配比如注解写的是createOrder控制台配的资源名是create-order完全对不上。解决规则不要指望靠控制台手点用代码在服务启动时加载或者推送到 Nacos 做持久化。用代码加载最简单直接PostConstruct public void initFlowRules() { ListFlowRule rules new ArrayList(); FlowRule rule new FlowRule(); rule.setResource(createOrder); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(100); rule.setLimitApp(default); rules.add(rule); FlowRuleManager.loadRules(rules); }setCount(100)表示该资源每秒最多 100 QPSsetGrade也可以改成FLOW_GRADE_THREAD按并发线程数限流。参数根据业务容量评估去调不要拍脑袋写 1 万压测数据才是依据。5.3 Seata 全局事务静默失效现象下单接口里库存服务扣减成功了订单也写进去了但账户服务扣余额抛了异常调用链直接返回失败。查数据库却发现库存和订单都没回滚。原因这是 Seata 最常见的坑链路有三个方面要排查。第一被调用的库存服务和账户服务的数据源没被 Seata 代理它们执行的 SQL 不会生成 undo_log全局事务根本感知不到这些分支第二GlobalTransactional写在了 Feign 被调方方法上而不是入口方法事务的起点错了第三参与事务的服务没有建undo_log表或者事务分组名和 Seata Server 配置不一致导致分支事务注册不上去。解决检查每个服务的数据源代理配置确认是DataSourceProxy包装过的数据源核对所有服务的spring.cloud.alibaba.seata.tx-service-group与 Seata Server 端配置一致初始化脚本里建好 undo_log 表。有个排查技巧看 Seata Server 日志里有没有收到分支事务注册请求如果全程没有一条分支注册记录说明被调方根本没接进来。5.4 网关绕过导致伪造用户身份现象局域网内直接请求系统服务的 IP 加端口构造一个scloud-user-id请求头就能以任意用户身份调用业务接口。原因网关透传用户信息是通过 header 实现的但业务服务本身没有校验请求来源是否真的经过网关。生产环境如果把业务服务端口暴露在办公网或公网等于给攻击者留了一扇后门。解决最简单的方案是业务服务只绑定内网 IP安全组规则只放行网关所在机器访问进阶做法是在网关生成 header 时附带签名把 userId、userKey 和时间戳拼起来用约定的密钥做 HMAC-SHA256业务服务端校验签名有效才信任这个 header。我在这个项目里用的签名方案。从那以后我每次新加服务第一件事就是确认端口没暴露到外网这个习惯救了我好几次。6. 二次开发新增一个微服务与业务拆分的最小闭环6.1 新增微服务的最小改动清单在这个骨架下新增一个业务服务不需要从零搭建。复制system模块改一改即可按顺序走以下七步步骤改动点说明1复制工程目录改pom.xml的 artifactId 和端口端口在 Nacos 上统一管理2新建数据库执行初始化 SQL只建当前服务需要用的表3在 Nacos 新建scloud-xxx.yaml包含数据源、Redis、日志级别4修改 bootstrap.yml 的 application.name必须和 Nacos 里的 dataId 前缀一致5在网关配置里加一条路由指定 Path 前缀和 lb 服务名6在认证服务的白名单里加接口路径不需要登录就能访问的接口才加7启动服务从网关转发请求验证不要直连服务验证6.2 遗留单体业务如何拆分老业务拆微服务时核心不是代码量而是数据边界。经验是多表 join 查询强关联的数据尽量放在同一个服务里不要为了微服务而硬拆订单、库存、用户这类跨域写操作频繁的优先拆出来报表查询类业务对实时性要求不高可以先留着不动。我个人的拆分习惯是先画一版数据域依赖图标出哪些表被三个以上模块引用这些表所在的服务要优先确定再从网关入口梳理一遍接口清单确认哪些接口会被多个服务调用最后才动代码。拆完以后每个服务独立验证接口能通、权限能过、事务能回滚再合并到主干。这个项目给我最大的收获是把 Nacos、Sentinel、Seata 这三样东西从“听说过”变成了“踩过坑知道怎么排”。从那以后我每次做微服务改造都强制先走一遍环境检查namespace 对不对、数据源代理没代理、资源名一致不一致这三步能解决八成的玄学问题。希望帮到你。本文还有配套的精品资源点击获取