dubbo:是一个基于soa思想的rpc框架

  • SOA 思想:面向服务,把业务拆成可复用、松耦合的服务,通过标准接口对外提供能力。
  • RPC 框架:让程序像调用本地方法一样调用远程服务。

其核心部分包含:

  • 远程通讯:提供对多种基于长连接的NIO框架抽象封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交换方式。
  • 集群容错:提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。
  • 自动发现:基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。

可以先记住一句话:提供者把服务注册到注册中心,消费者去订阅,真正调用时是消费者直接打到提供者,不再经过注册中心。

架构的演变

1794135450968

可以按下面这条线理解这张图:

  • 单体:所有功能在一个工程里,开发简单,但越来越大之后,改一处可能影响全部,也很难按模块扩容。
  • 垂直拆分:按业务线拆成多个应用,比单体好管,但应用之间如果靠互相调数据库或复制代码,还是会缠在一起。
  • 分布式服务 / SOA:把公共能力抽成服务,通过接口对外提供。这时就需要 RPC 框架来做远程调用、注册发现、超时重试和容错。
  • Dubbo 所在的位置:它不负责页面和网关,专注把“服务与服务之间的调用”做好。

架构

5_dubbo的系统架构

五大核心角色

  • Provider(服务提供者):暴露服务的服务提供方。
  • Consumer(服务消费者):调用远程服务的服务消费方。
  • Registry(注册中心):服务注册与发现的中心(通常是 Zookeeper 或 Nacos)。
  • Monitor(监控中心):统计服务的调用次数和调用时间的监控中心。
  • Container(服务容器):负责启动、加载和运行服务提供者的容器(如 Spring 容器)。

核心调用流程

通信方式:虚线(init 初始化)、点划线(async 异步)、实线(sync 同步)。

  • 0. start(启动):服务容器(Container)启动,加载服务提供者(Provider)。
  • 1. register(注册):Provider 启动后,将自己提供的服务注册到 Registry 中。
  • 2. subscribe(订阅):Consumer 启动时,向 Registry 订阅自己需要的服务。
  • 3. notify(通知):Registry 将 Provider 的地址列表异步通知给 Consumer。
  • 4. invoke(调用):Consumer 拿到地址后,直接向 Provider 发起 RPC 同步调用(注意:核心业务调用是直接点对点的,不经过注册中心,保证性能)。
  • 5. count(监控):Consumer 和 Provider 在内存中累计调用次数和时间,定时(异步)发送给 Monitor 监控中心。

再补两点新手很容易误会的地方:

  • 注册中心挂了,已经订阅成功的消费者通常还能用手里的地址列表继续调用,只是看不到新上线或刚下线的机器。所以注册中心很重要,但调用热路径不经过它。
  • 消费者本地会缓存提供者地址。提供者下线时,会主动从注册中心摘掉自己,消费者随后收到通知,把这台机器从列表里去掉。

和 Demo 的关系

Demo 仓库:Sovzn/dubbo-learn

模块 作用
dubbo-api 只放接口,提供者和消费者都依赖它
dubbo-provider 服务实现,启动时会拉起本机内嵌 ZooKeeper(127.0.0.1:2181)
dubbo-consumer 消费者

启动方式:克隆仓库后用 IDEA 打开,先运行 ProviderApplication,再运行 ConsumerApplication,然后打开上面的页面。每个链接对应下面的一个知识点。

项目拆分

正式用 Dubbo 时,不要把接口、实现、调用方塞进同一个模块。常见拆法是三块:

  • api:只放接口和 DTO,谁调用谁依赖。
  • provider:实现接口并暴露服务。
  • consumer:引用服务,写自己的业务。

为什么要这样拆:

  • 消费者不应该依赖提供者的实现类,否则两者又缠在一起,远程调用也就失去意义。
  • 接口独立成 jar 后,提供者和消费者可以分开编译、分开发布。
  • 以后一个接口被多个消费者使用,只要发 api 包即可。

对应 Demo 就是上面三个模块。关键结构如下:

// dubbo-api 里只有接口,没有实现
public interface HelloService {
    String sayHello(String name);
}
<!-- 提供者和消费者的 pom 都只依赖 api,不要互相依赖 -->
<dependency>
    <groupId>com.sovzn</groupId>
    <artifactId>dubbo-api</artifactId>
    <version>1.0.0</version>
</dependency>

快速使用

本 Demo 用的是 Spring Boot 2.7 + Dubbo 2.7.23 + 注解,这是现在学习最常见的组合。大致四步:

1. 父工程统一版本(JDK 8、Spring Boot、Dubbo BOM)。

2. 提供者:实现接口并暴露。

@EnableDubbo
@SpringBootApplication
public class ProviderApplication {
    public static void main(String[] args) {
        EmbeddedZooKeeper.start(2181); // 学习用内嵌 ZK,正式环境请用独立集群
        SpringApplication.run(ProviderApplication.class, args);
    }
}

@DubboService(interfaceClass = HelloService.class, group = "beijing", version = "1.0.0")
public class BeijingHelloServiceImpl implements HelloService {
    public String sayHello(String name) {
        return "你好 " + name;
    }
}

3. 消费者:引用接口,像调本地方法一样调。

@DubboReference(group = "beijing", version = "1.0.0", check = false)
private HelloService beijingHello;

beijingHello.sayHello("Sovzn"); // 看起来像本地调用,实际已经走 RPC

4. 两边都要配注册中心。

dubbo:
  application:
    name: dubbo-provider   # 消费者改成自己的名字
  registry:
    address: zookeeper://127.0.0.1:2181
  scan:
    base-packages: com.sovzn.dubbo.provider.service

浏览器访问 /api/hello,能看到类似 你好 Sovzn,我是 beijing / 1.0.0,协议=dubbo...,就说明通了。

配置方式与优先级

Dubbo 常见三种配置方式,效果等价,混用时要知道谁覆盖谁:

  • XML:老项目常见,<dubbo:service> / <dubbo:reference>。
  • 注解:@DubboService、@DubboReference,本 Demo 用的就是它。
  • 配置文件:application.yml / application.properties,适合写全局默认值。

覆盖关系可以记这句:更精确的配置优先于更宽的配置。

从窄到宽一般是:

  1. 方法级(某个方法的 timeout)
  2. 接口级(某个 @DubboService / @DubboReference)
  3. 全局的 dubbo.provider / dubbo.consumer
  4. 再往外才是默认值

另外还有:

  • 命令行或 JVM 参数(如 -Ddubbo.protocol.port=20882)常用来启动第二实例,优先级很高。
  • 超时这类“调用方要等多久”的配置,以消费者侧最终生效的值为准。 提供者也可以配 timeout,表示自己最多执行多久,两边最好对照着看。

XML 长相(不必在 Demo 里再写一份,认识即可):

<dubbo:application name="dubbo-provider"/>
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<dubbo:protocol name="dubbo" port="20880"/>
<dubbo:service interface="com.sovzn.dubbo.api.HelloService"
               ref="helloService" group="beijing" version="1.0.0"/>
<dubbo:reference id="helloService"
                 interface="com.sovzn.dubbo.api.HelloService"
                 group="beijing" version="1.0.0" check="false"/>

Dubbo 的细节

下面这些都是日常用得最多的“小旋钮”。Demo 里能点的,文中会标出来。

timeout(超时)

调用方最多等多久。超过就放弃,抛超时异常,避免一个慢服务把整个调用链拖死。

@DubboReference(version = "1.0.0", timeout = 2000, retries = 0)
private TradeService tradeService;

Demo:

  • /api/timeout-ok:提供者只睡 200ms,小于 2000,会成功。
  • /api/timeout:提供者睡 5000ms,消费者只等 2000ms,页面会返回超时说明。

retries(重试次数)

总调用次数 = 1 + retries。 retries=2 表示失败后再试 2 次,最多一共 3 次。

非常重要的使用原则:

  • 查询、幂等操作可以重试。
  • 下单、支付、写数据等非幂等操作,retries 建议设 0,否则可能重复提交。

version(版本)

同一个接口可以同时存在 1.0.0 和 2.0.0。消费者写明自己要哪个版本,常用来做灰度升级:新老实现同时在线,切完再下线旧的。

@DubboService(..., group = "beijing", version = "2.0.0")
public class HelloServiceV2Impl implements HelloService { ... }

Demo:/api/hello 走 1.0.0,/api/hello-v2 走 2.0.0。

version="*" 表示任意版本,学习时可用来观察,生产上要谨慎,避免碰巧打到不兼容的实现。

group(分组)

和版本很像,但语义不同:版本多为“同一业务的新旧实现”,分组多为“同一接口的不同业务或不同集群”,例如北京机房 / 上海机房,或订单组 / 售后组。下面“服务分组”会展开,Demo 见 /api/hello-shanghai。

注意:不指定 group、version 时双方都走默认值(group 为空、version 为 0.0.0),同一接口只能有一个实现类,多个实现必须用分组或版本区分,且消费者要引用对上。

不指定时只能有一个实现:

// 提供者不写 group、version,相当于 group=""、version="0.0.0"
@DubboService
public class HelloServiceImpl implements HelloService {
    public String sayHello(String name) {
        return "你好 " + name;
    }
}

// 消费者同样不写,才能对上上面这个默认实现
@DubboReference
private HelloService helloService;

要两个实现同时在线,就必须错开,并且引用要对上:

@DubboService(group = "beijing", version = "1.0.0")
public class BeijingHelloServiceImpl implements HelloService { /* ... */ }

@DubboService(group = "shanghai", version = "1.0.0")
public class ShanghaiHelloServiceImpl implements HelloService { /* ... */ }

@DubboReference(group = "beijing", version = "1.0.0")
private HelloService beijingHello;

@DubboReference(group = "shanghai", version = "1.0.0")
private HelloService shanghaiHello;

如果再写一个也不指定 group、version 的 @DubboService,就会和第一个默认实现抢同一条服务,不建议这样做。

loadbalance(负载均衡)

当同一个服务有多个提供者时,消费者要挑一台来调。内置策略:

策略 含义 什么时候常用
random 随机(可按权重),默认 机器配置接近时最省事
roundrobin 轮询(可按权重) 希望请求尽量均匀
leastactive 调用中数量最少的优先 慢的机器会少接到新请求
consistenthash 一致性哈希,相同参数尽量打到同一台 要会话亲和,或希望同一用户总去同一台

在消费者引用上指定,这是最常用的写法:

@DubboReference(group = "beijing", version = "1.0.0", loadbalance = "roundrobin")
private HelloService helloService;

也可以给所有引用设默认值,单个引用再覆盖:

# application.yml(消费者)
dubbo:
  consumer:
    loadbalance: random

// 这个引用不用默认的 random,改成轮询
@DubboReference(group = "beijing", version = "1.0.0", loadbalance = "roundrobin")
private HelloService helloService;

// 同一个用户总打到同一台,适合有状态或缓存场景
@DubboReference(group = "beijing", version = "1.0.0", loadbalance = "consistenthash")
private HelloService hashedHello;

本 Demo 默认只起一个提供者,所以页面上看不出“换机器”的效果。若要自己试:再启动一个提供者,并用 -Ddubbo.protocol.port=20882 换端口,然后多次刷新 /api/hello。

async(异步调用)

默认是同步:调用线程一直等到结果。异步时,调用先返回 Future,当前线程可以先干别的,稍后再取结果。适合“同时调好几个服务再汇总”的场景。注意异步不要把异常吞掉,取结果时仍要处理超时和失败。

@DubboReference(async = true)
private HelloService helloService;

helloService.sayHello("Sovzn");
String result = RpcContext.getContext().getFuture().get();

delay(延迟暴露)

提供者可以等 Spring 完全启动后再注册。delay=-1 表示等容器启动完成再暴露,避免服务还没准备好就被打进来。本 Demo 的 application.yml 已这样配置。

delay 配在提供者上,建议写在配置里做全局默认,单个服务也可以覆盖:

# 提供者 application.yml
dubbo:
  provider:
    delay: -1   # 等 Spring 启动完成后再注册,避免服务没准备好就被打进来
// 只让这个服务晚 5 秒再暴露(单位是毫秒;-1 同样表示等容器启动完成)
@DubboService(group = "beijing", version = "1.0.0", delay = 5000)
public class BeijingHelloServiceImpl implements HelloService { /* ... */ }

delay = -1 是等 Spring 完全启动后再注册;写成正数则是启动后再额外等这么多毫秒。

token(令牌)

给服务加一个共享令牌,防止未经授权的消费者直连。属于安全加固.

token 配在提供者上,消费者要用同一个值才能调。没带令牌的直连会被拒绝。

# 提供者 application.yml
dubbo:
  provider:
    token: true   # true 表示自动生成令牌,并通过注册中心带给合法消费者

也可以自己指定一个固定令牌:

@DubboService(group = "beijing", version = "1.0.0", token = "sovzn-secret")
public class BeijingHelloServiceImpl implements HelloService { /* ... */ }
# 如果提供者写了固定 token,消费者也要带上同一个值
dubbo:
  consumer:
    token: sovzn-secret
@DubboReference(group = "beijing", version = "1.0.0", token = "sovzn-secret")
private HelloService helloService;

更省事的是提供者写 token: true:消费者走注册中心时会自动拿到令牌,一般不用自己填。它防的是有人绕过注册中心、直接用 url = "dubbo://ip:20880" 来调你的服务。令牌只是一层校验,不能替代鉴权、网络隔离和正式环境的安全方案。

actives / connections(并发与连接)

  • actives:某个方法或服务同时进行中的请求上限,用来做限流,防止把提供者打满。
  • connections / accepts:连接数相关限制。连接是宝贵资源,高并发时要和线程池一起看。

ctives 两边都能配,实际生效的是更小的那个;connections 配在消费者,accepts 配在提供者。

// 提供者:这个服务同时处理中的请求最多 50 个,超出就排队/报错
@DubboService(group = "beijing", version = "1.0.0", actives = 50)
public class BeijingHelloServiceImpl implements HelloService { /* ... */ }
// 消费者:对这个引用,同时进行中的调用最多 10 个
@DubboReference(group = "beijing", version = "1.0.0", actives = 10, connections = 2)
private HelloService helloService;

也可以写在配置里:

# 提供者 application.yml
dubbo:
  provider:
    actives: 50      # 每个服务同时处理中的请求上限
    accepts: 100     # 这个提供者最多接受多少条客户端连接
  protocols:
    dubbo:
      port: 20880
      accepts: 100   # 也可以写在协议上,限制这条协议端口的接入连接数
      threads: 50    # 业务线程池大小,要和 actives、连接数一起看
# 消费者 application.yml
dubbo:
  consumer:
    actives: 10      # 每个引用同时进行中的请求上限
    connections: 2   # 对每个提供者建立几条长连接,默认 dubbo 协议通常是 1 条共享连接

actives 限的是“正在处理的请求数”,用来保护线程池不要被打满;connections 是消费者向每台提供者开几条连接;accepts 是提供者最多接多少条进来的连接。连接数开太大没意义,要和协议线程池(threads)匹配。

文档与注册信息

@DubboService 还可以写 document、方法参数说明等,这些会进元数据,方便运维查看。不是运行必需项。

启动时检查

配置项是 check。

  • check=true(比较严格):消费者启动时,如果某个引用在注册中心找不到任何提供者,启动直接失败。好处是“没准备好就不要上线”。
  • **check=false**:即使提供者还没起来,消费者也能先启动,等调用时再找服务。本地调试非常方便。
dubbo:
  consumer:
    check: false

也可以只给某一个引用关闭检查:

@DubboReference(group = "beijing", version = "1.0.0", check = false)

本 Demo 全局和单个引用都用了 check=false,所以允许你先起消费者再起提供者(虽然推荐仍是先起提供者)。

注意:check 管的是启动期有没有地址,不保证运行期每一次调用都成功。运行期失败要靠超时、重试和集群策略处理。

集群容错

当调用失败时,Dubbo 用 cluster 决定下一步怎么做。默认是 failover。

策略 行为 适合场景
failover 失败后重试,可能换一台机器。retries 控制再试几次 读操作、幂等调用(默认)
failfast 失败立即抛错,不重试 新增、支付等非幂等写操作
failsafe 失败只记日志,返回空结果,不抛异常 记录日志、发通知等“失败可忽略”
failback 失败后在后台定时重试 消息补偿、最终要成功但不挡当前请求
forking 同时并行调用多台,一台成功就返回 对实时性要求很高的读,比较耗资源
broadcast 广播给所有提供者,任意一台失败则失败 通知所有机器更新缓存、刷新本地状态

和 retries 的关系:

  • failover 必须配合 retries 才有“再试几次”的效果。
  • failfast 时把 retries 设为 0,语义更清晰。
  • 重试会放大流量:一次用户请求内部可能变成 3 次远程调用,下游压力要算进去。

Demo 里 TradeService.unstable() 会按“失败、失败、成功”循环:

  • /api/failover:cluster=failover, retries=2,一次点击内部最多试 3 次,通常能成功。
  • /api/failfast:第一次失败就立刻把错误返回到页面。
@DubboReference(cluster = "failover", retries = 2)
private TradeService failoverTrade;

@DubboReference(cluster = "failfast", retries = 0)
private TradeService failfastTrade;

服务降级与本地存根

容错解决的是“偶尔失败怎么挺过去”,降级解决的是“依赖挂了或太慢时,我能不能先给一个保底结果”。

mock(本地伪装 / 降级)

消费者可以给引用配置 mock。远程调用失败或空结果时 (超时、连不上、没有提供者;提供者里抛出的业务异常(包括空指针)会直接回给消费者,不会自动走 Mock。),改为走本地逻辑,例如返回缓存或默认值,避免把异常直接抛到用户脸上。

@DubboReference(mock = "fail:return null")  // 失败时返回 null,最简单的写法

也可以写自己的 mock 类,类名约定为接口名 + Mock,并实现同一接口:

public class HelloServiceMock implements HelloService {
    public String sayHello(String name) {
        return "服务暂时不可用,这是降级结果";
    }
}
@DubboReference(mock = "true") // 失败时找 HelloServiceMock

mock=force:return xxx 表示强制走伪装、根本不调远程,适合临时屏蔽一个下游。

约定名 接口名 + Mock 只能有一个,比如 HelloServiceMock。失败时 mock = "true" 只会去找这一个类,不能靠再写一个同名约定类来切换。

如果要两套降级逻辑,就各自写成普通类,在引用上写全类名指定用哪一个:

public class HelloServiceBusyMock implements HelloService {
    public String sayHello(String name) {
        return "现在太忙,请稍后再试";
    }
}

public class HelloServiceDownMock implements HelloService {
    public String sayHello(String name) {
        return "服务暂时不可用,这是降级结果";
    }
}
@DubboReference(group = "beijing", version = "1.0.0",
        mock = "com.sovzn.dubbo.api.mock.HelloServiceBusyMock")
private HelloService helloService;

@DubboReference(group = "shanghai", version = "1.0.0",
        mock = "com.sovzn.dubbo.api.mock.HelloServiceDownMock")
private HelloService shanghaiHello;

mock = "true" 仍然只对应那个约定的 HelloServiceMock。强制不调远程时也可以写成 mock = "force:com.sovzn.dubbo.api.mock.HelloServiceDownMock"。这些 Mock 类要让消费者能加载到,一般放在 dubbo-api 里,或单独做成消费者能依赖到的模块。

stub(本地存根)

stub 是在真正发起远程调用之前先走进消费者本地的一段代码。适合做参数校验、缓存、或先判断“根本不必远程调用”。它和 mock 的差别:

  • stub:调用前介入,常用于包装、校验。

stub 是消费者本地的包装类:远程调用前先走到它,适合做参数校验、缓存或先拦一层。约定类名是接口名 + Stub,或者自己写全类名。

public class HelloServiceStub implements HelloService {

    private final HelloService helloService; // Dubbo 传入的远程代理

    // 必须有这个构造器,参数就是真正的远程接口
    public HelloServiceStub(HelloService helloService) {
        this.helloService = helloService;
    }

    @Override
    public String sayHello(String name) {
        if (name == null || name.trim().isEmpty()) {
            return "名字不能为空,本地直接拦截,没有打到提供者";
        }
        return helloService.sayHello(name);
    }
}
@DubboReference(group = "beijing", version = "1.0.0", stub = "true")
// 等价于 stub = "com.sovzn.dubbo.api.HelloServiceStub"
private HelloService helloService;

和 mock 不同:stub 是每次调用都会先经过本地类,远程通不通都走它;mock 是远程失败(或 force)后才降级。Stub 类同样要让消费者能加载到,通常放在 dubbo-api 里。

  • mock:远程失败后介入,常用于降级。

线程模型

提供者收到请求后,要决定:IO 线程只负责收发,还是连业务一起跑?业务又用哪种线程池?

这由两个配置共同决定:dispatcher(消息如何分发)和 threadpool(业务线程池类型)。

dispatcher

值 含义
all 所有消息都交给业务线程池,IO 线程尽量只做读写。Demo 使用这个,也最常见
direct IO 线程直接执行业务。业务必须非常轻,否则 IO 会被堵住
message 只有请求/响应交给业务线程,连接、心跳等仍在 IO 线程
execution 只有请求交给业务线程,响应仍在 IO 线程处理
connection 连接上的事件放到单独线程池,其它在 IO 线程

threadpool

值 含义
fixed 固定大小线程池,队列满了会拒绝,行为可预期。Demo 使用它
cached 不够就加线程,空闲会回收,突发流量时要防止线程打满
limited 只增不减,有上限
eager 先造线程再入队,更积极地应对突发
dubbo:
  protocols:
    dubbo:
      dispatcher: all
      threadpool: fixed
      threads: 50

/api/ping 的返回值里带有提供者线程名,通常能看到业务跑在 Dubbo 的线程池里,而不是 Tomcat 或主线程。

调优时记住:线程不是越多越好。池子太大,机器会在上下文切换上浪费时间;太小,请求会排队甚至超时。要结合 actives、超时和下游耗时一起看。

直连提供者

正常情况消费者只认“服务是谁”,具体打哪一台由注册中心和负载均衡决定。但调试时你可能想写死某一台:

@DubboReference(group = "beijing", version = "1.0.0",
        url = "dubbo://127.0.0.1:20880")
private HelloService directHello;

特点:

  • 这次引用不再靠注册中心挑选地址。
  • 很适合本地调试、临时绕过注册中心,或明确要打某台出问题的机器。
  • 生产上不要把直连写死在代码里,否则扩缩容和故障转移都会失效。

Demo:/api/direct。

多协议

协议决定数据包怎么写、走哪种连接。常见的有:

协议 特点
dubbo 默认,长连接、NIO、二进制,性能好,最常用
hessian 走 HTTP,穿越网关/防火墙更方便,性能一般不如 dubbo
http / rest 更偏 Web,方便非 Java 调用方
rmi 老协议,现在很少首选
triple(Dubbo 3) 基于 HTTP/2 + protobuf,兼容 gRPC,跨语言更友好

同一个服务可以同时用多种协议暴露:

@DubboService(..., protocol = {"dubbo", "hessian"})
public class BeijingHelloServiceImpl implements HelloService { ... }
dubbo:
  protocols:
    dubbo:
      name: dubbo
      port: 20880
    hessian:
      name: hessian
      port: 20881

消费者既可以通过注册中心用 dubbo 协议调用,也可以直连 hessian:

@DubboReference(url = "hessian://127.0.0.1:20881", ...)

Demo:/api/hello 一般是 dubbo 协议,/api/hessian 走 hessian,返回值里的 协议= 可以看到差别。

服务分组和多版本

这两个经常一起出现,但不要混:

  • group:把同一接口分成不同的业务或集群。消费者必须声明自己要哪一组。
  • version:同一组里的新旧实现共存,做灰度。

本 Demo 同时有三套 HelloService:

实现类 group version 谁在调用
BeijingHelloServiceImpl beijing 1.0.0 /api/hello
ShanghaiHelloServiceImpl shanghai 1.0.0 /api/hello-shanghai
HelloServiceV2Impl beijing 2.0.0 /api/hello-v2

关键点:

  • 提供者和消费者的 接口 + group + version 必须对得上,少写一个就可能 No provider。
  • 需要调用多组时,就声明多个 @DubboReference,不要指望一个引用自动打到所有组。
  • 分组也可以配合负载均衡、不同的超时和线程池,做出“核心链路更严格、次要链路更宽松”。

泛化引用

普通引用要求消费者编译期就有接口 class。泛化引用则只需要接口全名、方法名和参数类型,用 GenericService.$invoke 调用。

适合:

  • 网关统一转发,不可能为每个后端服务都引入 api 包
  • 测试平台、运维工具
  • 调用方暂时还拿不到 api 包
@DubboReference(interfaceName = "com.sovzn.dubbo.api.HelloService",
        group = "beijing", version = "1.0.0", generic = true, check = false)
private GenericService genericHello;

Object result = genericHello.$invoke(
        "sayHello",
        new String[]{"java.lang.String"},
        new Object[]{"Sovzn"});

Demo:/api/generic。

对应还有泛化实现(提供者也不事先实现具体接口,按参数动态处理),多用于做统一适配层,入门阶段先把“泛化引用”用熟即可。

负载均衡再记一句

前面表格已经列出四种策略。实践中再记两点:

  • 可以给提供者设 weight(权重),随机和轮询都会按权重倾斜,配置高的机器多接请求。

权重写在提供者上。机器更好、实例更能扛量,就把 weight 调大:

// 这台机器性能更好,接更多请求。默认权重是 100
@DubboService(group = "beijing", version = "1.0.0", weight = 200)
public class BeijingHelloServiceImpl implements HelloService {
    // ...
}

也可以写在提供者的配置里,作为这个应用里所有服务的默认权重:

# 提供者 application.yml
dubbo:
  provider:
    weight: 200

多台实例时,比如 A 的 weight = 200、B 的 weight = 100,random 和 roundrobin 都会让 A 大约接到 2/3 的请求、B 接到 1/3。消费者不用改,选地址时会读到注册中心里带过来的权重。

  • 一致性哈希要选对“用哪个参数做 hash”,否则看起来“随机”,亲和性就失效了。

隐式传参 RpcContext

有些数据不想改接口签名,但又要带到对端,例如 traceId、登录用户、来源机房。可以用 RpcContext 做隐式传参:

// 消费者在调用前放入
RpcContext.getContext().setAttachment("traceId", "abc-123");
helloService.sayHello("Sovzn");

// 提供者在方法里取出
String traceId = RpcContext.getContext().getAttachment("traceId");

本 Demo 的北京实现里用 RpcContext 读取了当前协议,并把线程名一并返回,就是同一套 API。

注意:

  • attachment 要控制体积,不要当传输大对象的通道。
  • 一次调用结束后 Context 会清理,别当成任意时刻都能取的全局变量。
  • 异步调用时要小心 Context 传递,必要时先把 Future 拿出来。

序列化

RPC 要把内存里的对象变成字节发到网上,对端再还原回来,这就是序列化。Dubbo 2.7 默认常用 hessian2。

选择时看三件事:性能、兼容性、安全性。注意:

  • 接口里的参数和返回值必须能被序列化,尽量用普通 JavaBean、基本类型和常用集合。
  • 双方的 api 包要兼容:字段只增不删更安全,随便改字段名或类型会导致反序列化失败。
  • 不要随便打开不信任来源的序列化协议,避免安全问题。

注册中心再理解

Demo 用的是 ZooKeeper。这里只补和 Dubbo 相关的部分:

  • 提供者启动时把“我提供哪个接口、地址、分组、版本、协议”写到 ZK。
  • 消费者订阅后,ZK 把地址列表推过来;之后有机器上线或下线,会再通知一次。
  • 调用不经过 ZK,ZK 只解决“去哪找人”。
  • 本地也会缓存地址,所以 ZK 短暂不可用,已启动的消费者往往还能继续调。

除了 ZK,常见还有 Nacos、Redis、Multicast。Multicast 不用额外中间件,但在 Windows 或复杂网络里经常不稳,只适合极简单的试验。学习阶段用 ZK 就够了。

本 Demo 在提供者启动时内嵌了一个 ZK,避免你还没安装就跑不起来。端口 2181 已被占用时,会自动复用本机已有的 ZK。生产环境请使用独立集群,不要把注册中心嵌在业务进程里。

Filter 与 SPI

Filter(过滤器)

一次 RPC 会经过一串过滤器,很像 Web 里的拦截器:记录日志、做鉴权、传 traceId、统计耗时,都可以做成 Filter。Dubbo 自己的超时、Token 等也是通过扩展机制挂上去的。

入门阶段先建立这个印象:你在业务方法里看到的那一下调用,前后其实还有框架层的责任链。

SPI(可插拔扩展)

Dubbo 的负载均衡、集群策略、协议、序列化、过滤器,几乎都是可替换的。它使用自己的 SPI:在 META-INF/dubbo/ 下写配置,就能接入自定义实现。

你不一定马上要写扩展,但要明白:遇到“我想换一种负载策略 / 加一个调用日志”时,优先找已有扩展点,而不是改框架源码。

路由与动态配置

除了代码和本地 yml,还可以通过注册中心或配置中心,在运行时调整:

  • 某台消费者暂时只调用某几台提供者(路由)
  • 临时加大超时、关掉重试、把某个服务强制降级(动态配置)

这就是架构图里“动态配置”的含义。本地 Demo 没有接配置中心,但你要知道:很多线上调整并不需要重新发版。

QoS

QoS 是 Dubbo 带的本机运维端口,可以查看已发布服务、进行上下线等。本 Demo 中:

  • 提供者:22222
  • 消费者:22223

务必 qos-accept-foreign-ip: false,只允许本机访问,不要对公网开放。

参数回调与事件通知

这两个相对少用,知道有即可:

  • 参数回调:接口方法里带一个回调参数,提供者在执行过程中可以反过来调消费者。适合“服务端主动推一点进度”的场景,但会让调用关系变复杂。
  • 事件通知:在 oninvoke / onreturn / onthrow 里挂钩子,调用开始、成功、失败时执行额外逻辑。

日常业务优先把接口设计简单,用普通返回值或消息队列,而不是一上来就上回调。

Dubbo 3 你会听到的变化

本 Demo 和大多数经典教程一样,用的是 Dubbo 2.7。以后看资料时可能会碰到 Dubbo 3,先有个印象:

  • 应用级服务发现:不再只按“接口”注册,还可以按应用发现,减轻注册中心压力。
  • Triple 协议:基于 HTTP/2,兼容 gRPC,跨语言更自然。
  • 和 Spring Boot、云原生的整合更紧,配置项名称也有些变化。

核心概念没有变:依然是提供者、消费者、注册中心、超时、容错、负载均衡。先把 2.7 用熟,再迁 3 会轻松很多。

常见坑

  • 接口、group、version 对不上:最常见的 No provider available。先核对这三项,再看注册中心连没连上。
  • 先起消费者再起提供者,且 check=true:启动直接失败。本地学习用 check=false。
  • retries 用在写操作上:失败重试导致重复下单。写操作把 retries 设为 0,或保证接口幂等。
  • 只改了提供者 timeout,消费者仍按自己的值等待:超时要以消费者侧生效配置为主,两边对照着改。
  • 直连写死在生产配置:扩容、迁移都会失效。直连只用于调试。
  • 2181 / 20880 / 8080 被占用:内嵌 ZK 遇到 2181 被占用会尝试复用;Dubbo 端口和消费者 Web 端口被占用则需先关掉旧进程。
  • 序列化失败:api 包版本不一致,或传了不能序列化的对象。
  • 非幂等 + failover:和 retries 是同一个坑,换机器重试也可能重复执行。