重新认识Dubbo

Dubbo 最初被定位为一个高性能的 RPC 框架,但这是过去,随着 Dubbo 3 的演进(尤其是与 Spring Cloud Alibaba 的整合),这个定义已远远不足以描述它的全貌。

因为,它已经具备了完整的 微服务治理能力,包括服务注册发现、配置中心、熔断限流、可观测性等,是一个面向云原生、支持多语言、具备完整微服务治理能力的一站式微服务解决方案

什么意思,Dubbo 不是只有那几个注解来作为 RPC 框架的能力了,而是可以作为 Spring Cloud Alibaba 中的单独一个框架,自己实现服务注册发现、配置中心、熔断限流、可观测性等这些微服务框架需要的功能。

所以说,Dubbo 3 不再只是调用远程方法,而是构建和运维整个微服务体系的基础设施。

img

服务治理、服务通信。这是dubbo框架所提供的核心功能。实际上也是微服务框架所要提供的核心功能。

Dubbo 与 Spring Cloud 对比

而且Dubbo开发相较于 Spring Cloud 具有一些优势

  • Dubbo 开箱即用,相对于 Spring Cloud 要易用一些
  • Dubbo 功能丰富,基于原生库或轻量扩展即可实现绝大多数的微服务治理能力
  • Dubbo 是面向超大规模微服务集群设计的,拥有高性能的 RPC 通信协议设计与实现,而且轻松支持百万规模集群实例的地址发现与流量治理
  • Dubbo 对扩展支持性好

Dubbo 虽然可以独立使用,但在 Spring Cloud 生态中,推荐搭配 Spring Cloud Alibaba

一些需要认识的内容

云原生

首先,云原生没有确切的定义云原生一直在发展变化之中,解释权不归某个人或组织所有。

首先,云原生借了云计算的东风,没有云计算,自然没有云原生,云计算是云原生的基础。

随着虚拟化技术的成熟和分布式框架的普及,在容器技术、可持续交付、编排系统等开源社区的推动下,以及微服务等开发理念的带动下,应用上云已经是不可逆转的趋势

要转向云原生应用需要以新的云原生方法开展工作,云原生包括很多方面:基础架构服务、虚拟化、容器化、容器编排、微服务。

所以,云原生(CloudNative)是一个组合词,Cloud+Native。

  • Cloud表示应用程序位于云中,而不是传统的数据中心;
  • Native表示应用程序从设计之初即考虑到云的环境,原生为云而设计,在云上以最佳姿势运行,充分利用和发挥云平台的弹性+分布式优势。

Pivotal 最新官网对云原生概括为4个要点:DevOps + 持续交付 + 微服务 + 容器

  • DevOps:这是个组合词,Dev+Ops,就是开发和运维合体,实际上DevOps应该还包括测试,DevOps 是一个敏捷思维,是一个沟通文化,也是组织形式,为云原生提供持续交付能力。
  • 持续交付:持续交付是不误时开发,不停机更新,小步快跑,反传统瀑布式开发模型,这要求开发版本和稳定版本并存,其实需要很多流程和工具支撑。
  • 容器化:Docker是应用最为广泛的容器引擎,在思科谷歌等公司的基础设施中大量使用,是基于LXC技术搞的,容器化为微服务提供实施保障,起到应用隔离作用,K8S是容器编排系统,用于容器管理,容器间的负载均衡,谷歌搞的,Docker和K8S都采用Go编写,都是好东西。
  • 微服务:看到这里的应该都懂微服务了吧
img

结果后来 云原生计算基金会成立又来搅和一把,现在已经很复杂了

总而言之,符合云原生架构的应用程序应该是:采用开源堆栈(K8S,Docker)进行容器化,基于微服务架构提高灵活性和可维护性,借助敏捷方法、DevOps支持持续迭代和运维自动化,利用云平台设施实现弹性伸缩、动态调度、优化资源利用率。

如果说PC时代的基础设施是控件库,互联网时代的基础实施是云,那AI时代基础设施是什么?又会有什么高端玩法?只能说什么时候有什么时候的花样了。

Triple 协议

这是 Dubbo 嘴里一直在说的下一代协议

在以前 Dubbo 2.x 的时候,Dubbo 使用的是Dubbo 协议,这是一个私有二进制协议,但是现在 Dubbo 3.x 使用的是Triple 协议,它基于 HTTP/2 + gRPC,兼容原生 gRPC,支持流式通信,而且也增加了 Dubbo 特有的治理字段

从 Triple 协议开始,Dubbo 才支持基于 IDL 的服务定义。此外,Dubbo 还集成了业界主流的大部分协议

那么,它就有一些核心的优势了

  • 网关友好:可直接被 Envoy、Spring Cloud Gateway 代理,传统 Dubbo 协议不行
  • 多语言天然支持:使用 .proto 定义服务,Java/Go/Python 共享同一份 IDL 来生成代码
  • 流式通信:支持 Request Stream、Response Stream、Bidirectional Stream

emmm,之前在说 RPC 的时候就说过 Dubbo 是如何跨语言的,它通过 IDL(.proto)实现跨语言互通,通过 Protobuf 定义服务接口,然后去生成多语言客户端 / 服务端的代码

金丝雀发布

首先,Dubbo 3 将过去分散的路由、限流、降级等规则,抽象为一套流量和服务治理规则,无论你使用:

  • 纯 SDK 模式
  • Sidecar Mesh
  • Proxyless Mesh

都能用同一套 YAML 规则进行管理 ,那么此时,我们就可以进行测试,这个在 Dubbo 那边叫了个词叫 金丝雀发布

那么 金丝雀发布 就是一种软件发布策略,旨在在将新版本全面推广到生产环境之前,先在一小部分用户或服务器上进行测试和验证。这种策略的名称来源于矿工在矿井中使用金丝雀来检测有毒气体的做法。

  • 新代码或更新逐渐部署到少量用户或服务器,以降低潜在风险

  • 测试新版本的性能和表现,在保证系统整体稳定运行的前提下,尽早发现新版本在实际环境上的问题。

RPC vs REST

Dubbo 架构

首先,Dubbo 的架构如下

img

下面是它的源码架构

format,webp
  • dubbo-common:公共逻辑模块: 包括Util类和通用模型
  • dubbo-remoting 远程通信模块: 相当于dubbo协议的实现,如果RPC使用RMI协议则不需要使用此包
  • dubbo-rpc 远程调用模块: 抽象各种协议,以及动态代理,包含一对一的调用,不关心集群的原理。
  • dubbo-cluster 集群模块: 将多个服务提供方伪装成一个提供方,包括负载均衡,容错,路由等,集群的地址列表可以是静态配置的,也可以是注册中心下发的.
  • dubbo-registry 注册中心模块: 基于注册中心下发的集群方式,以及对各种注册中心的抽象
  • dubbo-monitor 监控模块: 统计服务调用次数,调用时间,调用链跟踪的服务.
  • dubbo-config 配置模块: 是dubbo对外的api,用户通过config使用dubbo,隐藏dubbo所有细节
  • dubbo-container 容器模块: 是一个standlone的容器,以简单的main加载spring启动,因为服务通常不需要Tomcat/Jboss等web容器的特性,没必要用web容器去加载服务.

Dubbo微服务组件包含三个中心组件:

  • 注册中心,协调Consumer与Provider之间的地址与发现
  • 配置中心
    • 存储Dubbo启动阶段的全局配置,保证配置的跨环境共享与全局一致性
    • 负责服务治理规则(路由规则/动态配置等)的存储与推送
  • 元数据中心
    • 接收Provider上报的服务接口元数据,为Admin等控制台提供运维能力(如服务测试/接口文档等)
    • 也作为注册中心的额外扩展,作为服务发现机制的补充,提供额外的接口/方法级别配置信息的同步能力

可以看到,整体上来看,Dubbo 首先是一款RPC框架,用户在使用 Dubbo 时首先需要定义好 Dubbo 服务,其次,是在将 Dubbo 服务部署上线之后,依赖 Dubbo 的应用层通信协议实现数据交换,再者,Dubbo 所传输的数据都要经过序列化,而这里的序列化协议是完全可扩展的。

然后,在分布式系统中,服务越来越多,应用之间的部署越来越繁杂,用户作为RPC的消费方,如何动态知道服务提供方地址,因此,Dubbo 引入了注册中心来协调提供方和消费方的地址,然后又引入了各种各样的微服务组件例如熔断限流,监控中心等。形成了一个完整的微服务框架。

img

其中

角色 职责 说明
Provider 暴露服务 实现业务接口,启动时向注册中心注册自己的地址
Consumer 调用服务 启动时从注册中心订阅服务,获得提供者列表,发起远程调用
Registry 服务注册与发现 协调 Provider 和 Consumer,推送地址变更(如 Nacos/ZK)
Monitor 调用统计 收集 RPC 调用次数、耗时等指标,用于运维分析(可选)
Container 运行容器 服务运行的宿主环境(Spring Boot、Tomcat、Main 函数等)

那么,上述的流程可以概括为

  1. Provider 启动 → 注册到 Registry
  2. Consumer 启动 → 订阅 Registry → 获取 Provider 列表
  3. Consumer 调用本地代理 → Dubbo 选择一个 Provider 发起网络请求
  4. Provider 执行方法 → 返回结果 → Consumer 接收结果

那么,三大中心通过 RPC 协议,构建了完整的微服务治理体系,它们是这样协同工作的

sequenceDiagram
    participant P as Provider
    participant R as Registry (Nacos)
    participant M as Metadata Center
    participant C as Consumer

    P->>R: 注册应用实例 (user-service@10.0.0.1)
    P->>M: 上报元数据 (接口/方法/参数)

    C->>R: 订阅 user-service
    R-->>C: 返回实例列表 [10.0.0.1]

    C->>M: 拉取 user-service 的元数据
    M-->>C: 返回接口定义

    C->>P: 通过 tri 协议调用 sayHello()
    P-->>C: 返回结果

Dubbo 的 RPC 架构

那么,Dubbo 的 RPC 流程的架构流转如下

  • 使用 Dubbo 的第一步就是定义 Dubbo 服务,服务在 Dubbo 中的定义就是完成业务功能的一组方法的集合
  • 然后定义好服务之后,服务端(Provider)需要提供服务的具体实现,并将其声明为 Dubbo 服务
  • 站在服务消费方(Consumer)的视角,通过调用 Dubbo 框架提供的 API 可以获得一个服务代理(stub)对象,然后就可以像使用本地服务一样对服务方法发起调用了。
  • 在消费端对服务方法发起调用后,Dubbo 框架负责将请求发送到部署在远端机器上的服务提供方,提供方收到请求后会调用服务的实现类,之后将处理结果返回给消费端,这样就完成了一次完整的服务调用。

但这背后,Dubbo 实际上完成了一整套高度抽象且可扩展的 RPC 调用流水线

  • 服务定义和服务契约:Java 接口方式 和 IDL 方式

  • 服务暴露:当 Provider 应用启动时,Dubbo 会自动执行服务暴露(Service Export)流程,这个流程之后等说到 RPC 的时候再说

  • 服务引用:Consumer 启动时,执行服务引用(Service Refer)流程

Dubbo 的注册中心架构

平常我们使用的注册中心还是配置中心,基本都是 Naocs,在 Dubbo 框架下,也是一样的,Dubbo 官方主推方案 就是 Naocs

image-20260201115736292

但是,Dubbo 是一个框架,不是一个具体的中间件,它定义了注册中心、配置中心、元数据中心的 SPI 接口规范

实现了 Dubbo 的注册中心的是 dubbo-registry-* 模块,一般情况下,我们使用dubbo-registry-nacos,而下面要说的配置中心也是一样的

Dubbo 注册中心主要承载的作用是服务注册和服务发现的职责,Dubbo支持接口级别和应用级别的两种粒度的服务发现和服务注册。

img

Dubbo的配置中心架构

整个部署架构中,整个集群内实例(无论是Provider还是Consumer)都会共享该配置中心集群中的配置。

图片

在 Dubbo 中,配置中心(Config Center)承担三个核心职责,具体实现我们还是使用 Nacos 为例子,因为我不熟悉 Zookeeper

职责 说明 传统方式对比
1. 外部化配置 集中存储 dubbo.properties 内容,替代本地配置文件 无需打包修改配置,支持跨环境共享
2. 动态配置 运行时动态修改超时、重试、线程池等参数 无需重启应用,秒级生效
3. 服务治理规则存储 存储路由规则(标签/条件)、降级规则、权重调整等 实现灰度发布、故障隔离、流量调度

具体实现就是 Dubbo 将配置中心视为一个 支持监听(Watch)的 Key-Value 存储系统:

  • Key:由 {namespace}/{group}/{key} 三元组唯一标识;
  • Value:配置内容

Dubbo 通过 SPI 机制 对接不同配置中心

Dubbo的元数据中心架构

Dubbo 的元数据中心是为整个微服务系统提供服务元信息的存储、查询与同步能力

Dubbo 3 会需要一个元数据中心来维护RPC服务与应用的映射关系,即接口与应用的映射关系

img

因为如果采用了应用级别的服务发现和服务注册,在注册中心中将采用“应用 —实例列表”结构的数据组织形式,不再是以往的“接口 —实例列表”结构的数据组织形式,这是什么意思,举个例子:

  • Dubbo 2.x 的时候,每个接口都需要独立注册,注册中心压力大

  • 而 Dubbo 3.x 的时候,一个应用只注册一次:

    这就产生一些额外的问题了,那么,例如 Consumer 要调 UserService.sayHello(),但注册中心只返回了 user-service 的 IP。如何知道 user-service 是否提供了 UserService?其中是不是有 .sayHello()这个方法?参数是什么?

  • 那么,Dubbo 3.x,引入 元数据中心,存储 接口 ↔︎ 应用 的映射关系 + 接口详细元数据。

Dubbo 元数据中心包含 两类核心数据,分别解决不同问题

  1. 地址发现元数据:建立 接口名 → 应用名 的映射关系
  2. 服务运维元数据:描述 一个应用提供的所有接口详情

而且一般来说,未显式配置 metadata-report,Dubbo 自动将注册中心用作元数据中心

还是那句话,优先选择 Nacos,因为它与 Dubbo 生态深度集成

Dubbo的内部模块分层

虽然我不打算深入 Dubbo 的源码,不像我之前讲 Spring Cloud 那样,但了解分层有助于理解扩展点,因为 SPI 是其中重要的内容

层级 模块 职责
Service 业务接口 用户定义的 DemoService
Config 配置层 @DubboService, dubbo.properties
Proxy 代理层 为 Consumer 生成本地代理对象
Registry 注册层 封装注册/发现逻辑
Cluster 集群层 负载均衡、容错、路由
Protocol 协议层 dubbo://, tri:// 等协议实现
Exchange 交互层 Request-Response 模式封装
Transport 传输层 Netty / Grizzly 网络通信
Serialize 序列化层 Hessian2 / JSON / Protobuf

Dubbo的可扩展性

Dubbo 使用插件化的设计模式,这个设计模式具体的体现在其中叫做 SPI

为什么要在系统中强调可扩展性呢?一个很大的原因是,比如一个系统设计好之后,后期又需要加入一些功能代码或者插件,如何最小化改变原有代码加入功能代码或者插件,这个就是扩展性,Dubbo在架构时期也一直重视可扩展性。

开闭原则:对扩展开放,对修改关闭 —— 新功能应通过“新增代码”实现,而非“修改框架源码”。

所以说,Dubbo 很重视框架的扩展性,每个扩展点只封装一个变化因子,最大化复用

Dubbo 完全重写了 SPI 机制,命名为 ExtensionLoader

  • 按需加载
  • IOC
  • AOP
  • 自动激活和适配扩展,而且支持排序
img

使用 Dubbo + Spring Boot 构建微服务项目

这是我们接下来演示如何使用和实践 Dubbo 的核心内容

搭建项目

首先,从一个空的 Maven 项目开始搭建,使用 spring boot 组织为一个微服务项目

image-20260201133713390

其中,服务 user-service 是生产者,order-service 是消费者,dubbo-public-api 是 公共api 模块,定义服务接口,供服务提供者和消费者共享

其中,涉及到的依赖如下

1
2
3
4
5
<!-- Dubbo Spring Boot Starter -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>

一般来说,谁要调用远程服务的接口,就需要加上这个依赖,那么,也就是一般情况下,就是服务消费者需要加上

父模块的 pom 文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<!-- 使用 Spring Boot 3.x + Dubbo 3.x -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.12</version>
<relativePath/>
</parent>

<groupId>hbnu.project</groupId>
<artifactId>DubboSpringBootDemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>DubboSpringBootDemo</name>
<description>Demo project for Dubbo with Nacos</description>
<packaging>pom</packaging>

<modules>
<module>dubbo-public-api</module>
<module>user-service</module>
<module>order-service</module>
</modules>

<properties>
<java.version>21</java.version>
<dubbo.version>3.2.18</dubbo.version>
<nacos.version>2.2.3</nacos.version>
</properties>

<!-- 统一依赖管理 -->
<dependencyManagement>
<dependencies>
<!-- Dubbo BOM -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-bom</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Nacos Client -->
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>${nacos.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>

子的也就是按需要去配置了,我就不放出来了

配置我放出来,服务提供者和服务消费者的配置大差不差,但是服务提供者不用配置 rpc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
server:
port: 8089

spring:
application:
name: order-service

dubbo:
application:
name: ${spring.application.name}
qos-enable: true # 启用 QOS 服务治理
qos-port: 22223 # QOS 端口(与 user-service 不同)

protocol:
name: tri

registry:
address: nacos://127.0.0.1:8848
username: nacos
password: nacos

config-center:
address: nacos://127.0.0.1:8848
username: nacos
password: nacos

metadata-report:
address: nacos://127.0.0.1:8848
username: nacos
password: nacos

consumer:
timeout: 3000 # 消费者超时时间
retries: 2 # 重试次数
loadbalance: roundrobin # 负载均衡策略
cluster: failover # 集群容错策略
check: false # 启动时不检查服务提供者

搭建项目

dubbo-public-api 公共接口模块

首先,在 dubbo-public-api 这个公共接口暴露模块中,我们需要声明使用的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package hbnu.project.dubbopublicapi.api;

import hbnu.project.dubbopublicapi.dto.UserDTO;

/**
* 用户服务接口
* 用于演示 Dubbo RPC 调用
*/
public interface UserService {

/**
* 根据用户ID获取用户信息
*/
UserDTO getUserById(Long userId);

/**
* 根据用户名获取用户信息
*/
UserDTO getUserByName(String username);

/**
* 创建用户
*/
UserDTO createUser(UserDTO userDTO);
}

一般情况下,我习惯是把 DTO 放到公共接口模块中,看你们自己

user-service服务提供者

然后搭建服务提供者,首先,服务提供者需要实现上面我们暴露的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package hbnu.project.userservice.service;

import hbnu.project.dubbopublicapi.api.UserService;
import hbnu.project.dubbopublicapi.dto.UserDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

/**
* 用户服务实现类
* 使用 @DubboService 注解暴露服务
*/
@Slf4j
@Service
@DubboService(
version = "1.0.0",
timeout = 3000,
loadbalance = "roundrobin", // 负载均衡策略:轮询
retries = 2, // 重试次数
cluster = "failover" // 集群容错策略:失败自动切换
)
public class UserServiceImpl implements UserService {

// 模拟数据库存储,懒得新搞数据库了
private final Map<Long, UserDTO> userMap = new HashMap<>();
private final Map<String, UserDTO> userByNameMap = new HashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);

public UserServiceImpl() {
// 初始化一些测试数据
initTestData();
}

private void initTestData() {
UserDTO user1 = new UserDTO(1L, "zhangsan", "zhangsan@example.com", "13800138001", "北京市朝阳区");
UserDTO user2 = new UserDTO(2L, "lisi", "lisi@example.com", "13800138002", "北京市海淀区");

userMap.put(user1.getId(), user1);
userMap.put(user2.getId(), user2);
userByNameMap.put(user1.getUsername(), user1);
userByNameMap.put(user2.getUsername(), user2);
}

@Override
public UserDTO getUserById(Long userId) {
log.info("【服务提供者】收到请求:getUserById, userId={}", userId);

UserDTO user = userMap.get(userId);
if (user == null) {
log.warn("用户不存在:userId={}", userId);
return null;
}

log.info("【服务提供者】返回用户信息:{}", user);
return user;
}

@Override
public UserDTO getUserByName(String username) {
log.info("【服务提供者】收到请求:getUserByName, username={}", username);

UserDTO user = userByNameMap.get(username);
if (user == null) {
log.warn("用户不存在:username={}", username);
return null;
}

log.info("【服务提供者】返回用户信息:{}", user);
return user;
}

@Override
public UserDTO createUser(UserDTO userDTO) {
log.info("【服务提供者】收到请求:createUser, userDTO={}", userDTO);

Long newId = idGenerator.getAndIncrement();
userDTO.setId(newId);

userMap.put(newId, userDTO);
userByNameMap.put(userDTO.getUsername(), userDTO);

log.info("【服务提供者】创建用户成功:{}", userDTO);
return userDTO;
}

}

order-service服务消费者

对于服务消费者,一样要创建服务类,这样能演示 RPC 远程调用和本地调用两种内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package hbnu.project.orderservice.service;

import hbnu.project.dubbopublicapi.dto.OrderDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

/**
* 订单服务
* 演示如何调用远程的 UserService
*/
@Slf4j
@Service
public class OrderService {

private final Map<Long, OrderDTO> orderMap = new HashMap<>();
private final AtomicLong orderIdGenerator = new AtomicLong(1);

/**
* 创建订单(需要调用用户服务验证用户)
*/
public OrderDTO createOrder(Long userId, String productName, Double amount) {
log.info("【订单服务】创建订单:userId={}, productName={}, amount={}", userId, productName, amount);

// 这里会通过 Dubbo 调用远程的 UserService

OrderDTO order = new OrderDTO();
order.setId(orderIdGenerator.getAndIncrement());
order.setUserId(userId);
order.setProductName(productName);
order.setAmount(amount);
order.setCreateTime(LocalDateTime.now());
order.setStatus("待支付");

orderMap.put(order.getId(), order);

log.info("【订单服务】订单创建成功:{}", order);
return order;
}

/**
* 根据订单ID获取订单信息
*/
public OrderDTO getOrderById(Long orderId) {
return orderMap.get(orderId);
}
}

对于控制器,需要使用 @DubboReference 注入远程服务,跟 Feign 不太一样

1
2
3
4
5
6
7
8
9
10
11
// 使用 @DubboReference 注入远程服务
// 配置服务治理参数
@DubboReference(
version = "1.0.0",
timeout = 3000,
loadbalance = "roundrobin", // 负载均衡:轮询
retries = 2, // 重试次数
cluster = "failover", // 集群容错:失败自动切换
check = false // 启动时不检查服务提供者是否可用
)
private UserService userService;

然后编写控制器中对应的接口就可以了,这里省略了

演示服务发现

当然,dubbo 默认在 public 的命名空间中,其中分组也是默认组

image-20260201140833545

这样很明显是不够,本来 Nacos 中什么东西几乎都放到 public 中的默认组,所以,为了更好的服务隔离,我们需要针对其进行相关配置

首先,我们需要为两个服务进行命名空间的隔离

1
2
3
4
5
6
7
8
9
10
11
registry:
address: nacos://127.0.0.1:8848?namespace=08ed0e80-607b-4d6e-8fc9-64d4c33e3713
username: nacos

config-center:
address: nacos://127.0.0.1:8848?namespace=08ed0e80-607b-4d6e-8fc9-64d4c33e3713
username: nacos

metadata-report:
address: nacos://127.0.0.1:8848?namespace=08ed0e80-607b-4d6e-8fc9-64d4c33e3713
username: nacos

有人说这么写不太清晰,但是我习惯了,当然,有人说也可以这么写,对于服务发现,因为namespcae这个配置条在parameters

1
2
3
4
5
6
registry:
address: nacos://127.0.0.1:8848
username: nacos
password: zjm10086
parameters:
namespace: 08ed0e80-607b-4d6e-8fc9-64d4c33e3713

我们重新启动一下项目,来看看服务发现的情况

image-20260201162420379

可以看到服务发现正确的对应到对应的命名空间了

image-20260201162439817

除了 namespace,还可以用 group 对服务进行逻辑分组。

那么,分组的配置也很清晰

1
2
3
4
5
6
dubbo:
registry:
...
parameters:
namespace: 08ed0e80-...
group: USER_GROUP # ← 添加 group

我们曾经在 @DubboService@DubboReference 中都指定了版本号

image-20260201143838878
image-20260201143900467

这个是接口灰度发布的基础,但是具体我好像没用过,不太清楚

最后测试一下 RPC 远程调用,因为服务发现和接口发现是 RPC 远程调用的基础

image-20260201144000231

对于流量管理和服务治理:

对 Nacos 而言,所有流量治理规则和外部化配置都应该是全局可见的,因此相同逻辑集群内的应用都必须使用相同的 namespace 与 group。其中,namespace 的默认值是 public,group 默认值是 dubbo,应用不得擅自修改 namespace 与 group,除非能保持全局一致。

流量治理规则的增删改建议通过 dubbo-admin 完成,

演示配置中心

那么,服务都进行环境隔离了,那么配置中心肯定也要来一个环境隔离才对

那么,配置中心的环境隔离就可以这样写,别忘了 Dubbo 有元数据空间和配置空间

1
2
3
4
5
6
7
8
9
10
11
12
config-center:
address: nacos://127.0.0.1:8848
username: nacos
password: zjm10086
namespace: 08ed0e80-607b-4d6e-8fc9-64d4c33e3713

metadata-report:
address: nacos://127.0.0.1:8848
username: nacos
password: zjm10086
parameters:
namespace: 08ed0e80-607b-4d6e-8fc9-64d4c33e3713

当然你也可以继续进行 Group 的级别的设置

那么,我们当然可以进行进一步的实践,也就是外部化配置,不写死在 application.yml

上面的配置意味着什么,意味着 Dubbo 会从 Nacos 拉取配置,但我还没有在 Nacos 中创建任何 Dubbo 配置项,所以它只用了本地 application.yml 的默认值。

image-20260201143624381

当然,dubbo 是有一个全局配置的

1
2
3
4
dubbo:
config-center:
address: nacos://127.0.0.1:2181
config-file: dubbo.properties # optional

config-file - 全局外部化配置文件 key 值,默认 dubbo.propertiesconfig-file 代表将 Dubbo 配置文件存储在远端注册中心时,文件在配置中心对应的 key 值,通常不建议修改此配置项。

image-20260201162015991

dataId 是 dubbo.properties,group 分组与 config-center 保持一致,如未设置则默认填 dubbo

image-20260201163530222

当然你也可以不停机修改服务配置

这些配置会自动覆盖代码中或 application.yml 中的同名配置!

对服务的配置也一样,也可以创建对应的配置文件,但是通常情况下

因为有元空间的存在,你可以深入到某个指定的接口进行配置,动态调整某个服务在的接口的权重以及其他内容,需开启 service-name-mapping=true

1
2
3
4
5
6
7
8
# 权重路由规则(示例)
configVersion: v2.7
scope: service
key: hbnu.project.dubbopublicapi.api.UserService
configs:
- side: provider
parameters:
weight: 100 # 默认是 100,可调低到 10 做灰度

然后启动我们观察一下配置的情况

首先看到全局配置被加载了

image-20260201162344684

当然,也可以在Java代码中使用注解@NacosValue读取相应的配置参数,如果希望参数值动态刷新,必须设置autoRefreshed为true。

1
2
@NacosValue(value = "${user.id}", autoRefreshed = true)
private String userId;

演示 RPC 调用

以此接口为案例吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* 演示:创建订单(需要先验证用户)
*/
@PostMapping("/create")
public ResponseEntity<Map<String, Object>> createOrder(
@RequestParam Long userId,
@RequestParam String productName,
@RequestParam Double amount) {
log.info("【订单服务】收到请求:创建订单, userId={}, productName={}, amount={}",
userId, productName, amount);

Map<String, Object> result = new HashMap<>();

try {
// 1. 先调用用户服务验证用户是否存在
UserDTO user = userService.getUserById(userId);

if (user == null) {
result.put("success", false);
result.put("message", "用户不存在,无法创建订单");
return ResponseEntity.ok(result);
}

// 2. 创建订单
OrderDTO order = orderService.createOrder(userId, productName, amount);

result.put("success", true);
result.put("message", "订单创建成功");
result.put("order", order);
result.put("user", user);
result.put("callType", "Dubbo RPC 远程调用 + 本地服务");

log.info("【订单服务】订单创建成功:{}", order);

} catch (Exception e) {
log.error("【订单服务】创建订单失败", e);
result.put("success", false);
result.put("message", "创建订单失败:" + e.getMessage());
}

return ResponseEntity.ok(result);
}

然后,启动项目,可以看到服务都是启动成功了,其中 Dubbo 的 RPC 相关内容也起来了

image-20260201140409089

就以上面提到的 Controller 接口来做示例了

image-20260201140647279

那么试一下混合调用,创建一个订单

image-20260201140744416

可以看到,RPC 远程服务接口就这样被启用了

就算不用 Dubbo 的各种服务治理什么的,光是一个服务调用我就感觉比 Feign 要好用