Java进阶(1) | 微服务入门:从单体到 Spring Cloud 全家桶

发布时间:2026/7/17 13:49:47

Java进阶(1) | 微服务入门:从单体到 Spring Cloud 全家桶 本系列系统梳理了 Java 开发的详细知识点从基础语法到工程实践层层递进内容详实成体系建议先收藏再慢慢阅读方便日后随时回顾查阅。前言前面 27 篇基本都是一个应用、一个数据库的视角——写 Controller、Service、Mapper连上 MySQL/Redis 就能跑。但当系统越来越大比如一个完整的广告投放平台要做投放管理、计费、报表、用户中心继续把所有功能塞进一个项目会越来越难维护。这篇文章站在初学者的角度梳理微服务到底是什么、为什么要拆、拆了之后会冒出哪些新问题以及 Spring Cloud 生态里对应的解决方案——目标是建立一个完整的概念地图不深挖某一个组件的所有细节。1. 为什么需要微服务1.1 单体应用一开始很香把广告投放平台的所有功能——用户管理、广告投放、计费结算、数据报表——都写在一个项目里打包成一个 jar部署成一个进程。这就是单体架构Monolith。优点缺点开发简单一个项目一次启动本地调试方便代码越大越难维护改一个小功能可能要重新理解一大片代码部署简单一个 jar一台服务器牵一发动全身改了报表模块的代码整个应用都要重新打包、重新部署调用简单模块间是 Java 方法调用没有网络开销扩展不灵活报表模块需要更多 CPU但只能把整个应用一起扩容技术栈绑定整个项目只能用一种语言、一套框架团队小、业务简单的时候单体应用是最优选择——不要为了赶时髦而过早拆微服务拆分本身是有代价的后面会讲。但当团队变大、业务模块变多单体的缺点那一列会越来越明显几十个人同时改一个项目光是合并代码冲突就能耗费大量时间。1.2 微服务按业务拆成多个独立的应用微服务架构Microservices把单体应用按业务拆成多个独立部署、独立运行的小应用每个小应用只负责一块业务用户服务、投放服务、计费服务、报表服务……有自己的代码仓库、可以独立开发和部署一般有自己独立的数据库不共享表服务之间通过网络请求HTTP/RPC通信不再是 Java 方法调用1.3 微服务不是免费的午餐拆分之后每个模块内部确实变简单了——投放服务只用关心投放逻辑。但模块之间的复杂度从方法调用变成了网络调用这就引入了一整套单体应用里根本不存在的新问题单体应用里微服务里模块 A 调用模块 B一次本地方法调用几乎不耗时不会失败服务 A 调用服务 B要先知道 B 在哪台机器上、走网络请求可能超时、可能失败一个数据库事务搞定多表更新跨服务的数据一致性本地事务管不到别的服务改配置改完重启一下就行几十个服务实例配置怎么统一改、统一生效一个日志文件出错了直接搜关键字一个请求经过 5 个服务日志分散在 5 个地方怎么串起来排查接下来每一节都是在回答拆分之后冒出来的这个新问题业界是怎么解决的。2. 微服务要解决的核心问题总览把上面的问题对应到具体技术方案先建立一张地图后面每一节展开讲一个问题一句话描述常见方案服务怎么找到对方投放服务想调用计费服务但计费服务部署在哪台机器、哪个端口注册中心Eureka / Nacos服务之间怎么调用找到了地址怎么发起调用、怎么少写重复代码OpenFeign / RestTemplate请求量大了怎么分摊计费服务部署了 3 个实例请求该打给哪一个负载均衡LoadBalancer外部请求怎么进来手机 App 不会直接调用内部几十个服务应该打到哪API 网关Spring Cloud Gateway配置怎么统一管理几十个服务的数据库地址、开关配置难道一个个改配置中心Nacos Config某个服务挂了怎么办计费服务卡死会不会拖垮调用它的所有服务熔断与降级Sentinel跨服务的数据一致性“扣费成功但投放记录创建失败”怎么保证两边一致分布式事务Seata出问题了怎么排查一个慢请求经过了 5 个服务慢在哪一环链路追踪Sleuth Zipkin怎么部署这么多服务几十个服务手动一个个启动、升级根本管不过来容器化Docker Kubernetes下面按这张表的顺序逐个用最简单的例子讲清楚是什么、为什么需要、长什么样。3. 服务注册与发现3.1 为什么需要假设投放服务要调用计费服务的接口POST /billing/charge。在单体应用里这就是一次方法调用现在计费服务是独立部署的投放服务必须知道计费服务现在跑在哪台机器、哪个端口。最朴素的办法是把地址写死在配置文件里billing-service:url:http://192.168.1.100:8081问题来了计费服务扩容到 3 个实例写一个地址够吗某个实例重启后 IP 变了配置文件要不要手动改服务越来越多每个服务都要维护一份别人的地址表3.2 注册中心服务的电话簿注册中心就是一个所有服务都能访问的电话簿服务注册计费服务启动时把自己的名字 IP 端口报告给注册中心心跳续约计费服务定期向注册中心发心跳证明自己还活着注册中心如果一段时间没收到心跳就把这个实例摘除服务发现投放服务要调用计费服务时先问注册中心计费服务现在有哪些可用实例拿到一个地址列表常见注册中心Netflix EurekaSpring Cloud 早期默认、阿里 Nacos同时支持注册中心 配置中心国内更主流。3.3 最简示例Spring Cloud Alibaba Nacos!-- pom.xml让服务接入 Nacos 注册中心 --dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId/dependency# application.ymlspring:application:name:billing-service# 注册到 Nacos 的服务名cloud:nacos:discovery:server-addr:127.0.0.1:8848# Nacos 服务器地址服务启动后就会自动注册到 Nacos——不需要写任何代码靠的是 Spring Boot 的自动装配机制参考 17 节 Spring Boot。4. 服务间调用OpenFeign4.1 用 RestTemplate 调用能跑但很啰嗦22 节介绍过RestTemplate参考 22 节 HTTP 客户端调用别的服务时可以这样写Stringurlhttp://billing-service/billing/charge;// billing-service 是注册中心里的服务名ChargeResultresultrestTemplate.postForObject(url,request,ChargeResult.class);每次调用都要手写 URL、手动拼参数、手动处理返回值——服务调用一多代码里到处都是这种字符串拼接。4.2 OpenFeign把远程调用伪装成本地方法OpenFeign的思路定义一个 Java 接口加上注解描述调用哪个服务的哪个接口剩下的事情拼 URL、发 HTTP 请求、解析响应框架自动完成——业务代码里调用这个接口就像调用本地的 Service 一样。dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-openfeign/artifactId/dependency// 1. 定义接口声明我要调用 billing-service 的 /billing/chargeFeignClient(namebilling-service)publicinterfaceBillingClient{PostMapping(/billing/charge)ChargeResultcharge(RequestBodyChargeRequestrequest);}// 2. 像调用普通 Service 一样使用ServiceRequiredArgsConstructorpublicclassAdService{privatefinalBillingClientbillingClient;publicvoidlaunchAd(LongadId){ChargeResultresultbillingClient.charge(newChargeRequest(adId,100));// ... 后续逻辑}}FeignClient(name billing-service)里的billing-service就是 3.3 节里计费服务注册到 Nacos 时用的名字——OpenFeign 会自动去注册中心查这个名字对应的地址列表调用方完全不用关心具体 IP。5. 负载均衡5.1 为什么需要计费服务为了扛住更大流量部署了 3 个实例。投放服务通过注册中心拿到的是一个地址列表3 个地址每次调用该选哪一个5.2 负载均衡器在多个实例间分配请求负载均衡Load Balancing就是按某种策略把请求分摊到多个服务实例上策略说明轮询Round Robin依次轮流分配给每个实例最简单也最常用随机Random随机选一个实例加权Weighted性能好的机器配置更高权重分配更多请求Spring Cloud 里OpenFeign4 节和RestTemplate都可以直接集成spring-cloud-starter-loadbalancer——你在 4.2 节写的BillingClient调用背后实际上已经经过了负载均衡器业务代码完全无感知。这也是注册中心 负载均衡组合的核心价值调用方只写服务名该打给哪个实例完全交给框架处理。6. API 网关6.1 为什么需要现在系统内部有用户服务、投放服务、计费服务、报表服务……前端网页/App要调用这些接口时难道要分别记住每个服务的地址吗而且像登录校验“限流”统一加密这些逻辑难道每个服务都要重复写一遍6.2 网关所有外部请求的统一入口API 网关是部署在最外层的一个独立服务所有外部请求先经过网关再由网关转发给内部对应的服务网关的核心能力能力说明路由根据请求路径把/api/users/**转发给用户服务/api/ads/**转发给投放服务统一鉴权在网关层校验 Token校验失败直接拒绝内部服务不用每个都写一遍登录校验限流控制每秒最多放行多少请求保护后端服务日志/监控统一记录每个请求的耗时、状态码Spring Cloud Gateway 的路由配置示例spring:cloud:gateway:routes:-id:ad-service-routeuri:lb://ad-service# lb:// 表示走负载均衡ad-service 是注册中心里的服务名predicates:-Path/api/ads/**# 匹配这个路径前缀的请求-id:billing-service-routeuri:lb://billing-servicepredicates:-Path/api/billing/**7. 配置中心7.1 为什么需要每个服务都有application.yml——数据库地址、Redis 地址、第三方接口的开关。当服务数量变多或者同一个服务部署了几十个实例改一个配置比如把某个开关从false改成true要逐个服务、逐个实例去改配置文件再重启吗测试环境和生产环境的配置经常不一样怎么管理7.2 配置中心配置集中存储动态推送配置中心把配置文件统一存放在一个中心服务里各个服务启动时从配置中心拉取自己的配置配置中心修改配置后还能实时推送给正在运行的服务不用重启。# bootstrap.yml告诉服务去 Nacos 拉取配置spring:application:name:billing-servicecloud:nacos:config:server-addr:127.0.0.1:8848file-extension:yaml# 对应 Nacos 上的 billing-service.yaml配合RefreshScope注解被它标注的 Bean 在配置中心的值变化时会自动刷新不需要重启服务RestControllerRefreshScopepublicclassFeatureController{Value(${feature.new-billing-enabled:false})privatebooleannewBillingEnabled;}Nacos 同时承担注册中心3 节和配置中心两个角色是它在国内 Spring Cloud Alibaba 生态里很受欢迎的原因之一。8. 服务容错熔断、降级、限流8.1 为什么需要一个服务挂了不能拖垮所有人假设报表服务调用计费服务查询账单数据但计费服务因为数据库慢查询卡住了每个请求都要等 30 秒才超时。这时报表服务的所有请求都卡在等计费服务的调用上报表服务的线程池被这些卡住的请求占满报表服务自己也变得不可用了——故障从一个服务扩散到了调用它的所有服务这就是雪崩8.2 三个关键手段手段含义类比限流限制单位时间内允许通过的请求数量超出的直接拒绝景区限制每天入园人数熔断监控调用失败率失败率过高时暂时停止调用这个服务一段时间直接返回失败/默认值不再傻等家里的保险丝电流过大就跳闸降级调用失败/熔断时返回一个兜底结果比如缓存数据、默认值而不是直接报错给用户高速路口堵车时导航改走备用路线8.3 熔断器的三种状态关闭正常状态请求正常发给计费服务打开失败率超过阈值后熔断器跳闸后续请求不再真正调用计费服务直接走降级逻辑比如返回账单暂不可用请稍后重试半开经过一段冷却时间后放一小部分请求试探计费服务是否恢复——如果成功就回到关闭状态否则继续保持打开常见组件Sentinel阿里支持限流熔断降级有可视化控制台、Resilience4j轻量级库。Netflix Hystrix 是早期的代表但已停止维护。// Resilience4j 风格的熔断示例伪代码示意实际配置以注解/配置文件为主CircuitBreaker(namebillingService,fallbackMethodfallbackGetBill)publicBillgetBill(LongadId){returnbillingClient.getBill(adId);}// 熔断或异常时的降级方法方法签名要和原方法一致多一个 Throwable 参数publicBillfallbackGetBill(LongadId,Throwablet){returnBill.unavailable();// 返回一个表示暂不可用的默认对象}9. 分布式事务9.1 为什么需要广告投放操作需要同时做两件事计费服务给广告主账户扣费投放服务创建一条投放记录在单体应用里这两步在一个Transactional方法里要么都成功要么都回滚参考 MyBatis/Spring 事务管理。但现在这是两个服务、两个独立数据库——本地事务管不到另一个服务的数据库。如果扣费成功但创建投放记录失败了钱扣了但广告没投出去数据就不一致了。9.2 几种常见方案了解即可不深入方案思路特点2PC两阶段提交协调者先问所有参与者能不能提交都说能才真正提交强一致但所有参与者要互相等待性能差很少在微服务里直接用TCCTry-Confirm-Cancel每个操作拆成预留资源(Try)、“确认执行(Confirm)”、取消释放(Cancel)三步性能较好但每个业务都要写三套接口开发成本高Saga把一个大事务拆成一串本地事务每一步失败时执行对应的补偿操作比如扣费对应的补偿是退款不要求强一致适合电商/广告这类最终一致场景本地消息表 / 消息队列先在本地数据库记一条待发送消息本地事务提交后再异步通知其他服务失败可重试实现简单对最终一致性场景很常用实际项目中这部分常用阿里的Seata框架封装了上述模式尤其是 AT 模式类似自动化的 2PC业务代码改动很小。初学阶段记住一个结论分布式事务追求的通常是最终一致性过程中可能短暂不一致但最终会达成一致而不是单体应用里的强一致性。10. 链路追踪与日志聚合10.1 为什么需要一个创建广告投放的请求可能依次经过网关 → 投放服务 → 计费服务 → 用户服务查询广告主余额→ 报表服务记录一条投放日志。如果这个请求总共耗时 2 秒慢在哪一步如果报错了是哪个服务报的错每个服务的日志都是独立文件单看一个服务的日志看不到完整的调用链路。10.2 链路追踪给每个请求一个身份证号链路追踪的核心思路很简单请求进入网关时生成一个全局唯一的TraceId这个 TraceId 会随着请求通过 HTTP Header一路传递给后续调用的每个服务每个服务打日志时都把这个 TraceId 一起打印出来。TraceId: abc123 网关 [abc123] 收到请求 /api/ads/launch 投放服务 [abc123] 处理投放请求调用计费服务 计费服务 [abc123] 扣费成功耗时 800ms ← 原来慢在这里 投放服务 [abc123] 创建投放记录成功排查问题时只要拿着这个 TraceId 去日志系统里搜索就能把分散在 4 个服务里的日志串成一条完整的调用链并看到每一步的耗时。常见组合Spring Cloud Sleuth生成和传递 TraceIdZipkin可视化展示调用链路和耗时SkyWalking国产 APM 工具功能更全链路追踪 性能监控 告警接入时几乎不用改代码ELKElasticsearch Logstash Kibana把所有服务的日志统一收集、索引支持按 TraceId 搜索11. 容器化与部署简介11.1 为什么需要微服务拆分之后服务数量从 1 个变成几十个。每个服务可能用了不同版本的 JDK、不同的依赖库版本——在物理机/虚拟机上手动部署、手动处理环境不一致的问题几十个服务根本管不过来。11.2 Docker把运行环境打包进去Docker把应用代码、运行时依赖JDK 版本、系统库、配置文件一起打包成一个镜像Image。这个镜像在任何装了 Docker 的机器上运行出来的**容器Container**行为都完全一致——彻底解决在我电脑上能跑的问题。# Dockerfile描述如何构建 billing-service 的镜像 FROM eclipse-temurin:17-jre COPY target/billing-service.jar app.jar ENTRYPOINT [java, -jar, /app.jar]# 构建镜像dockerbuild-tbilling-service:1.0.# 运行容器dockerrun-d-p8081:8081 billing-service:1.011.3 Kubernetes管理大量容器几十个服务、每个服务还要部署多个实例光靠docker run一个个启动也不现实。KubernetesK8s是容器编排平台负责能力说明自动调度把容器分配到集群里资源充足的机器上运行自动扩缩容根据 CPU/内存使用率自动增加或减少服务实例数量自动恢复容器挂了自动重启机器挂了自动迁移到其他机器服务发现K8s 内置了类似 3 节注册中心的能力滚动更新发布新版本时逐个替换旧实例过程中服务不中断初学阶段不需要精通 K8s 的所有概念知道它是用来管理大量容器的就足够建立起完整的认知地图了——后续可以专门写一篇深入。12. 小结主题核心问题关键方案服务注册与发现服务怎么找到对方Nacos / Eureka注册 心跳 发现服务调用调用代码怎么写得简洁OpenFeign把远程调用伪装成接口方法负载均衡多个实例怎么分摊请求LoadBalancer轮询/随机/加权对调用方透明API 网关外部请求统一入口在哪Spring Cloud Gateway路由 鉴权 限流配置中心配置怎么集中管理、动态生效Nacos Config RefreshScope服务容错一个服务挂了怎么不拖垮全部限流/熔断/降级Sentinel / Resilience4j分布式事务跨服务数据一致性追求最终一致性Saga / 消息队列 / Seata链路追踪一个慢请求慢在哪一步TraceId 贯穿全链路Sleuth Zipkin / SkyWalking容器化部署几十个服务怎么管Docker 打包 Kubernetes 编排给初学者的建议不要一上来就想着把所有组件都用上——先理解单体的痛点 → 拆分之后冒出的新问题 → 每个新问题对应的方案这条逻辑链再结合实际项目规模决定要不要引入某个组件。Mini-SSP 这类中小型项目初期甚至可以先不拆微服务把按业务模块划分代码包做好参考 16 节 Spring IoC/AOP 的分层思想等真的遇到一个团队改一个项目互相阻塞的问题时再考虑拆分。 如果这篇文章对你有帮助别忘了点赞、收藏、关注三连关注我让你在 Java 学习的道路上不迷路持续为你带来成体系的 Java 干货~

相关新闻