核心概念

服务发现路由是 Dubbo 服务调用链路中的两个不同阶段,它们解决不同的问题:

  • 服务发现(Service Discovery):解决”有哪些服务提供者可用”的问题
  • 路由(Routing):解决”选择哪个服务提供者”的问题

核心区别

  • 服务发现静态的,获取全量的可用 Provider 列表
  • 路由动态的,根据规则从 Provider 列表中筛选、排序

服务发现(Service Discovery)

1. 核心职责

服务发现负责

  • Provider 启动时,向注册中心注册服务
  • Consumer 启动时,从注册中心订阅服务
  • 实时感知 Provider 的上线/下线
  • 维护全量的可用 Provider 列表

2. 实现机制

// ==================== Provider 端:服务注册 ====================
@Service
public class UserServiceImpl implements UserService {
    // 启动时,向注册中心注册服务
}

// 注册的信息:
// dubbo://192.168.1.100:20880/com.example.UserService
//   ?version=1.0.0
//   &group=default
//   &timeout=3000
//   &serialization=hessian2
//   &methods=getUser,updateUser

// ==================== Consumer 端:服务订阅 ====================
@Reference
private UserService userService;

// 启动时,向注册中心订阅服务
// 获取所有 Provider 的地址列表:
// [
//   dubbo://192.168.1.100:20880/com.example.UserService,
//   dubbo://192.168.1.101:20880/com.example.UserService,
//   dubbo://192.168.1.102:20880/com.example.UserService
// ]

// ==================== 动态感知 ====================
// Provider 下线 → 注册中心通知 Consumer → 从列表中移除
// Provider 上线 → 注册中心通知 Consumer → 添加到列表中

3. 服务发现流程

// Consumer 启动流程
public class ServiceDiscoveryProcess {
    
    public void start() {
        // 1. 连接注册中心
        Registry registry = registryFactory.getRegistry(registryUrl);
        
        // 2. 订阅服务
        registry.subscribe(
            URL.valueOf("consumer://192.168.1.200/com.example.UserService"),
            new NotifyListener() {
                @Override
                public void notify(List<URL> urls) {
                    // 3. 接收 Provider 列表
                    // urls = [
                    //   dubbo://192.168.1.100:20880/UserService,
                    //   dubbo://192.168.1.101:20880/UserService,
                    //   dubbo://192.168.1.102:20880/UserService
                    // ]
                    
                    // 4. 更新本地 Provider 列表
                    updateProviders(urls);
                    
                    // 5. 建立连接
                    for (URL url : urls) {
                        createConnection(url);
                    }
                }
            }
        );
    }
    
    // 实时感知 Provider 变化
    public void handleProviderChange(List<URL> newUrls) {
        // 对比新旧列表
        // 下线的 Provider → 关闭连接
        // 上线的 Provider → 建立连接
    }
}

4. 注册中心的作用

// Zookeeper 结构示例
/dubbo
  /com.example.UserService
    /providers
      /dubbo%3A%2F%2F192.168.1.100%3A20880%2FUserService  (临时节点)
      /dubbo%3A%2F%2F192.168.1.101%3A20880%2FUserService  (临时节点)
      /dubbo%3A%2F%2F192.168.1.102%3A20880%2FUserService  (临时节点)
    /consumers
      /consumer%3A%2F%2F192.168.1.200%2FUserService  (临时节点)
    /configurators  (配置规则)
    /routers  (路由规则)

// Provider 下线
// 1. Provider 进程退出
// 2. Zookeeper 检测到心跳丢失
// 3. 删除临时节点
// 4. 触发 Watch 机制
// 5. 通知所有订阅的 Consumer
// 6. Consumer 从列表中移除该 Provider

路由(Routing)

1. 核心职责

路由负责

  • 从全量 Provider 列表中筛选符合条件的
  • 根据规则排序分组
  • 实现流量控制(灰度、AB测试、地域就近)
  • 不改变 Provider 列表的可用性,只影响选择

2. 路由类型

(1)条件路由(Condition Router)

// 规则格式:condition://0.0.0.0/com.example.UserService
//   ?category=routers
//   &rule=<条件> => <结果>

// 示例 1:VIP 用户路由到高性能服务器
// consumer.vip = true => host = 192.168.1.100

// 示例 2:测试环境路由到测试服务器
// consumer.env = test => host = 192.168.1.101

// 示例 3:按地域就近路由
// consumer.region = beijing => provider.region = beijing

// 示例 4:强制路由(排除某些 Provider)
// => host != 192.168.1.102
// 代码实现
public class ConditionRouter implements Router {
    
    @Override
    public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, Invocation invocation) {
        // 1. 解析条件
        String vip = RpcContext.getContext().getAttachment("vip");
        
        if ("true".equals(vip)) {
            // 2. 筛选符合条件的 Provider
            return invokers.stream()
                .filter(invoker -> "192.168.1.100".equals(invoker.getUrl().getHost()))
                .collect(Collectors.toList());
        }
        
        // 3. 不满足条件,返回所有
        return invokers;
    }
}

(2)标签路由(Tag Router)

// Provider 配置标签
@Service(tag = "gray")
public class UserServiceGrayImpl implements UserService {
    // 灰度环境
}

@Service(tag = "stable")
public class UserServiceImpl implements UserService {
    // 稳定环境
}

// Consumer 指定标签
RpcContext.getContext().setAttachment("tag", "gray");
User user = userService.getUser(123L);  // 路由到灰度环境

// 路由逻辑
public class TagRouter implements Router {
    
    @Override
    public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, Invocation invocation) {
        String requestTag = RpcContext.getContext().getAttachment("tag");
        
        if (requestTag != null) {
            // 筛选匹配标签的 Provider
            List<Invoker<T>> taggedInvokers = invokers.stream()
                .filter(invoker -> requestTag.equals(invoker.getUrl().getParameter("tag")))
                .collect(Collectors.toList());
            
            if (!taggedInvokers.isEmpty()) {
                return taggedInvokers;
            }
        }
        
        // 没有匹配标签,返回无标签的 Provider
        return invokers.stream()
            .filter(invoker -> invoker.getUrl().getParameter("tag") == null)
            .collect(Collectors.toList());
    }
}

(3)脚本路由(Script Router)

// 使用 JavaScript 脚本自定义路由
function route(invokers, invocation) {
    var result = new java.util.ArrayList();
    
    // 获取请求参数
    var userId = invocation.getArguments()[0];
    
    // 自定义路由逻辑:按用户 ID 哈希分流
    var targetRegion = (userId % 2 == 0) ? "regionA" : "regionB";
    
    for (var i = 0; i < invokers.size(); i++) {
        var invoker = invokers.get(i);
        var region = invoker.getUrl().getParameter("region");
        
        if (region == targetRegion) {
            result.add(invoker);
        }
    }
    
    return result;
}

3. 路由链

Dubbo 支持多个路由规则组成路由链:

// 路由链执行顺序
public class RouterChain {
    
    private List<Router> routers = Arrays.asList(
        new ConditionRouter(),  // 条件路由
        new TagRouter(),        // 标签路由
        new ScriptRouter()      // 脚本路由
    );
    
    public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, Invocation invocation) {
        List<Invoker<T>> result = invokers;
        
        // 依次执行每个路由规则
        for (Router router : routers) {
            result = router.route(result, invocation);
            
            // 如果某个路由规则筛选后为空,终止路由链
            if (result.isEmpty()) {
                break;
            }
        }
        
        return result;
    }
}

// 执行流程示例
// 初始 Provider 列表:[A, B, C, D, E]
// 
// 1. ConditionRouter 筛选
//    条件:consumer.region = beijing
//    结果:[A, B, C]  (只有 A、B、C 在北京)
// 
// 2. TagRouter 筛选
//    条件:tag = gray
//    结果:[A, B]  (只有 A、B 是灰度标签)
// 
// 3. ScriptRouter 筛选
//    条件:userId % 2 == 0
//    结果:[A]  (只有 A 符合自定义规则)
// 
// 最终:从 [A] 中进行负载均衡

服务发现 vs 路由对比

1. 核心区别

维度 服务发现 路由
目标 获取全量 Provider 列表 从列表中筛选符合条件的
时机 服务启动、Provider 上下线 每次 RPC 调用前
频率 低频(Provider 变化时) 高频(每次调用)
依赖 注册中心(Zookeeper、Nacos) 本地内存(路由规则)
作用范围 所有 Consumer 单次调用
可配置性 自动(框架完成) 灵活(规则可配置)

2. 调用链路中的位置

// ==================== 完整调用流程 ====================

// 1. 服务发现阶段(启动时)
@Reference
private UserService userService;
// ↓
// Consumer 启动,订阅服务
// 从注册中心获取 Provider 列表:
// [Provider-A, Provider-B, Provider-C, Provider-D, Provider-E]

// 2. 调用阶段(运行时)
User user = userService.getUser(123L);
// ↓
// 【路由阶段】从 Provider 列表中筛选
// 条件路由 → [Provider-A, Provider-B, Provider-C]
// 标签路由 → [Provider-A, Provider-B]
// ↓
// 【负载均衡】从筛选后的列表中选择一个
// 负载均衡策略(LeastActive) → Provider-A
// ↓
// 【发起 RPC 调用】
// 调用 Provider-A 的 getUser 方法

3. 职责划分

// 服务发现的职责
public interface ServiceDiscovery {
    // 注册服务
    void register(URL url);
    
    // 注销服务
    void unregister(URL url);
    
    // 订阅服务(获取全量 Provider 列表)
    void subscribe(URL url, NotifyListener listener);
    
    // 取消订阅
    void unsubscribe(URL url, NotifyListener listener);
    
    // 查询服务(返回所有可用 Provider)
    List<URL> lookup(URL url);
}

// 路由的职责
public interface Router {
    // 从 Provider 列表中筛选符合条件的
    <T> List<Invoker<T>> route(List<Invoker<T>> invokers, Invocation invocation);
    
    // 路由规则优先级
    int getPriority();
}

实际应用场景

场景 1:灰度发布

// 1. 服务发现:获取所有 Provider
// [
//   Provider-A (stable),
//   Provider-B (stable),
//   Provider-C (gray, version=2.0)
// ]

// 2. 路由:根据标签筛选
@Reference
private UserService userService;

// 普通用户 → 路由到 stable
User user1 = userService.getUser(123L);
// 路由结果:[Provider-A, Provider-B]

// 灰度用户 → 路由到 gray
RpcContext.getContext().setAttachment("tag", "gray");
User user2 = userService.getUser(456L);
// 路由结果:[Provider-C]

场景 2:跨机房调用

// 1. 服务发现:获取所有 Provider
// [
//   Provider-A (region=beijing),
//   Provider-B (region=beijing),
//   Provider-C (region=shanghai)
// ]

// 2. 路由:就近路由
// 北京的 Consumer → 路由到北京的 Provider
RpcContext.getContext().setAttachment("region", "beijing");
User user = userService.getUser(123L);
// 路由结果:[Provider-A, Provider-B]

// 3. 负载均衡:从 [Provider-A, Provider-B] 中选择一个

场景 3:AB 测试

// 1. 服务发现:获取所有 Provider
// [Provider-A (v1), Provider-B (v1), Provider-C (v2)]

// 2. 路由:根据用户分组
public class ABTestRouter implements Router {
    
    @Override
    public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, Invocation invocation) {
        Long userId = (Long) invocation.getArguments()[0];
        
        // 10% 用户体验新版本
        if (userId % 10 == 0) {
            // 路由到 v2
            return invokers.stream()
                .filter(i -> "v2".equals(i.getUrl().getParameter("version")))
                .collect(Collectors.toList());
        } else {
            // 路由到 v1
            return invokers.stream()
                .filter(i -> "v1".equals(i.getUrl().getParameter("version")))
                .collect(Collectors.toList());
        }
    }
}

最佳实践

1. 服务发现配置

dubbo:
  registry:
    # 注册中心地址
    address: nacos://127.0.0.1:8848
    
    # 注册超时时间
    timeout: 5000
    
    # 会话超时时间
    session: 60000
    
    # 是否注册(Provider)
    register: true
    
    # 是否订阅(Consumer)
    subscribe: true
    
    # 是否动态注册(false 表示不会被自动下线)
    dynamic: true

2. 路由规则配置

# 通过 Dubbo Admin 动态配置
# 或通过配置中心(Nacos、Apollo)

# 条件路由示例
dubbo:
  router:
    condition:
      - "consumer.region = beijing => provider.region = beijing"
      - "consumer.vip = true => provider.host = 192.168.1.100"
    
    # 标签路由
    tag:
      enabled: true
      force: false  # 找不到匹配标签时,是否强制返回空(false 表示降级到无标签)

3. 混合使用

// 服务发现 + 路由 + 负载均衡 + 容错
@Reference(
    // 服务发现(自动)
    registry = "nacos://127.0.0.1:8848",
    
    // 路由(规则配置)
    // 通过 Dubbo Admin 配置条件路由、标签路由
    
    // 负载均衡
    loadbalance = "leastactive",
    
    // 容错
    cluster = "failover",
    retries = 2
)
private UserService userService;

答题总结

服务发现与路由的核心区别

1. 概念差异

  • 服务发现:解决”有哪些 Provider 可用”(获取全量列表)
  • 路由:解决”选择哪个 Provider”(从列表中筛选)

2. 执行时机

  • 服务发现:启动时、Provider 上下线时(低频)
  • 路由:每次 RPC 调用前(高频)

3. 数据来源

  • 服务发现:注册中心(Zookeeper、Nacos)
  • 路由:本地内存(路由规则)

4. 调用链路

服务发现 → 获取全量 Provider 列表
  ↓
路由 → 根据规则筛选
  ↓
负载均衡 → 选择一个 Provider
  ↓
发起 RPC 调用

5. 典型应用

  • 服务发现:Provider 上下线感知、健康检查
  • 路由:灰度发布、AB 测试、跨机房就近调用

6. 路由类型

  • 条件路由:基于条件表达式筛选
  • 标签路由:基于标签匹配
  • 脚本路由:自定义脚本逻辑

面试技巧:强调服务发现和路由是调用链路的不同阶段,服务发现是静态的全量获取,路由是动态的规则筛选。可以画出调用链路图,说明两者的协作关系,体现对分布式系统的深入理解。