news 2026/10/13 3:47:00

微服务骨架搭建实践:从服务拆分到Nacos注册与Feign调用的关键问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微服务骨架搭建实践:从服务拆分到Nacos注册与Feign调用的关键问题

day-06 是我真正把微服务骨架搭通的一天。前五天一直在看概念、画架构图,第六天终于有四个服务注册到了同一个注册中心,A 服务能通过服务名调用 B 服务并正常返回数据。那种感觉就像是把一堆零件组装成了能发动的机器,虽然还不太稳,但至少不再是散件。

这篇笔记是这个系列里比较关键的一天,它把我从"看微服务"拉到了"做微服务"的状态。我想重点记录这几件事:服务到底怎么拆才不会后悔、注册中心有哪些容易忽略的坑、Feign 调用前必须配好的参数、网关和配置中心怎么配合、分布式事务在什么情况下才需要认真考虑。适合正在从单体走向微服务,或者刚把微服务跑起来但被各种隐性问题困扰的开发者参考。

如果你已经有微服务实战经验,这篇笔记也能提供一些对照——我踩过的坑,大概率也是你会在某个阶段遇到的。

1. day-06 的起点:为什么我把"最快迁移"改成了"业务边界优先"

1.1 单体系统的三个单点,逼我动手

先说我最初的想法。拿到一个已有单体应用时,第一反应是直接把接口按模块拆出来,尽快迁移,能跑就行。但真正动手时才发现,如果只是把代码拆开、数据库还放一起,很多单体阶段的问题根本没有解决,只是让调用从本地方法变成了远程请求。

单体系统最明显的问题是发布互相牵扯。一个支付模块改了两行代码,就可能导致整个应用需要回归测试,因为大家都在同一个进程里,类路径上任何改动都可能产生全局影响。第二个问题是资源单点,所有模块共享同一个数据库连接池,某个慢查询把连接池占满,所有模块一起变慢。第三个问题更隐蔽,是故障扩散,一个接口发生内存溢出,整个进程都会被拖垮,其他模块连日志都来不及打出来。

这三个痛点足够让我下决心拆。但怎么拆,直接决定了后面每天的效率。

1.2 拆分的两条底线:数据边界和认知边界

我第一天画的拆分方案是典型的"伪微服务":把用户接口、订单接口、支付接口分别拆成三个服务,但数据库还是同一个。这个方案在团队里讨论时被否了,原因是数据库不拆分的话,服务之间为了查数据往往直接连主库表,边界很快会被突破,最后你会得到三个服务和一座被所有人踩着的数据库。

所以第一条底线是数据边界优先。一个服务负责一组数据,数据表的读写权基本只属于这个服务,别的服务要数据只能走接口。这样拆分出的服务才有独立的故障和扩展能力。

第二条底线是认知边界。服务的粒度应当和你的脑子匹配,一个人要能清楚说出这个服务在做什么、依赖谁、被谁依赖。按照这条标准,小团队初期拆 2 到 4 个服务比较合适,而不是一上来就拆十几个"微服务"。服务多了之后,光是梳理调用链路、处理配置环境,就要花掉大量时间。

1.3 我在模拟项目 X 里的拆分结果

为了后面演示,我把一个模拟电商系统拆成了四个服务,划分如下:

服务核心职责数据域主要对外接口
user-service用户注册、登录、资料管理用户表/api/user/**
order-service创建订单、订单查询订单表/api/order/**
product-service商品信息、库存管理商品表、库存表/api/product/**
pay-service支付发起、支付回调支付流水表/api/pay/**

当时有人问我为什么库存不放 order-service,而要放在 product-service。我的理由是"库存量是商品信息的一部分",如果订单服务直接改库存表,库存变化就被订单服务的数据规则控制了,商品侧就无法对库存做独立的上架、盘点等操作。这个决定在后面做下单扣库存时多了一些跨服务调用,但也让两个业务域的边界清楚了很多。

这个拆分结果不是最优的,但对一个学习项目来说,能保证每个服务都有独立的数据容器、独立部署、独立负责人,已经足够支撑后续的注册、调用、追踪等实践。

2. 服务注册与发现:Nacos 上手最容易踩的两个坑

2.1 注册中心解决了什么问题

服务拆完之后,第一个问题是:user-service 怎么找到 order-service?如果按单体的思路,在配置里写死 IP 和端口,那么只要订单服务换一台机器、临时扩容或发布重启,调用方就要改配置、改代码。几个服务时还能忍,服务一多就是灾难。

注册中心做的事就类似于一个动态通讯录。服务启动时把自己注册进去,服务下线时把自己移除,调用方通过服务名去找可用实例。我用的注册中心是 Nacos,因为和 Spring Cloud 体系集成起来最省事,同时它也带配置中心功能,后面第 5 天会用到。

2.2 命名空间与分组:先分环境再谈隔离

这是我第二天踩的一个典型坑。我同时在本机和一台测试服务器上启动了同一个服务,结果 Nacos 控制台里出现了两个 IP,开发环境的实例和测试环境的实例混在一起。当时 A 同学写的测试脚本就调到了我本机的服务,跑出来一堆奇怪的网络错误。

实际解法是给不同环境设置 namespace。Nacos 的 namespace 是环境级隔离,同一个 namespace 内的服务才可见;group 是更小的隔离单元,比如同一个环境内把不同业务线分开。我的开发、测试、生产分别建了三个 namespace,然后每个服务的配置里指定 namespace:

spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: dev group: DEFAULT_GROUP

配置改完之后,我把 dev 环境的实例单独看,再也不用担心测试环境的流量打到本机。这个操作其实不难,但很多人都等到环境串了才发现。

2.3 临时实例与心跳保护:一次发布雪崩的复盘

注册中心最让我印象深刻的,是"保护阈值"这个设置。Nacos 里的服务实例默认是临时实例,靠客户端每 5 秒发一次心跳来维持在线状态,如果心跳停了 15 秒,服务端就会把它标成不健康,再等待 30 秒移除实例。这些都是默认值,通常不用改。

但有一批服务被批量下线发布时,健康实例会骤减。Nacos 发现健康实例的比例低于某个阈值时,会进入保护模式:不再摘除不健康实例,避免所有流量都打到剩余的健康实例上导致它们也被压垮。听起来是保护,实际在我这次的发布里成了帮倒忙,因为不健康实例还会被消费者调用,结果大量请求打到正在关闭的老实例上,出现了持续几分钟的报错。

那次之后我改了发布节奏:一次只重启一小批实例,而不是所有节点同时杀;服务启动后加入延迟注册;在关停前先调用接口把当前实例下掉,保证没有新流量进来。这些配合好了,保护阈值才真正是保护而不是负担。

3. 服务间调用与负载均衡:OpenFeign 用顺手前要搞懂的细节

3.1 声明式调用到底替我们做了什么

服务之间调用最直观的做法是用 RestTemplate 或 HttpClient 手动拼 URL,但我选的是 OpenFeign。它最大的价值是像写本地方法一样声明远程调用:定义一个接口,加上注解,Spring 在启动时会给这个接口生成动态代理。你调用这个接口的方法时,它会把方法名、参数、注解转成一次 HTTP 请求,再通过负载均衡器找到目标服务实例。

一个简单的客户端接口是这样的:

@FeignClient(name = "product-service", contextId = "productClient") public interface ProductClient { @GetMapping("/api/product/{skuId}") ProductInfo getProduct(@PathVariable("skuId") String skuId); }

这里 name 必须和注册中心里的服务名一致,路径要和提供方接口的 Controller 完全一致。少了 contextId 在多客户端场景下会有 bean 名称冲突,这也是一个经常让人困惑的报错点。

3.2 超时、重试、幂等:一次偶发 502 的排查过程

Feign 用起来很简单,但参数不配好就会出现诡异问题。有次遇到偶发 502,一段时间后自己恢复,看业务日志又没什么异常。后来排查才发现,下游一个服务因为外部接口抖动,单次查询耗时从几百毫秒涨到 8 秒,而调用方 Feign 的 readTimeout 没有显式配置,在当时的默认配置下等了很短就抛异常。异常一多,线程池被打满,网关层就返回 502。

我的建议是联调之前就把超时时间按接口特征配好:

feign: client: config: default: connectTimeout: 2000 readTimeout: 5000

如果服务里有批量查询、导出等长耗时接口,再单独给那个 FeignClient 覆盖 readTimeout。至于重试,OpenFeign 本身默认不重试,但 Spring Cloud LoadBalancer 打开重试后,会把请求再次发到下一个实例。这里必须注意幂等性,如果下游接口不是幂等的,重试可能造成重复下单、重复扣款。判断标准很简单:这个接口重复执行两次,业务结果是否一样?不一样就要做幂等控制,或者干脆把重试关掉。

3.3 负载均衡策略:轮询、权重还是最小连接

Feign 的负载均衡默认是轮询,对绝大多数场景够用。但如果实例性能不一致,比如其中一台是配置较弱的旧机器,轮询会让慢机器被频繁调用。这时候可以改用权重策略,给高性能实例配置更大的权重,或者用最小连接数策略,让请求优先进入当前连接数最少的实例。

具体选型我会先看两点:实例数量多不多、单实例处理能力是否平均。如果只有两台性能一样的机器在跑,轮询最简单也最可预测;如果实例能力参差不齐,我选权重;如果单个实例的响应时间波动大,最小连接数往往更平滑。负载均衡策略没有绝对标准,需要结合监控数据不断观察。

4. 网关统一入口:不是加一个 Spring Cloud Gateway 就完事

4.1 网关的职责边界:路由、鉴权、限流怎么切

服务多了以后,客户端不可能记住每个服务的地址,前端也不可能给每个服务单独配跨域。网关就是为了解决入口统一的问题。我用的是 Spring Cloud Gateway,它基于 WebFlux,简单理解就是所有外部流量先到网关,网关根据路径前缀把请求转发给对应的后端服务。

但网关不能什么都干。我的边界划分是:路由转发由网关负责,统一的登录态校验放在网关,跨域和基础限流也放在网关;但业务校验、业务逻辑、数据聚合一律不进网关。网关一旦塞入太多业务逻辑,它就从"流量入口"变成了"又一个单体应用",性能和稳定性都会出问题。

网关路由配置很简单:

spring: cloud: gateway: routes: - id: user-route uri: lb://user-service predicates: - Path=/api/user/** filters: - StripPrefix=1

这里 lb:// 表示按服务名从注册中心发现实例,Path 用前缀匹配,StripPrefix=1 会把 /api/user 这一段剥掉,正好对应后端 Controller 里的路径。

4.2 过滤器顺序与登录态透传

网关做鉴权的常见做法是写一个 GlobalFilter,校验请求头里的 Token,把解析出来的用户 ID 放到下游请求头里。这里比较容易忽略的是过滤器顺序:如果多个过滤器一起注册,order 值小的先执行。鉴权过滤器必须在转发过滤器之前执行,否则请求已经发出去了,Token 还没校验。

登录态透传有一个安全细节:下游服务不能无条件信任网关传过来的 X-User-Id 请求头,因为内网服务之间如果直接互相调用,也可能伪造这个头。我的做法是在网关校验通过后把用户 ID 重新写入请求头,而下游对外接口不接受客户端传入的这个头。如果是服务间直接调用,则需要走单独的认证方式,不能只靠一个 Header。

4.3 跨域与超时:两个现场级问题

网关最常见的两个现场问题,一个是 CORS 重复,另一个是超时。后端服务和网关如果都配置了 CORS,浏览器会收到两个 Access-Control-Allow-Origin 头,直接报跨域错误。正确做法是只在网关配 CORS,后端服务对内部调用不需要跨域配置。

超时的问题是 Spring Cloud Gateway 默认的 HTTP 客户端响应超时比较短,遇到文件上传、批量导出这类请求,网关会先返回超时错误,但后端还在继续处理,造成操作重复。所以后端如果有长耗时接口,要单独调大网关的响应超时参数,而不是一味增加路由级别的时间。网关是异步非阻塞模型,但不代表可以无限等,限时设置是保护自己的第一步。

5. 配置中心与可观测性:微服务跑起来后真正保命的两个能力

5.1 配置集中管理与动态刷新

服务一多,配置会散落在每个 jar 的 application.yml 里。改一个数据库地址要连上好几台服务器,改错了都不知道是谁的问题。我把配置集中到 Nacos Config 后,每个服务只保留一句指向配置中心的地址和定位信息,其余配置统一在 Nacos 控制台维护。

配置项我分成三类:应用独有配置、共享配置、敏感配置。共享配置比如日志级别、公共开关,用 shared-configs 拉取;数据库密码这类敏感配置则要加密存储,Nacos 自带的加密能力通常不够,我会用 KMS 或密码工具做加密,不在控制台明文保存。@RefreshScope 加在需要动态刷新的 Bean 上之后,修改配置不需要重启服务即可生效,但要注意动态刷新对连接池这类资源不一定完全适用,改完还是要观察是否真正重建。

5.2 链路追踪:从"谁调的谁"到"哪一步慢"

单体应用慢,可以看一份日志定位到方法。微服务慢,一个请求要经过网关、用户服务、订单服务、商品服务,如果每段日志没有串联标识,查一次慢请求可能要翻四个服务的日志,对不上时间点,排错效率极低。

我在第 6 天接入了 SkyWalking。它做的事情是给每个请求生成一个全局唯一的 traceId,请求经过每个服务时都带上传递,每个服务把耗时通过 Agent 上报,最终在 SkyWalking UI 上呈现一条调用链,你可以清楚看到请求在哪个服务、哪个步骤耗时最长。接入成本不高,Java 服务挂一个 agent 参数即可。

要提醒的是,链路追踪不是只给排查问题用,它还是容量评估的基础。你需要在压测时打开调用链,才能知道哪些节点的耗时有异常,为后续优化提供依据。

5.3 日志聚合的简单落地

链路追踪解决了"调用关系"的问题,日志聚合解决的是"排错上下文"的问题。不同服务的日志如果散落在各自主机上,排查问题时只能靠人肉登录。我采用的轻量方案是统一日志格式加集中采集,前端日志里通过 MDC 把当前 traceId 打印出来:

<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>

采集端用 Filebeat 把日志推给 Elasticsearch,Kibana 里按 traceId 搜索,一次请求的上下游日志就能全部聚到一起。这套组合看起来要部署三四个组件,但每个都几乎零开发量,配置一次长期受益。

6. 数据一致性:微服务绕不开的分布式事务问题

6.1 分布式事务的现实

微服务拆开之后,最痛的问题是:原本在一个数据库事务里做完的事情,现在要跨多个服务。比如创建订单要写订单表,还要扣减库存,库存数据在商品服务里,这两个操作无法共享同一个本地事务。

要接受一个现实:分布式场景下,强一致性通常意味着很高的代价,尤其是要兼顾并发性能的时候。多数业务最终会选择最终一致性,也就是经过一段时间后,数据会达到一致状态。只要这段时间内操作可以被幂等处理,业务上通常可以接受。

6.2 本地消息表方案的完整设计

如果不用分布式事务框架,最常见也最可控的方案是本地消息表。设计核心思路是:把"业务操作"和"要发给下游的消息"放在同一个本地事务里,然后通过异步任务逐步推进。

具体步骤可以按这个顺序落地:

  1. 在调用方数据库里建一张消息表,和业务主表处于同一事务。比如下单时,先写订单、同时写一条"扣库存消息",一起提交或一起回滚。
  2. 一个定时任务扫描消息表中状态为"待发送"的记录,按顺序发送给下游服务或写入消息队列。
  3. 下游服务收到消息后执行扣库存,成功后返回确认。
  4. 调用方把消息状态改成已确认,后续不再处理。
  5. 下游执行时要做幂等,用业务唯一键去重,避免同一条消息被消费多次。

这套方案的好处是逻辑清晰、不依赖额外框架,坏处是要维护定时任务和消息表,对数据库有一定额外压力。当消息量很大时,消息表会成为业务库的负担,那时候应该把消息表替换成独立的事务消息中间件。

6.3 Seata AT 模式在什么场景下才值得引入

如果你不想自己写消息表,可以考虑 Seata 这类分布式事务框架。Seata 的 AT 模式从应用角度看改动很小,它通过解析业务 SQL,自动记录改动前后的数据快照,事务提交前先获取全局锁,成功后删除快照,失败时用快照做补偿回滚。

听起来很方便,但引入它要付出代价:每个分支事务都要操作 undo_log 表,全局锁会降低并发,服务越多、链路越长,性能损耗越明显。我的判断是,只有满足以下条件才值得引入:事务较短、并发量可控、对一致性要求较高。一旦链路很长或流量很大,我会优先考虑把链路改成异步或使用业务补偿,而不是依赖全局事务。

6.4 一个典型取舍:下单扣库存

回到电商的经典场景。创建订单和扣库存,我最终没上分布式事务,而是采用订单先置为待支付、然后异步扣减库存、扣减失败则订单自动关闭并发送通知的方案。用户看到的体验是:下单之后可能要等一两秒才能确定库存是否扣减成功,但绝大多数场景都能成功,失败场景则由系统补偿。

这个取舍的核心是:库存扣减的失败概率很低,最终一致就能满足业务;如果一上来就用强一致的分布式事务,不仅实现复杂,还会拖慢下单链路。微服务改造的一个关键思维就是,为不同数据一致性要求选择不同手段,而不是把单体里的事务习惯原样搬过来。

7. day-06 之后的路线:我的排序和这天的复盘

7.1 下一步该补什么

第 6 天跑通骨架之后,我给自己列了一个优先级清单,不是按技术热门程度排的,而是按"哪个不补会先出事"排的。

第一优先是流量控制和降级。现在服务之间是同步调用,只要一个服务抖动,调用方就会被拖住,所以我下一步会接入 Sentinel 或 Resilience4j,给核心调用加上限流、熔断和降级规则。第二是异步化。像短信通知、积分变更这类非核心操作,我计划引入消息队列削峰填谷,把同步链路缩短。第三才是容器化部署,把服务统一打成镜像,用 Docker Compose 管理,这样环境迁移和扩容才会更顺畅。服务网格这些概念我暂时不碰,因为它解决的问题,当前规模下还用不着。

7.2 这天实操中最值得记住的三件事

三件事如果只能留三件,我会选这些:拆分顺序比选型重要,先定数据边界和认知边界再选框架;超时、重试、幂等这些参数要在联调前配好,而不是等线上出问题再补;链路追踪和配置中心不是"运维的事",是每个写微服务接口的人都要参与的基础设计。

如果让我重来一次,我不会一开始就把注意力放在研究注册中心的源码或者网关的高级特性上,而是先让两个服务通过注册中心互相调通,再把超时、日志、链路追踪这些"看不见的能力"补上。微服务的骨架搭起来不难,难的是让骨架在真实流量下还能站得住。day-06 这天我完成的只是前者,后面的路,都是从这些基础能力开始的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 3:46:53

Spring Boot+Vue疫苗接种预约平台:防超卖与并发控制实战

想搞清楚Spring Boot Vue怎么做预约平台的人&#xff0c;多半是遇到了同样的问题&#xff1a;业务看着简单&#xff0c;不就是“选日期、选时段、点预约”嘛&#xff0c;但真动起手来&#xff0c;疫苗库存怎么扣、号源怎么放、怎么防止同一个号被两个人抢到、前端日历怎么联动…

作者头像 李华
网站建设 2026/10/13 3:46:53

SpringBoot+Vue前后端分离美食网站毕设:从数据库设计到部署实战

1. 项目定位与需求拆解先说结论&#xff1a;这是一套标准到可以直接拿去部署演示的Java Web毕业设计项目&#xff0c;技术栈固定在SpringBoot Vue&#xff0c;前后端分离&#xff0c;B/S架构&#xff08;即Browser/Server&#xff0c;浏览器访问服务器&#xff0c;用户不需要安…

作者头像 李华
网站建设 2026/10/13 3:46:15

高并发抽奖系统实战:iris+xorm+Redis 防超卖与原子扣减

简介&#xff1a;这是一套基于 Go 语言、采用 iris 框架搭配 xorm、MySQL 与 Redis 构建的企业级抽奖系统源码&#xff0c;面向具备一定 Go 后端基础、希望深入理解高并发抽奖业务与后台管理的开发者。系统围绕黑名单、虚拟券、奖品、获奖记录、用户及每日抽奖情况共六张数据表…

作者头像 李华
网站建设 2026/10/13 3:46:15

Flutter汇率库curo鸿蒙化适配实战:精度与平台通道全解析

最近在做一批 Flutter 三方库的鸿蒙化适配&#xff0c;手头这个叫 curo 的库&#xff0c;算是我踩坑最深的之一。curo 在 Flutter 社区里不算最热的那一档&#xff0c;但功能指向非常明确&#xff1a;多币种汇率获取、货币换算、多币种资产按基准货币折算&#xff0c;以及一套自…

作者头像 李华
网站建设 2026/10/13 3:45:44

英文版Linux安装全程指南:从镜像到中文输入法

想装 Linux&#xff0c;但是一看官网下载的镜像默认是英文界面就犯怵&#xff1f;这大概是 Linux 新手咨询里出现频率最高的开场白了。我当年第一次装的时候也有这个顾虑&#xff0c;甚至为了规避英文&#xff0c;专门去找那种号称“中文版”的第三方改版镜像。结果装完才发现&…

作者头像 李华