Java 项目实战: 交友项目 - Dubbo 的内部结构与一次调用的完整链路

概述

把 Dubbo 拆成 Provider、Consumer、Registry、Monitor 四个角色之后,真正要搞清楚的是一句话:Consumer 发起一次 userService.queryById(1L),中间到底经过了哪些组件。这篇文章先把官方经典架构图复原出来,再把这条调用链拆到"内部结构"这一层------从代理、集群容错一直到 Nacos 上的服务地址推送。

纲要

  • 为什么需要这几个角色:远程调用绕不开的三个问题(地址获取、提供者变更、调用监控)
  • 五个角色与职责:Container / Provider / Consumer / Registry / Monitor,以及讲义口径的服务暴露、发现、调用、监控编号流程
  • 一次调用的完整链路:从 Container 启动 Provider 到 Monitor 统计,逐步拆解
  • 调用链的内部分层(本篇核心):Proxy → Cluster → Directory → Router → LoadBalance → Protocol → 网络,以及 Provider 侧的反向链路
  • SPI 扩展机制:为什么换注册中心、换协议只是一行配置的事
  • 工程结构实读 :dubbo-demo 的真实模块划分与各模块在架构中的位置
  • 常见误解澄清:Monitor 是否必需、Consumer 要不要直连 Provider、注册中心挂了会怎样
  • 把项目跑起来缺什么:Nacos 与 dubbo-admin
  • 配置速览 :Provider 侧与 Consumer 侧的 dubbo.* 关键项

为什么 Dubbo 要拆成这几个角色

Dubbo 干的事是远程调用加服务治理。远程调用一旦脱离单机,就绕不开三个问题:

  1. Consumer 怎么拿到 Provider 的地址。写死在配置里不行,提供者一扩容就要改代码重启。
  2. Provider 发生变更时怎么快速切换 。提供者从 20881 换到 20882,甚至下线一台,消费者列表必须跟着动。
  3. 调用过程怎么监控。调用次数、耗时、有没有慢调用,得有个地方统计,不然线上出问题只能靠猜。

前两个问题,学过 Spring Cloud 的人会立刻想到注册中心------没错,Dubbo 也是这条路。第三个问题才是 Dubbo 长期被忽略的一块:它在框架内部就预留了监控中心,把调用数据收集起来,这也是早期 Dubbo 相对"纯 HTTP 调用"的一个治理优势。

五个角色与职责

先看官方经典架构图。用 Mermaid 复原一遍,节点名保持 Dubbo 原生叫法,方便对照日志和源码里的类名。
#mermaid-svg-AHlUV5dfuts1m3Sp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AHlUV5dfuts1m3Sp .error-icon{fill:#552222;}#mermaid-svg-AHlUV5dfuts1m3Sp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AHlUV5dfuts1m3Sp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AHlUV5dfuts1m3Sp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AHlUV5dfuts1m3Sp .marker.cross{stroke:#333333;}#mermaid-svg-AHlUV5dfuts1m3Sp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AHlUV5dfuts1m3Sp p{margin:0;}#mermaid-svg-AHlUV5dfuts1m3Sp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AHlUV5dfuts1m3Sp .cluster-label text{fill:#333;}#mermaid-svg-AHlUV5dfuts1m3Sp .cluster-label span{color:#333;}#mermaid-svg-AHlUV5dfuts1m3Sp .cluster-label span p{background-color:transparent;}#mermaid-svg-AHlUV5dfuts1m3Sp .label text,#mermaid-svg-AHlUV5dfuts1m3Sp span{fill:#333;color:#333;}#mermaid-svg-AHlUV5dfuts1m3Sp .node rect,#mermaid-svg-AHlUV5dfuts1m3Sp .node circle,#mermaid-svg-AHlUV5dfuts1m3Sp .node ellipse,#mermaid-svg-AHlUV5dfuts1m3Sp .node polygon,#mermaid-svg-AHlUV5dfuts1m3Sp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AHlUV5dfuts1m3Sp .rough-node .label text,#mermaid-svg-AHlUV5dfuts1m3Sp .node .label text,#mermaid-svg-AHlUV5dfuts1m3Sp .image-shape .label,#mermaid-svg-AHlUV5dfuts1m3Sp .icon-shape .label{text-anchor:middle;}#mermaid-svg-AHlUV5dfuts1m3Sp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AHlUV5dfuts1m3Sp .rough-node .label,#mermaid-svg-AHlUV5dfuts1m3Sp .node .label,#mermaid-svg-AHlUV5dfuts1m3Sp .image-shape .label,#mermaid-svg-AHlUV5dfuts1m3Sp .icon-shape .label{text-align:center;}#mermaid-svg-AHlUV5dfuts1m3Sp .node.clickable{cursor:pointer;}#mermaid-svg-AHlUV5dfuts1m3Sp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AHlUV5dfuts1m3Sp .arrowheadPath{fill:#333333;}#mermaid-svg-AHlUV5dfuts1m3Sp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AHlUV5dfuts1m3Sp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AHlUV5dfuts1m3Sp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AHlUV5dfuts1m3Sp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AHlUV5dfuts1m3Sp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AHlUV5dfuts1m3Sp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AHlUV5dfuts1m3Sp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AHlUV5dfuts1m3Sp .cluster text{fill:#333;}#mermaid-svg-AHlUV5dfuts1m3Sp .cluster span{color:#333;}#mermaid-svg-AHlUV5dfuts1m3Sp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AHlUV5dfuts1m3Sp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AHlUV5dfuts1m3Sp rect.text{fill:none;stroke-width:0;}#mermaid-svg-AHlUV5dfuts1m3Sp .icon-shape,#mermaid-svg-AHlUV5dfuts1m3Sp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AHlUV5dfuts1m3Sp .icon-shape p,#mermaid-svg-AHlUV5dfuts1m3Sp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AHlUV5dfuts1m3Sp .icon-shape .label rect,#mermaid-svg-AHlUV5dfuts1m3Sp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AHlUV5dfuts1m3Sp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AHlUV5dfuts1m3Sp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AHlUV5dfuts1m3Sp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Container 服务运行容器
1 暴露/注册 自身地址
2 订阅 所需服务
3 通知 提供者地址列表
4 发起 RPC 调用
5 上报调用次数与耗时
5 上报调用次数与耗时
Provider 服务提供者
Registry 注册中心
Consumer 服务消费者
Monitor 监控中心

角色职责:

节点 角色说明 对应到业务代码
Container 服务的运行容器,负责启动、加载、运行服务提供者 Spring Boot 引导类 UserProviderApplication + 内嵌 Tomcat
Provider 暴露服务的服务提供方 user-provider 模块,@DubboService 修饰的 UserServiceImpl
Consumer 调用远程服务的服务消费方 user-consumer 模块,@DubboReference 注入的 UserService
Registry 服务注册与发现的注册中心 Nacos(nacos://127.0.0.1:8848)
Monitor 统计服务调用次数和调用时间的监控中心 dubbo-admin 控制台

关于 Container 有个细节要说明:讲义 2.1 的节点表其实只列了 Provider、Consumer、Registry、Monitor 四个角色,Container 是在调用关系第 1 条里出现的------"服务容器负责启动,加载,运行服务提供者"。官方经典图把它单独画成一个节点,是因为 Provider 本身也是一段要跑起来的 Java 进程,谁来拉起它、谁来管它的生命周期,需要有个明确的角色。在 Spring Boot 里这个角色就是引导类加内嵌容器。

讲义的调用关系说明原文如下,编号我原样保留:

  1. 服务容器负责启动、加载、运行服务提供者。
  2. 服务提供者在启动时,向注册中心注册自己提供的服务。
  3. 服务消费者在启动时,向注册中心订阅自己所需的服务。
  4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  5. 服务消费者从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

第 6 条那句"定时每分钟发送一次"是关键信息------监控数据不是每次调用同步上报的,而是在内存里攒着,按分钟批量发。这意味着 Monitor 短暂不可用不会阻塞业务调用,追这一层的时候不用怀疑是不是监控把请求卡住了。

一次调用的完整链路

把上面六条步骤摊到时间轴上看,就得到这篇最该记住的一张图。以"用用户 id 查询用户"为例。
Monitor Consumer Registry(Nacos) Provider Container Monitor Consumer Registry(Nacos) Provider Container #mermaid-svg-fNwLSIjqe7CIyW72{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fNwLSIjqe7CIyW72 .error-icon{fill:#552222;}#mermaid-svg-fNwLSIjqe7CIyW72 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fNwLSIjqe7CIyW72 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fNwLSIjqe7CIyW72 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fNwLSIjqe7CIyW72 .marker.cross{stroke:#333333;}#mermaid-svg-fNwLSIjqe7CIyW72 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fNwLSIjqe7CIyW72 p{margin:0;}#mermaid-svg-fNwLSIjqe7CIyW72 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fNwLSIjqe7CIyW72 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-fNwLSIjqe7CIyW72 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fNwLSIjqe7CIyW72 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-fNwLSIjqe7CIyW72 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-fNwLSIjqe7CIyW72 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-fNwLSIjqe7CIyW72 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-fNwLSIjqe7CIyW72 .sequenceNumber{fill:white;}#mermaid-svg-fNwLSIjqe7CIyW72 #sequencenumber{fill:#333;}#mermaid-svg-fNwLSIjqe7CIyW72 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-fNwLSIjqe7CIyW72 .messageText{fill:#333;stroke:none;}#mermaid-svg-fNwLSIjqe7CIyW72 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fNwLSIjqe7CIyW72 .labelText,#mermaid-svg-fNwLSIjqe7CIyW72 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-fNwLSIjqe7CIyW72 .loopText,#mermaid-svg-fNwLSIjqe7CIyW72 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-fNwLSIjqe7CIyW72 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fNwLSIjqe7CIyW72 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-fNwLSIjqe7CIyW72 .noteText,#mermaid-svg-fNwLSIjqe7CIyW72 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-fNwLSIjqe7CIyW72 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fNwLSIjqe7CIyW72 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fNwLSIjqe7CIyW72 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fNwLSIjqe7CIyW72 .actorPopupMenu{position:absolute;}#mermaid-svg-fNwLSIjqe7CIyW72 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-fNwLSIjqe7CIyW72 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fNwLSIjqe7CIyW72 .actor-man circle,#mermaid-svg-fNwLSIjqe7CIyW72 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-fNwLSIjqe7CIyW72 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 内存中记录 provider 地址列表 提供者变更时,注册中心基于长连接主动推送 ① 启动、加载、运行 1 ① 注册自身地址 ip:20881 + 服务名 2 ② 启动消费者 3 ② 订阅 cn.mf.user.service.UserService 4 ③ 返回可用 Provider 地址列表 5 ④ 从列表中按负载均衡策略选一台 6 ④ RPC 调用 queryById(1L) 7 ④ 返回结果(序列化后回传) 8 ⑤ 每分钟定时上报调用次数/耗时 9 ⑤ 每分钟定时上报调用次数/耗时 10

逐步看关键动作:

① 启动与注册。 Container 拉起 Provider 进程。Provider 启动完成、端口监听就绪后,把"我提供 UserService,地址是 192.168.x.x:20881"这条信息写到注册中心。注意这里注册的是 Dubbo 协议端口 20881 ,不是 Web 端口 18080------两者各管一摊,一个是 HTTP 接口给浏览器调的,一个是 RPC 给 Consumer 调的。讲义里提到端口从 20881 变成 20882 的场景,说的就是 RPC 端口变化后消费者怎么跟上。

② 订阅。 Consumer 启动时,去注册中心订阅自己需要的服务。订阅的粒度是接口全限定名 + 版本号 ,@DubboReference(version = "2.0.0") 里的 version 就是订阅条件的一部分。

③ 通知。 注册中心把匹配的 Provider 地址列表返回给 Consumer。之后如果有变更------新增实例、下线实例、端口改变------注册中心会基于长连接主动推送,不需要 Consumer 轮询。这条长连接是"注册中心挂掉后已连接双方还能继续调用"的技术根源。

④ 调用。 Consumer 从地址列表里按负载均衡策略挑一台,发起 RPC 调用。讲义给的口径是"基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用"------后半句就是 Dubbo 的集群容错,默认策略 Failover,失败重试到别的节点。

⑤ 监控。 双方各自在内存累计调用次数和耗时,每分钟发一次给监控中心。这一步是可选的:Monitor 配置留空就不上报;生产上更常见的做法是干脆关掉 Dubbo 自带的统计,用 SkyWalking、Prometheus 这类 APM 顶替。所以排查问题发现 Monitor 里没数据,先确认是不是根本没配。

调用链的内部分层

上面的时序图回答的是"哪些角色参与了",但"内部结构"真正指的是下一层:一次 invoke 在框架内部依次穿过了哪些组件。

讲义本节(2.1)只讲了角色级流程,没有展开 Invoker、Cluster 这一层。以下内容按 Dubbo 官方文档的调用链口径补充,用于把"内部结构"这个名字落到实处。

Consumer 侧发起一次调用,方法调用会被一层层包装、过滤、再选路,最终落到网络:
#mermaid-svg-EGzTg95umrHNKfm1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EGzTg95umrHNKfm1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EGzTg95umrHNKfm1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EGzTg95umrHNKfm1 .error-icon{fill:#552222;}#mermaid-svg-EGzTg95umrHNKfm1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EGzTg95umrHNKfm1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EGzTg95umrHNKfm1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EGzTg95umrHNKfm1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EGzTg95umrHNKfm1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EGzTg95umrHNKfm1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EGzTg95umrHNKfm1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EGzTg95umrHNKfm1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EGzTg95umrHNKfm1 .marker.cross{stroke:#333333;}#mermaid-svg-EGzTg95umrHNKfm1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EGzTg95umrHNKfm1 p{margin:0;}#mermaid-svg-EGzTg95umrHNKfm1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EGzTg95umrHNKfm1 .cluster-label text{fill:#333;}#mermaid-svg-EGzTg95umrHNKfm1 .cluster-label span{color:#333;}#mermaid-svg-EGzTg95umrHNKfm1 .cluster-label span p{background-color:transparent;}#mermaid-svg-EGzTg95umrHNKfm1 .label text,#mermaid-svg-EGzTg95umrHNKfm1 span{fill:#333;color:#333;}#mermaid-svg-EGzTg95umrHNKfm1 .node rect,#mermaid-svg-EGzTg95umrHNKfm1 .node circle,#mermaid-svg-EGzTg95umrHNKfm1 .node ellipse,#mermaid-svg-EGzTg95umrHNKfm1 .node polygon,#mermaid-svg-EGzTg95umrHNKfm1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EGzTg95umrHNKfm1 .rough-node .label text,#mermaid-svg-EGzTg95umrHNKfm1 .node .label text,#mermaid-svg-EGzTg95umrHNKfm1 .image-shape .label,#mermaid-svg-EGzTg95umrHNKfm1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-EGzTg95umrHNKfm1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EGzTg95umrHNKfm1 .rough-node .label,#mermaid-svg-EGzTg95umrHNKfm1 .node .label,#mermaid-svg-EGzTg95umrHNKfm1 .image-shape .label,#mermaid-svg-EGzTg95umrHNKfm1 .icon-shape .label{text-align:center;}#mermaid-svg-EGzTg95umrHNKfm1 .node.clickable{cursor:pointer;}#mermaid-svg-EGzTg95umrHNKfm1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EGzTg95umrHNKfm1 .arrowheadPath{fill:#333333;}#mermaid-svg-EGzTg95umrHNKfm1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EGzTg95umrHNKfm1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EGzTg95umrHNKfm1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EGzTg95umrHNKfm1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EGzTg95umrHNKfm1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EGzTg95umrHNKfm1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EGzTg95umrHNKfm1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EGzTg95umrHNKfm1 .cluster text{fill:#333;}#mermaid-svg-EGzTg95umrHNKfm1 .cluster span{color:#333;}#mermaid-svg-EGzTg95umrHNKfm1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EGzTg95umrHNKfm1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EGzTg95umrHNKfm1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-EGzTg95umrHNKfm1 .icon-shape,#mermaid-svg-EGzTg95umrHNKfm1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EGzTg95umrHNKfm1 .icon-shape p,#mermaid-svg-EGzTg95umrHNKfm1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EGzTg95umrHNKfm1 .icon-shape .label rect,#mermaid-svg-EGzTg95umrHNKfm1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EGzTg95umrHNKfm1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EGzTg95umrHNKfm1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EGzTg95umrHNKfm1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务代码调用 userService.queryById(1L)
Proxy 代理层

把接口调用转成 Invocation 对象
Cluster 集群容错

失败重试 / 快速失败 / 并行等
Directory 服务目录

持有可用的 Invoker 列表
Router 路由

标签路由 / 条件路由过滤
LoadBalance 负载均衡

Random / RoundRobin / LeastActive / ConsistentHash
Protocol 协议 + 序列化

dubbo 协议封装请求体
网络传输

Netty 长连接发送到 Provider
Provider 侧协议解码
Filter 过滤器链

限流 / 超时 / 异常处理
Invoker 调用真实实现
UserServiceImpl.queryById()

几处容易卡住的地方:

  • Proxy 和真实接口的关系。 你在 Consumer 里注入的 UserService 不是 user-consumer 里写的实现类------这个模块根本没有实现类。它是一个动态代理对象,调用它其实是在往框架里塞一个 Invocation。
  • Directory 与 Router 的顺序。 Directory 给出的是"能用的全部 Invoker",Router 是在这批里做过滤(比如按标签、按条件表达式筛掉一部分),LoadBalance 再从筛完的结果里挑一个。顺序搞反的话,标签路由会失效。
  • Provider 侧并不是只执行方法。 请求进来同样要过 Filter 链,超时、限流、异常兜底都在这一层做,然后才由 Invoker 反射调用到 UserServiceImpl。

SPI:为什么 Dubbo 的组件这么容易换

上面每一层------Protocol、Registry、Cluster、LoadBalance、Filter------在 Dubbo 里都不是写死的类,而是通过 SPI(Service Provider Interface) 加载的可替换实现。

同样,SPI 这段讲义未展开,属于官方机制说明。

带来的实际效果很直接,这也是后面几篇"高级特性""Spring Cloud 整合 Dubbo"能成立的前提:

想换的东西 只改配置 底层发生了啥
注册中心 Zookeeper → Nacos dubbo.registry.address 换掉 RegistryFactory 的 SPI 实现
协议 dubbo → rest / triple dubbo.protocol.name 换掉 Protocol 的 SPI 实现
容错策略 Failover → Failfast cluster 属性 换掉 Cluster 的 SPI 实现
负载均衡 Random → RoundRobin loadbalance 属性 换掉 LoadBalance 的 SPI 实现

Dubbo 的 SPI 比 JDK 原生的 SPI 多了两点:按需加载 和 IOC/AOP 支持 (实现类可以注入别的扩展,也能被 Wrapper 包装成过滤器链)。所以 Filter 链能自动串起来,靠的就是这个机制。

工程结构实读

素材里的 dubbo-demo 就是把这套架构落地的最小工程。真实模块树如下(已忽略 .idea 与 target 目录):

tree 复制代码
dubbo-demo
├── pom.xml                     父 POM,packaging=pom,统一版本管理
├── user-api                    契约模块
│   ├── pom.xml
│   └── src/main/java/cn/mf/user/
│       ├── domain/User.java           实体(实现 Serializable)
│       └── service/UserService.java   接口定义
├── user-provider               服务提供者(对应架构图 Provider)
│   ├── pom.xml
│   └── src/main/
│       ├── java/cn/mf/user/
│       │   ├── UserProviderApplication.java   引导类(Container 侧入口)
│       │   ├── mapper/UserMapper.java
│       │   └── service/impl/
│       │       ├── UserServiceImpl.java       version = 1.0.0
│       │       └── UserServiceImpl2.java      version = 2.0.0
│       └── resources/application.yml          protocol / registry / scan
└── user-consumer               服务消费者(对应架构图 Consumer)
    ├── pom.xml
    └── src/main/
        ├── java/cn/mf/user/
        │   ├── UserConsumerApplication.java   引导类
        │   └── controller/UserController.java @DubboReference 注入接口
        └── resources/application.yml          registry / consumer 配置

三个模块在架构里的位置:

  • user-api = 契约。 只放接口和领域对象,没有任何实现,Provider 和 Consumer 都依赖它。这一步很关键------Dubbo 的远程调用是"面向接口"的,双方必须拿到同一份接口定义才能通信,而接口不能各写一份,否则序列化后的类名、方法签名对不上。
  • user-provider = 提供者。 有 UserMapper、数据库连接、真实实现类,对外暴露服务。
  • user-consumer = 消费者。 只有 Controller,没有实现类,UserService 通过 @DubboReference 注入的是代理对象。

两个实读发现,看代码时会碰到:

  1. 父 POM 的 <modules> 里有一个 user-server,但目录里并不存在这个模块。 这是课程演进过程留下的残留条目,构建时 mvn 会报找不到模块。对着素材抄目录树的时候,别把这个模块也一起建出来------真正存在的是 user-api、user-provider、user-consumer 三个。
  2. 这个快照处于"多版本"演示状态。 Provider 侧有两个实现,UserServiceImpl 是 version = "1.0.0"、UserServiceImpl2 是 "2.0.0";而 Consumer 的 @DubboReference(version = "2.0.0", retries = 0, loadbalance = "Random") 指向的是 2.0.0。版本号是订阅条件的一部分,对不上会出现"服务找不到"------这类问题十有八九出在版本或分组上,而不是网络。

契约模块与接口定义长这样,可以直接整段复制:

java 复制代码
package cn.mf.user.domain;

import lombok.Data;

import java.io.Serializable;

@Data
public class User implements Serializable {
    private Long id;
    private String username;
    private String address;
}
java 复制代码
package cn.mf.user.service;

import cn.mf.user.domain.User;

public interface UserService {

    String queryUsername(Long id);

    User queryById(Long id);
}

Provider 侧暴露服务,注解里的 version 就是注册到 Nacos 的版本标签:

java 复制代码
package cn.mf.user.service.impl;

import cn.mf.user.domain.User;
import cn.mf.user.mapper.UserMapper;
import cn.mf.user.service.UserService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;

// 暴露 Dubbo 服务,version 决定消费端按哪个版本订阅
@DubboService(version = "1.0.0")
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public String queryUsername(Long id) {
        return userMapper.findById(id).getUsername();
    }

    @Override
    public User queryById(Long id) {
        User user = userMapper.findById(id);
        user.setUsername(user.getUsername() + "v1.0");
        return user;
    }
}

Provider 的配置,三个块各管一件事:协议、注册中心、注解扫描包:

yaml 复制代码
server:
  port: 18080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/dubbo-demo?useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: user-provider
# 配置 Dubbo 提供者
dubbo:
  # 使用 dubbo 协议,RPC 端口 20881(不是 Web 端口)
  protocol:
    name: dubbo
    port: 20881
  # 注册中心地址
  registry:
    address: nacos://127.0.0.1:8848
  # Dubbo 注解的包扫描(@DubboService 所在包)
  scan:
    base-packages: cn.mf.user.service

Consumer 侧引用远程服务,这里的 UserService 是代理:

java 复制代码
package cn.mf.user.controller;

import cn.mf.user.domain.User;
import cn.mf.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    // 引用远程服务,实际注入的是动态代理
    @DubboReference(version = "2.0.0", retries = 0, loadbalance = "Random")
    private UserService userService;

    @GetMapping("/usernames/{id}")
    public String queryUsername(@PathVariable("id") Long id) {
        return userService.queryUsername(id);
    }

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        return userService.queryById(id);
    }
}

消费端配置里那两个开关,正好对应前面讲的已知坑:

yaml 复制代码
server:
  port: 18081
spring:
  application:
    name: user-consumer
dubbo:
  registry:
    address: nacos://127.0.0.1:8848
  consumer:
    check: false   # 关闭启动检查:本地没有 Provider 时也能启动
    retries: 0     # 关闭重试:写操作重复提交会被放大

端口口径说明一句:视频里口述的是 Web 端口 8080 / 8081,讲义 2.4 给的也是 18081 一带,而实读工程里 Provider 的 Web 端口是 18080、Consumer 是 18081、Dubbo 协议端口是 20881。以工程实际配置为准就不容易踩混。

常见误解澄清

排查 Dubbo 问题时,下面几个误解最容易把人带偏:

常见说法 实际情况
Monitor 是必需组件,没它服务起不来 Monitor 是可选上报,不配就不上报,不影响调用链路
Consumer 必须知道 Provider 的 IP,属于直连 Consumer 只认注册中心订阅到的地址列表,Provider 换 IP 由注册中心推送
注册中心就是个消息队列 它是服务地址的登记与推送中心,推送的是地址变更,不做业务消息缓冲
注册中心一挂,整个系统就废了 已建立连接的双方可继续调用,只是无法发现新实例、无法感知下线,属于"降级可用"

第四条要说准:注册中心不可用时,Consumer 本地内存里已经缓存了 Provider 地址列表,长连接也还在,业务调用照走。真正失效的是服务发现能力------新上线的实例没人告诉 Consumer,下线的实例 Consumer 还可能打过去(要靠失败重试兜底)。这个结论跟 Spring Cloud 里 Eureka / Nacos 挂掉时的表现是一致的,可以放在一起记。

把项目跑起来缺什么

dubbo-demo 里我们能写的只有 Provider 和 Consumer 两部分 Java 代码。图上的另外两个角色不是代码,是要提前装好的外部组件:

  • 注册中心 Nacos :startup.cmd -m standalone 单机启动,控制台在 http://127.0.0.1:8848/nacos,默认账号密码都是 nacos。没有它,Provider 注册不上去,Consumer 也订阅不到,日志里会是连接超时。
  • 监控中心 dubbo-admin:Dubbo 官方提供的控制台,能做服务查询、详情展示、服务测试。它是可选组件,本地不装也能完成调用,但排查"服务到底注册上去没有"时会方便很多。

这两样装好之后,再依次启动 UserProviderApplication 和 UserConsumerApplication,访问 http://localhost:18081/user/1,链路才算真正跑通。下一篇就按这个顺序,先把 Nacos 和监控中心装起来。

配置速览

配置项 位置 作用
dubbo.protocol.name Provider 协议名,dubbo 为默认二进制协议
dubbo.protocol.port Provider RPC 服务端口,示例为 20881
dubbo.registry.address 双方 注册中心地址,示例 nacos://127.0.0.1:8848
dubbo.scan.base-packages Provider 扫描 @DubboService 的包
dubbo.consumer.check Consumer 启动检查,false 表示没有提供者也能启动
dubbo.consumer.retries Consumer 失败重试次数,写操作建议 0
@DubboService(version) Provider 服务版本,注册到注册中心的标签之一
@DubboReference(version/loadbalance) Consumer 订阅版本与负载均衡策略

官方文档

总结

  • Dubbo 的四个角色里,Registry 解决"地址从哪来 + 变更怎么通知",Monitor 解决"调用怎么看",Container 解决"谁来拉起来"。五个角色的分工是绕开远程调用三个基本问题的最小解。
  • 讲义的六步流程是角色级视角;真正落到"内部结构",一次调用在 Consumer 侧要依次穿过 Proxy → Cluster → Directory → Router → LoadBalance → Protocol,在 Provider 侧反向穿过 Filter → Protocol → Invoker。Invoker 这一层才是 Dubbo 的统一抽象。
  • Monitor 是可选的,生产上更常见的是用 APM 顶替;retries = 0 是写操作的基本自觉。
  • 注册中心挂掉不等于系统挂掉------这一段可以跟 Spring Cloud 篇对照看,两个框架的结论一样,只是表现和超时时间不同。
  • 要把链路跑通,Provider 和 Consumer 的代码只是一半,另一半是 Nacos 与 dubbo-admin。
相关推荐
Wang's Blog1 小时前
Java 项目实战: 交友项目 - Dubbo 入门引导
java·dubbo·交友
蜗牛互联网2 小时前
Python Agent工具调用的白名单、参数校验与审批门禁
java·服务器·前端·人工智能·后端·python
SL_staff2 小时前
当规则引擎开始‘懂业务’:JVS-Rules金融风控四大原生能力拆解
java·python·全栈
Shaoshing2 小时前
前缀和与差分
java·数据结构·算法·前缀和·差分
dd聊技术2 小时前
Prompt 从代码常量搬进数据库:改一句话,不用再发一次版
java·后端
ly76893 小时前
Spring Data MongoDB 批量写入实践:BulkOperations 批大小与写关注权衡
java·mongodb·spring·批量写入·bulkoperations·写关注·mongotemplate
TAKETODAY3 小时前
Netty 是怎么把 HTTP 请求交给 MVC 的?一个请求的完整链路
java·netty
蚰蜒螟3 小时前
从内核启动到 Java NIO:一次 socket accept 的源码串联之旅
java·python·nio
会编程的吕洞宾3 小时前
Spring AI 2.0 自校正结构化输出实战:让大模型稳定吐出合格 JSON
java·人工智能·后端·spring