【分布式架构设计&微服务深入剖析03】之高性能RPC框架Apache Dubbo

1、项目架构演变过程

1.1 单体架构

  1. 描述:单体架构所有模块和功能都集中在一个项目中,部署时也是将项目所有功能部整体署到服务器中

  2. 架构图
    在这里插入图片描述

  3. 优点
    1)小项目开发快 成本低
    2)架构简单
    3)易于测试
    4)易于部署

  4. 缺点
    1)大项目模块耦合严重不易开发维护沟通成本高
    2)新增业务困难
    3)核心业务与边缘业务混合在一块,出现问题互相影响

1.2 垂直架构

  1. 描述:根据业务把项目垂直切割成多个项目

  2. 架构图
    在这里插入图片描述

  3. 优点
    1)系统拆分实现了流量分担,解决了并发问题
    2)可以针对不同系统进行优化
    3)方便水平扩展,负载均衡,容错率提高
    4)系统间相互独立,互不影响,新的业务迭代时更加高效

  4. 缺点
    1)服务系统之间接口调用硬编码
    2)搭建集群之后,实现负载均衡比较复杂
    3)服务系统接口调用监控不到位,调用方式不统一
    4)服务监控不到位
    5)数据库资源浪费,充斥慢查询,主从同步延迟大

1.3 分布式架构(SOA)

  1. 描述:SOA全称为Service Oriented Architecture,即面向服务的架构,是在垂直划分的基础上,将每个项目拆分出多个具备松耦合的服务,一个服务通常以独立的形式存在于操作系统进程中。各个服务之间通过网络调用,这使得构建在各种各样的系统中的服务可以 以一种统一和通用的方式进行交互

  2. 架构图
    在这里插入图片描述
    说明:
    1)分层: 按照业务性质分层,每一层要求简单和容易维护
    2)分级:按照业务性质分层,同一层的业务也要做好分级 依据业务的重要性进行分级按照二八定律,网站80%的流量都在核心功能上面,要优先保证核心业务的稳定
    3)隔离:不同性质不同重要性的业务做好隔离,包括业务、缓存、DB、中间件都要做好隔离
    4)调用:总体上调用要单向,可以跨层调用,但不能出现逆向调用

  3. 优点
    1)服务以接口为粒度,为开发者屏蔽远程调用底层细节,使用Dubbo面向接口远程方法调用屏蔽了底层调用细节
    2)业务分层以后架构更加清晰,并且每个业务模块职责单一,扩展性更强
    3)数据隔离,权限回收,数据访问都通过接口,让系统更加稳定安全
    4)服务应用本身无状态化,这里的无状态化指的是应用本身不做内存级缓存,而是把数据存入db
    5)服务责任易确定,每个服务可以确定责任人,这样更容易保证服务质量和稳定

  4. 缺点
    1)粒度控制复杂,如果没有控制好服务的粒度,服务的模块就会越来越多就会引发超时、分布式事务等问题
    2)服务接口数量不易控制,容易引发接口爆炸
    3)版本升级兼容困难,尽量不要删除方法、字段、枚举类型的新增字段
    4)调用链路长,服务质量不可监控,下游抖动可能会影响到上游业务,最终形成连锁反应,服务质量不稳定,同时链路的变成使得服务质量的监控变得困难

1.4 微服务架构

  1. 描述
    1)微服务架构是一种将单个应用程序作为一套小型服务开发的方法,每种应用程序都在其自己的进程中独立运行,并使用轻量级机制(通常是HTTP资源的API)进行通信。
    2)这些服务是围绕业务功能构建的,可以通过全自动部署机制进行独立部署。
    3)这些服务的集中化管理非常少,它们可以用不同的编程语言编写,并使用不同的数据存储技术。
    4)微服务是在SOA上做的升华,粒度更加细致,微服务架构强调的一个重点是"业务需要彻底的组件化和服务化"

  2. 架构图
    在这里插入图片描述

  3. 优点
    1)服务的独立部署
    2)服务的快速启动
    3)更加适合敏捷开发
    4)职责专一,由专门的团队负责专门的服务
    5)服务可以动态按需扩容
    6)代码的复用

  4. 缺点
    1)分布式部署,调用的复杂性高
    2)独立的数据库,分布式事务的挑战
    3)测试的难度提升
    4)运维难度的提升

2、Dubbo架构与实战

2.1 Dubbo概述

2.1.1 什么是Dubbo

Apache Dubbo是一款高性能的Java RPC框架。其前身是阿里巴巴公司开源的一个高性能、轻量级的开源Java RPC框架,可以和Spring框架无缝集成

2.1.2 Dubbo的特性

  • 面向接口的方式调用远程服务
  • 智能容错和负载均衡
  • 服务的自动注册和发现

2.2 Dubbo处理流程

  1. 流程图
    在这里插入图片描述

  2. 节点说明

节点角色说明
Provider暴露服务的服务提供方
Consumer调用远程服务的服务消费方
Register服务注册与发现的注册中心
Monitor统计服务的调用次数和调用时间的监控中心
Container服务运行容器,负责启动、加载、运行服务提供者
  1. 调用关系说明
    1)虚线:代表异步调用
    2)实线:代表同步访问
    3)蓝色虚线:是在启动时完成的功能
    4)红色虚线:是程序运行中执行的功能

  2. 调用流程
    1)服务提供者在服务容器启动时,向注册中心注册自己提供的服务
    2)服务消费者在启动时,向注册中心订阅自己所需的服务
    3)注册中心返回服务提供者地址列表给消费者,如果有变更注册中心会基于长连接推送变更数据给消费者
    4)服务消费者从提供者地址列表中,基于软负载均衡算法选一台提供者进行调用;如果调用失败,则重新选择一台
    5)服务提供者和消费者在内存中的调用次数和调用时间,定时每分钟发送给监控中心

2.3 服务注册中心Zookeeper

  1. Registry(服务注册中心)在其中起着至关重要的作用
  2. Dubbo官方推荐使用Zookeeper作为服务注册中心
  3. Zookeeper是Apache Hadoop的子项目,作 Dubbo服务的注册中心,工业强度较高,可用于生产环境,并推荐使用

2.4 Dubbo开发实战

2.4.1 实现步骤

  1. 建立maven工程并且创建API模块: 用于规范双方接口协定
  2. 提供provider模块,引入API模块,并且对其中的服务进行实现。将其注册到注册中心上,对外来统一提供服务
  3. 提供consumer模块,引入API模块,并且引入与提供者相同的注册中心。再进行服务调用

2.4.2 代码实现

2.4.1 api的方式

父工程demo-api

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.yuyz</groupId>
    <artifactId>demo-base</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>service-api</module>
        <module>service-provider</module>
        <module>service-consumer</module>
    </modules>

    <properties>
        <dubbo.version>2.7.5 </dubbo.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo</artifactId>
                <version>${dubbo.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-common</artifactId>
                <version>${dubbo.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-registry-zookeeper</artifactId>
                <version>${dubbo.version}</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.apache.dubbo</groupId>
                        <artifactId>dubbo-remoting-api</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>org.apache.dubbo</groupId>
                        <artifactId>dubbo-common</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-registry-nacos</artifactId>
                <version>${dubbo.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-rpc-dubbo</artifactId>
                <version>${dubbo.version}</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.apache.dubbo</groupId>
                        <artifactId>dubbo-remoting-api</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>org.apache.dubbo</groupId>
                        <artifactId>dubbo-common</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-remoting-netty4</artifactId>
                <version>${dubbo.version}</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.apache.dubbo</groupId>
                        <artifactId>dubbo-remoting-api</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-serialization-hessian2</artifactId>
                <version>${dubbo.version}</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.apache.dubbo</groupId>
                        <artifactId>dubbo-common</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- 日志配置 -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.16</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.5</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.5</version>
        </dependency>

        <!-- json数据化转换 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.3</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

公共接口子模块server-api

  1. pom.xml文件
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>demo-base</artifactId>
        <groupId>com.yuyz</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-api</artifactId>
</project>
  1. 定义接口HelloService.java
package com.yuyz.server;

public interface HelloService {

   public String sayHello(String name);
}

服务提供者子模块server-provider

  1. pom.xml文件
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>demo-base</artifactId>
        <groupId>com.yuyz</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-provider</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.yuyz</groupId>
            <artifactId>service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-registry-zookeeper</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-rpc-dubbo</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-remoting-netty4</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-serialization-hessian2</artifactId>
        </dependency>
    </dependencies>
</project>
  1. 服务端启动类DubboServer.java
package com.yuyz.service;

import com.yuyz.server.HelloService;
import com.yuyz.service.impl.HelloServiceImpl;

import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.ProtocolConfig;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.config.ServiceConfig;

import java.io.IOException;
import java.util.Arrays;

public class DubboServer {

  public static void main(String[] args) throws IOException {
    // 1、设置应用名称
    ApplicationConfig applicationConfig = new ApplicationConfig();
    applicationConfig.setName("service-provider");

    // 2、设置注册中心
    RegistryConfig registryConfig = new RegistryConfig();
    registryConfig.setProtocol("zookeeper"); // 设置zookeeper
    registryConfig.setAddress("ip:port"); // 设置注册中心地址
    registryConfig.setTimeout(10000); // 设置超时时间

    // 3、设置rmi协议
    ProtocolConfig protocolConfigRmi = new ProtocolConfig();
    protocolConfigRmi.setName("rmi");
    protocolConfigRmi.setPort(12881);

    // 4、设置dubbo协议
    ProtocolConfig protocolConfigDubbo = new ProtocolConfig();
    protocolConfigDubbo.setName("dubbo");
    protocolConfigDubbo.setPort(12882);

    // 5、暴露服务
    ServiceConfig<HelloService> serviceConfig = new ServiceConfig<HelloService>();
    serviceConfig.setApplication(applicationConfig);
    serviceConfig.setRegistry(registryConfig);
    serviceConfig.setProtocols(Arrays.asList(protocolConfigRmi, protocolConfigDubbo));

    serviceConfig.setInterface(HelloService.class); // 设置接口
    serviceConfig.setRef(new HelloServiceImpl()); // 设置具体实现类

    serviceConfig.export(); // 暴露服务

    System.in.read();
  }
}
  1. 接口实现类HelloServiceImpl.java
package com.yuyz.service.impl;

import com.yuyz.server.HelloService;

import org.apache.dubbo.config.annotation.Service;

@Service
public class HelloServiceImpl implements HelloService {

    @Override
    public String sayHello(String name) {
        return "name:" + name;
    }
}

消费者子模块server-consumer

  1. pom.xml文件
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>demo-base</artifactId>
        <groupId>com.yuyz</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-consumer</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.yuyz</groupId>
            <artifactId>service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-registry-zookeeper</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-rpc-dubbo</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-remoting-netty4</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-serialization-hessian2</artifactId>
        </dependency>
    </dependencies>
</project>
  1. 消费者启动类DubboConsumer.java
package com.yuyz.consumer;

import com.yuyz.server.HelloService;

import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.ReferenceConfig;
import org.apache.dubbo.config.RegistryConfig;

import java.io.IOException;

public class DubboConsumer {

  public static void main(String[] args) throws IOException {
    // 1、设置应用名称
    ApplicationConfig applicationConfig = new ApplicationConfig();
    applicationConfig.setName("server-consumer");
    applicationConfig.setQosEnable(true);
    applicationConfig.setQosPort(33333);
    applicationConfig.setQosAcceptForeignIp(false);

    // 2、设置注册中心
    RegistryConfig registryConfig = new RegistryConfig();
    registryConfig.setProtocol("zookeeper"); // 设置zookeeper
    registryConfig.setAddress(
        "139.196.154.141:2181,139.196.154.141:2182,139.196.154.141:2183"); // 设置地址
    registryConfig.setTimeout(10000); // 设置超时时间

    // 3、调用远程服务
    ReferenceConfig<HelloService> referenceConfig = new ReferenceConfig<HelloService>();
    referenceConfig.setApplication(applicationConfig);
    referenceConfig.setRegistry(registryConfig);
    referenceConfig.setInterface(HelloService.class);
    referenceConfig.setProtocol("dubbo");

    HelloService helloService = referenceConfig.get();
    String zhangsan = helloService.sayHello("zhangsan");
    System.out.println(zhangsan);

    System.in.read();
  }
}

2.4.2 注解的方式

父工程demo-annotation

<!-- 父工程pom.xml文件同上api的方式,此处省略 -->

公共接口子模块server-api

  1. pom.xml文件
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>demo-annotation</artifactId>
        <groupId>com.yuyz</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-api</artifactId>
</project>
  1. 定义接口HelloService.class
package com.yuyz.server;

public interface HelloService {

   public String sayHello(String name);
}

服务提供者子模块server-provider

  1. pom.xml文件
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>demo-annotation</artifactId>
        <groupId>com.yuyz</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-provider</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.yuyz</groupId>
            <artifactId>service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-registry-zookeeper</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-rpc-dubbo</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-remoting-netty4</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-serialization-hessian2</artifactId>
        </dependency>
    </dependencies>
</project>
  1. 配置文件dubbo-provider.properties
dubbo.application.name=service-provider
dubbo.registry.address=zookeeper://ip:port/ConfigCenterConfig?timeout=10000
dubbo.protocol.name=dubbo
dubbo.protocal.port=20880
  1. 配置文件log4j.properties
log4j.rootCategory=INFO,CONSOLE

log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender 
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{HH:mm:ss.SSS} [%t] %-5p %c.%M\(%F:%L\) - %m%n
  1. 服务端启动类DubboPureMain.java
package com.yuyz;

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

public class DubboPureMain {
  public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext context =
        new AnnotationConfigApplicationContext(ProviderConfiguration.class);
    context.start();
    System.in.read();
  }

  @Configuration
  @EnableDubbo(scanBasePackages = "com.yuyz.service.impl")
  @PropertySource("classpath:/dubbo-provider.properties")
  static class ProviderConfiguration {}
}

  1. 接口实现类HelloServiceImpl.java
package com.yuyz.service.impl;

import com.yuyz.server.HelloService;

import org.apache.dubbo.config.annotation.Service;

@Service
public class HelloServiceImpl implements HelloService {

    @Override
    public String sayHello(String name) {
        return "name:" + name;
    }
}

消费者子模块server-consumer

  1. pom.xml文件
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>demo-annotation</artifactId>
        <groupId>com.yuyz</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-consumer</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.yuyz</groupId>
            <artifactId>service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-registry-zookeeper</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-rpc-dubbo</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-remoting-netty4</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-serialization-hessian2</artifactId>
        </dependency>
    </dependencies>
</project>
  1. 配置文件dubbo-provider.properties
dubbo.application.name=service-consumer
dubbo.registry.address=zookeeper://ip:port/ConfigCenterConfig?timeout=10000
dubbo.application.qosEnable=true
dubbo.application.qosPort=33333
dubbo.application.qosAcceptForeignIp=false
  1. 配置文件log4j.properties
log4j.rootCategory=INFO,CONSOLE

log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender 
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{HH:mm:ss.SSS} [%t] %-5p %c.%M\(%F:%L\) - %m%n
  1. 消费者启动类AnnotationConsumerMain.java
package com.yuyz;

import com.yuyz.bean.ConsumerComponent;

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import java.io.IOException;

public class AnnotationConsumerMain {

  public static void main(String[] args) throws IOException {
    //
    AnnotationConfigApplicationContext context =
        new AnnotationConfigApplicationContext(ConsumerConfiguration.class);
    context.start();

    ConsumerComponent bean = context.getBean(ConsumerComponent.class);
    while (true) {
      System.in.read();
      String result = bean.hello("zhangsan");
      System.out.println("result:" + result);
    }
  }

  @Configuration
  @EnableDubbo
  @PropertySource("classpath:/dubbo-consumer.properties")
  @ComponentScan(value = "com.yuyz.bean")
  static class ConsumerConfiguration {}
}

  1. 调用服务类ConsumerComponent.java
package com.yuyz.bean;

import com.yuyz.server.HelloService;

import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Component;

@Component
public class ConsumerComponent {

  @Reference private HelloService helloService;

  public String hello(String name) {
    return helloService.sayHello(name);
  }
}

2.4.3 xml的方式

父工程demo-xml

<!-- 父工程pom.xml文件同上api的方式,此处省略 -->

公共接口子模块server-api

  1. pom.xml文件
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>demo-xml</artifactId>
        <groupId>com.yuyz</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-api</artifactId>
</project>
  1. 定义接口HelloService.class
package com.yuyz.server;

public interface HelloService {

   public String sayHello(String name);
}

服务提供者子模块server-provider

  1. pom.xml文件
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>demo-xml</artifactId>
        <groupId>com.yuyz</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-provider</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.yuyz</groupId>
            <artifactId>service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-registry-zookeeper</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-rpc-dubbo</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-remoting-netty4</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-serialization-hessian2</artifactId>
        </dependency>
    </dependencies>
</project>
  1. log4j.properties配置文件
log4j.rootCategory=INFO,CONSOLE

log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender 
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{HH:mm:ss.SSS} [%t] %-5p %c.%M\(%F:%L\) - %m%n
  1. dubbo-provider.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

<!-- 设置服务名称 -->
<dubbo:application name="server-provider-xml"/>

<!-- 设置zookeeper地址 -->
<dubbo:registry
        address="zookeeper://139.196.154.141:2181,139.196.154.141:2182,139.196.154.141:2183/ConfigCenterConfig?timeout=10000"/>

<!-- 设置dubbo协议 -->
<dubbo:protocol name="dubbo" port="20885"/>

<!-- 暴露服务 -->
<dubbo:service interface="com.yuyz.server.HelloService" ref="helloService"/>
<bean id="helloService" class="com.yuyz.service.impl.HelloServiceImpl"/>
</beans>
  1. 服务启动类ProviderApplication.java
package com.yuyz.service;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.io.IOException;

public class ProviderApplication {

  public static void main(String[] args) throws IOException {
    ClassPathXmlApplicationContext context =
        new ClassPathXmlApplicationContext("dubbo-provider.xml");
    context.start();
    System.in.read();
  }
}
  1. 接口实现类HelloServiceImpl.java
package com.yuyz.service.impl;

import com.yuyz.server.HelloService;

import org.apache.dubbo.config.annotation.Service;

@Service()
public class HelloServiceImpl implements HelloService {

  @Override
  public String sayHello(String name) {
    return "name:" + name;
  }
}

消费者子模块server-consumer

  1. pom.xml文件
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>demo-xml</artifactId>
        <groupId>com.yuyz</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-consumer</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.yuyz</groupId>
            <artifactId>service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-registry-zookeeper</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-rpc-dubbo</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-remoting-netty4</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-serialization-hessian2</artifactId>
        </dependency>
    </dependencies>
</project>
  1. 配置文件dubbo-consumer.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xmlns:dubo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <!-- 设置服务名称 -->
	<dubbo:application name="server-provider-xml">
    	<dubbo:parameter key="qos.enable" value="true"/>
      	<dubbo:parameter key="qos.accept.foreign.ip" value="false"/>
       	<dubbo:parameter key="qos.port" value="33333"/>	
   	</dubbo:application>

    <!-- 设置zookeeper地址 -->
    <dubbo:registry
            address="zookeeper://139.196.154.141:2181,139.196.154.141:2182,139.196.154.141:2183/ConfigCenterConfig?timeout=10000"/>

    <!-- 设置dubbo协议 -->
    <dubbo:protocol name="dubbo" port="20885"/>

    <!-- 远程服务 -->
    <dubo:reference id="helloService" interface="com.yuyz.server.HelloService"/>
</beans>
  1. 配置文件log4j.properties
log4j.rootCategory=INFO,CONSOLE

log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender 
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{HH:mm:ss.SSS} [%t] %-5p %c.%M\(%F:%L\) - %m%n
  1. 消费者启动类XMLConsumerMain.java
package com.yuyz.consumer;

import com.yuyz.server.HelloService;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.io.IOException;

public class XMLConsumerMain {
  public static void main(String[] args) throws IOException {
    ClassPathXmlApplicationContext context =
        new ClassPathXmlApplicationContext("dubbo-consumer.xml");
    context.start();
    HelloService helloService = (HelloService) context.getBean("helloService");

    String result = helloService.sayHello("lisi");
    System.out.println("result:" + result);
    System.in.read();
  }
}

2.5 Dubbo管理控制台dubbo-admin

2.5.1 作用

  1. 包含内容:服务管理、路由规则、动态配置、服务降级、访问控制、权重调整、负载均衡等管理功能

  2. 管理中心就是一个web应用,2.6版本之前是一个war包,现在是一个jar包可以直接通过java命令运行

2.5.2 控制台安装步骤

1.从git 上下载项目 https://github.com/apache/dubbo-admin
2.修改项目下的dubbo.properties文件,注意dubbo.registry.address对应的值需要对应当前使用的Zookeeper的ip地址和端口号
	dubbo.registry.address=zookeeper://zk所在机器ip:zk端口
	dubbo.admin.root.password=root
	dubbo.admin.guest.password=guest
3.切换到项目所在的路径 使用mvn 打包
	mvn clean package -Dmaven.test.skip=true
4.java 命令运行
	java -jar 对应的jar包

2.5.3 使用控制台

  1. 访问http://IP:端口
  2. 输入用户名root,密码root
  3. 点击菜单查看服务提供者和服务消费者信息
    在这里插入图片描述

2.6 Dubbo常用配置项说明

官方参考地址:参考手册

2.6.1 dubbo:application标签:对应的类org.apache.dubbo.config.ApplicationConfig,代表当前应用的信息

  1. 常用属性列表
属性说明
name当前应用程序的名称
owner当前应用程序的负责人
qosEnable是否启动QoS,默认true
qosPort启动QoS绑定的端口 默认22222
qosAcceptForeignIp是否允许远程访问,默认是fals
  1. 案例:
<!-- 设置服务信息 -->
<dubbo:application name="server-provider" owner="yuyz">
    <dubbo:parameter key="qos.enable" value="true"/>
    <dubbo:parameter key="qos.accept.foreign.ip" value="false"/>
    <dubbo:parameter key="qos.port" value="2222"/>
</dubbo:application>

2.6.2 dubbo:registry标签:对应的类org.apache.dubbo.config.RegistryConfig,代表该模块所使用的注册中心

  1. 常用属性列表
属性说明
id当前服务中provider或者consumer中存在多个注册中心时,则使用需要增加该配置
address当前注册中心的访问地址
protocol当前注册中心所使用的协议,也可以直接在address中写入,比如使用zookeeper,就可以写成 zookeeper://xx.xx.xx.xx:2181
timeout超时时间
  1. 案例
<!-- 设置注册中心信息 -->
<dubbo:registry address="zookeeper://ip:port"/>

2.6.3 dubbo:protocol标签:对应的类org.apache.dubbo.config.ProtocolConfig,指定服务在进行数据传输所使用的协议

  1. 常用属性列表
属性说明
id使用时多个协议需要指定
name指定协议名称,默认使用 dubbo
  1. 案例
<!-- 设置协议信息 -->
<dubbo:protocol name="dubbo" port="20884"/>

2.6.4 dubbo:service标签:对应的类org.apache.dubbo.config.ServiceConfig,指定当前需要对外暴露的服务信息

  1. 常用属性列表
属性说明
interface指定当前需要进行对外暴露的接口
ref具体实现对象的引用,一般我们在生产级别都是使用Spring去进行Bean托管的,所以这里面
一般也指的是Spring中的BeanId
version对外暴露的版本号。不同的版本号,消费者在消费的时候只会根据固定的版本号进行消费
  1. 案例
<!-- 配置服务信息 -->
<dubbo:service interface="com.yuyz.server.HelloService" ref="helloService"/>

2.6.5 dubbo:reference标签:对应的类org.apache.dubbo.config.ReferenceConfig,消费者的配置

  1. 常用属性列表
属性说明
id指定该Bean在注册到Spring中的id
interface服务接口名
version指定当前服务版本,与服务提供者的版本一致
registry指定所具体使用的注册中心地址。这里面也就是使用上面在 dubbo:registry中所声明的id
retries用于指定当前服务在执行时出现错误或者超时时的重试机制
timeout超时时间
  1. 案例
<!-- 远程服务 -->
<dubo:reference id="helloService" interface="com.yuyz.server.HelloService" timeout="8000" retries="3"/>

2.6.6 dubbo:method标签:对应的类org.apache.dubbo.config.MethodConfig,同时该标签为dubbo:service或dubbo:reference的子标签,用于控制到方法级

  1. 常用属性列表
属性说明
name指定方法名称,用于对这个方法名称的RPC调用进行特殊配置
async是否异步,默认false

3、Dubbo高级应用实战

3.1 SPI

3.1.1 SPI简介

  1. SPI全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制
  2. 目前有不少框架用它来做服务的扩展发现,简单来说,它就是一种动态替换发现的机制
  3. 使用SPI机制的优势是实现解耦,使得第三方服务模块的装配控制逻辑与调用者的业务代码分离

3.1.2 JDK中的SPI

  1. 调用关系图
    在这里插入图片描述

  2. 遵循约定
    1)当服务提供者提供了接口的一种具体实现后,在META-INF/services目录下创建一个以"接口全限定名"为命名的文件,内容为实现类的全限定名
    2)接口实现类所在的jar包放在主程序的classpath中
    3)主程序通过java.util.ServiceLoader动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM
    4)SPI的实现类必须携带一个无参构造方法

  3. 代码案例

接口层模块:java_spi_demo_api模块

定义接口

package com.yuyz.service;

public interface HelloService {
    public String sayHello();
}

接口实现层:java_spi_demo_impl模块

实现类CatHelloServiceImpl.java

package com.yuyz.service.impl;

import com.yuyz.service.HelloService;

public class CatHelloServiceImpl implements HelloService {
    public String sayHello() {
        return "miao! miao!";
    }
}

实现类DogHelloServiceImpl.java

package com.yuyz.service.impl;

import com.yuyz.service.HelloService;

public class DogHelloServiceImpl implements HelloService {
    public String sayHello() {
        return "wang! wang!";
    }
}

META-INF/services目录创建文件:com.yuyz.service.HelloService(接口全限定名)
,内容为实现类的全限定名

com.yuyz.service.impl.DogHelloServiceImpl
com.yuyz.service.impl.CatHelloServiceImpl

使用层:java_spi_demo_main模块
主程序通过java.util.ServiceLoader动态装载实现模块

package com.yuyz.test;

import com.yuyz.service.HelloService;

import java.util.ServiceLoader;

public class JavaSpiMain {
  public static void main(String[] args) {
    final ServiceLoader<HelloService> helloServices = ServiceLoader.load(HelloService.class);
    for (HelloService helloService : helloServices) {
      System.out.println(helloService.getClass().getName() + ":" + helloService.sayHello());
    }
  }
}

3.1.3 Dubbo中的SPI

  1. dubbo中大量的使用了SPI来作为扩展点,通过实现同一接口的前提下,可以进行定制自己的实现类。
  2. 比如比较常见的协议,负载均衡,都可以通过SPI的方式进行定制化,自己扩展
  3. Dubbo中已经存在的所有已经实现好的扩展点,如下图:
    在这里插入图片描述
  4. 代码案例

接口层模块:dubbo_spi_demo_api模块

pom.xml文件中引入dubbo

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>dubbo_spi_demo</artifactId>
        <groupId>com.yuyz</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>dubbo_spi_demo_api</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.7.5</version>
        </dependency>
    </dependencies>
</project>

定义接口HelloService.java,添加@SPI注解

package com.yuyz.server;

import org.apache.dubbo.common.extension.SPI;

@SPI
public interface HelloService {
    public String sayHello();
}

接口实现层:dubbo_spi_demo_impl模块

实现类CatHelloServiceImpl.java

package com.yuyz.service.impl;

import com.yuyz.service.HelloService;

public class CatHelloServiceImpl implements HelloService {
    public String sayHello() {
        return "miao! miao!";
    }
}

实现类DogHelloServiceImpl.java

package com.yuyz.service.impl;

import com.yuyz.service.HelloService;

public class DogHelloServiceImpl implements HelloService {
    public String sayHello() {
        return "wang! wang!";
    }
}

META-INF/dubbo目录创建文件:com.yuyz.service.HelloService(接口全限定名)
,内容为实现类的全限定名

dog=com.yuyz.service.impl.DogHelloServiceImpl
cat=com.yuyz.service.impl.CatHelloServiceImpl

使用层:dubbo_spi_demo_main模块
主程序借助ExtensionLoader类实现

package com.yuyz.test;

import com.yuyz.server.HelloService;

import org.apache.dubbo.common.extension.ExtensionLoader;

import java.util.Set;

public class DubboSpiMain {
  public static void main(String[] args) {

    // 获取扩展加载器
    ExtensionLoader<HelloService> extensionLoader =
        ExtensionLoader.getExtensionLoader(HelloService.class);

    // 遍历所有的支持的扩展点 META-INF.dubbo
    Set<String> extensions = extensionLoader.getSupportedExtensions();
    for (String extension : extensions) {
      String result = extensionLoader.getExtension(extension).sayHello();
      System.out.println(result);
    }
  }
}
  1. dubbo自己实现SPI的目的
    1)JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源
    2)如果有扩展点加载失败,则所有扩展点无法使用
    3)提供了对扩展点包装的功能(Adaptive),并且还支持通过set的方式对其他的扩展点进行注入

3.1.4 Dubbo SPI中的Adaptive功能

  1. Dubbo中的Adaptive功能,主要解决的问题是如何动态的选择具体的扩展点
  2. 通过getAdaptiveExtension统一对指定接口对应的所有扩展点进行封装,通过URL的方式对扩展点来进行动态选择
  3. dubbo中所有的注册信息都是通过URL的形式进行处理的
  4. 代码案例(在上面Dubbo中的SPI代码基础上实现)

接口层模块:dubbo_spi_demo_api模块
接口中新增方法

@Adaptive
public String syaHello(URL rul);

接口实现层:dubbo_spi_demo_impl模块
接口实现类DogHelloServiceImpl.java中新增方法

package com.yuyz.server.impl;

import com.yuyz.server.HelloService;

import org.apache.dubbo.common.URL;

public class DogHelloServiceImpl implements HelloService {
  public String sayHello() {
    return "wang wang!";
  }

  public String syaHello(URL rul) {
    return "wang wang!";
  }
}

接口实现类CatHelloServiceImpl.java中新增方法

package com.yuyz.server.impl;

import com.yuyz.server.HelloService;

import org.apache.dubbo.common.URL;

public class CatHelloServiceImpl implements HelloService {
  public String sayHello() {
    return "miao miao!";
  }

  public String syaHello(URL rul) {
    return "miao miao!";
  }
}

使用层:dubbo_spi_demo_main模块
新增启动类DubboAdaptiveMain.java

package com.yuyz.test;

import com.yuyz.server.HelloService;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;

public class DubboAdaptiveMain {
  public static void main(String[] args) {
    URL url = URL.valueOf("test://localhost/hello?hello.service=dog");
    final HelloService adaptiveExtension =
        ExtensionLoader.getExtensionLoader(HelloService.class).getAdaptiveExtension();
    String result = adaptiveExtension.syaHello(url);
    System.out.println(result);
  }
}

3.1.5 Dubbo调用时拦截操作

  1. 与很多框架一样,Dubbo也存在拦截(过滤)机制,可以通过该机制在执行目标程序前后执行我们指定的代码。
  2. Dubbo的Filter机制,是专门为服务提供方和服务消费方调用过程进行拦截设计的,每次远程方法执行,该拦截都会被执行
  3. 这样就为开发者提供了非常方便的扩展性,比如为dubbo接口实现ip白名单功能、监控功能 、日志记录等。
    步骤:
    1)实现 org.apache.dubbo.rpc.Filter 接口
    2)使用 org.apache.dubbo.common.extension.Activate接口进行对类进行注册,通过group可以指定生产端、消费端
@Activate(group = {CommonConstants.CONSUMER)

3)计算方法运行时间的代码实现
4)在META-INF.dubbo中新建org.apache.dubbo.rpc.Filter文件,并将当前类的全名写入

timerFilter=包名.过滤器的名字

3.2 负载均衡策略

3.2.1 负载均衡基本配置

  1. 负载均衡(Load Balance), 其实就是将请求分摊到多个操作单元上进行执行,从而共同完成工作任务
  2. 负载均衡策略主要用于客户端存在多个提供者时进行选择某个提供者
  3. 在集群负载均衡时,Dubbo 提供了多种均衡策略
  4. 配置负载均衡策略,既可以在服务提供者一方配置,也可以在服务消费者一方配置
// 服务消费配置方式,在@Reference标签上
@Reference(loadbalance = "random")
private HelloService helloService;


// 在服务提供者配置方式,在@Service标签上
@Service(loadbalance = "random")
public class HelloServiceImpl implements HelloService {
	// 
}
  1. 负载均衡策略
策略名称描述
Random LoadBalance随机策略按照权重设置的大小随机
RoundRobin LoadBalance轮询策略例如:a、b、c三个服务提供者。a执行完b执行然后c,然后在到a…
LeastActive LoadBalance最少活跃调用数策略活跃数指调用前后计数差,优先调用高的,相同活跃数的随机。使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大
ConsistentHash LoadBalance一致性Hash策略相同参数总是发送到同一个提供者,如果这个提供者挂掉了,它会根据它的虚拟节点,平摊到其它服务者,不会引起巨大的变动

注意:缺省为随机策略(random),也就是说,不配置负载均衡策略的时候,默认为random

3.2.2 自定义负载均衡器

  1. 新增模块dubbo-spi-loadbalance,自定义负载均衡器OnlyFirstLoadbalancer类,实现接口org.apache.dubbo.rpc.cluster.LoadBalance
  2. 配置负载均衡器在dubbo-spi-loadbalance工程的META-INF/dubbo目录下新建org.apache.dubbo.rpc.cluster.LoadBalance文件,并将当前类的全名写入
onlyFirst=包名.负载均衡器
  1. 在服务消费方指定自定义负载均衡器onlyFirst
@Reference(loadbalance = "onlyFirst")
private HelloService helloService;

3.3 异步调用

3.3.1 异步调用实现

  1. 消费端增加配置
<!-- 远程服务 -->
<dubo:reference id="helloService" interface="com.yuyz.server.HelloService" timeout="8000" retries="3">
		<!-- 方法设置为异步 -->
    <dubbo:method name="sayHello" async="true"/>
</dubo:reference>
  1. 通过RpcContext.getContext().getFuture()获取结果
public class XMLConsumerMain {
  public static void main(String[] args)
      throws IOException, ExecutionException, InterruptedException {
    ClassPathXmlApplicationContext context =
        new ClassPathXmlApplicationContext("dubbo-consumer.xml");
    context.start();
    HelloService helloService = (HelloService) context.getBean("helloService");

		// 同步返回的结果为空
    String result = helloService.sayHello("lisi", 1000);
    System.out.println("result:" + result);

		// 异步拿到返回结果
    Future<Object> future = RpcContext.getContext().getFuture();
    System.out.println("异步 result:" + future.get());
  }
}

3.4 线程池

3.4.1 Dubbo线程池

  1. dubbo在使用时,都是通过创建真实的业务线程池进行操作的。目前已知的线程池模型有两个(fix和cache)和java中的相互对应
  2. fix: 表示创建固定大小的线程池。也是Dubbo默认的使用方式,默认创建的执行线程数为200,并且是没有任何等待队列的。所以再极端的情况下可能会存在问题,比如某个操作大量执行时,可能存在堵塞的情况
  3. cache: 创建非固定大小的线程池,当线程不足时,会自动创建新的线程。但是使用这种的时候需要注意,如果突然有高TPS的请求过来,方法没有及时完成,则会造成大量的线程创建,对系统的CPU和负载都是压力,执行越多反而会拖慢整个系统

3.4.2 自定义线程池

  1. 在新增子模块dubbo-spi-threadpool中定义线程池,基础FixedThreadPool,实现Runnable
package com.yuyz.threadpool;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.threadpool.support.fixed.FixedThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class WachingThreadPool extends FixedThreadPool implements Runnable {
  private static final Logger LOGGER = LoggerFactory.getLogger(WachingThreadPool.class);
  // 定义线程池使用的阀值
  private static final double ALARM_PERCENT = 0.90;
  private final Map<URL, ThreadPoolExecutor> THREAD_POOLS = new ConcurrentHashMap<>();

  public WachingThreadPool() {
    // 每隔3秒钟执行一次
    Executors.newSingleThreadScheduledExecutor()
        .scheduleWithFixedDelay(this, 1, 3, TimeUnit.SECONDS);
  }

  @Override
  public Executor getExecutor(URL url) {
    final Executor executor = super.getExecutor(url);
    if (executor instanceof ThreadPoolExecutor) {
      THREAD_POOLS.put(url, (ThreadPoolExecutor) executor);
    }
    return executor;
  }

  @Override
  public void run() {
    for (Map.Entry<URL, ThreadPoolExecutor> entry : THREAD_POOLS.entrySet()) {
      final URL url = entry.getKey();
      final ThreadPoolExecutor executor = entry.getValue();

      int activeCount = executor.getActiveCount(); // 活跃线程
      int poolSize = executor.getCorePoolSize(); // 线程总数
      double usedPercent = activeCount / (poolSize * 1.0);
      LOGGER.info("线程池执行状态:[{}/{}:{}%]", activeCount, poolSize, usedPercent * 100);
      if (usedPercent > ALARM_PERCENT) {
        LOGGER.error("超出警戒线! host:{} 当前使用率是:{},URL:{}", url.getIp(), usedPercent * 100, url);
      }
      //
    }
  }
}
  1. resources/META-INF/dubbo文件下新增文件:org.apache.dubbo.common.threadpool.ThreadPool;内容如下:
watching=com.yuyz.threadpool.WachingThreadPool
  1. 服务提供者模块service-provider,引入dubbo-spi-threadpool依赖,新增配置
dubbo.provider.threadpool=watching
  1. 在调用方则尝试简单通过for循环启动多个线程来执行,查看服务提供方的监控情况

3.5 路由规则

3.5.1 路由规则简介

  1. 提供两个提供者(一台本机作为提供者,一台为其他的服务器),每个提供者会在调用时可以返回不同的信息以区分提供者
  2. 针对于消费者,我们这里通过一个死循环,每次等待用户输入,再进行调用,来模拟真实的请求情况,通过调用的返回值确认具体的提供者
  3. 我们通过ipconfig来查询到我们的IP地址,并且单独启动一个客户端,来进行如下配置(这里假设我们希望隔离掉本机的请求,都发送到另外一台机器上)
public class DubboRouterMain {
  public static void main(String[] args) {
    RegistryFactory registryFactory =
        ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
    Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://127.0.0.1:2181"));

    registry.register(
        URL.valueOf(
            "condition://0.0.0.0/com.lagou.service.HelloServi?cecategory=routers&force=true&dynamic=true&rule="
                + URL.encode("=> host != 你的 机器ip不能是127.0.0.1")));
  }
}
  1. 通过这个程序执行后,我们就通过消费端不停的发起请求,看到真实的请求都发到了除去本机以外的另外一台机器上

3.5.1 路由规则详解

路由规则本质上就是通过在zookeeper中保存一个节点数据,来记录路由规则。消费者会通过监听这个服务的路径,来感知整个服务的路由规则配置,然后进行适配。路由配置的参数:

  1. route:// 表示路由规则的类型,支持条件路由规则和脚本路由规则,可扩展,必填
  2. 0.0.0.0 表示对所有 IP 地址生效,如果只想对某个 IP 的生效,请填入具体 IP,必填
  3. com.lagou.service.HelloService 表示只对指定服务生效,必填
  4. category=routers 表示该数据为动态配置类型,必填
  5. dynamic : 是否为持久数据,当指定服务重启时是否继续生效。必填
  6. runtime : 是否在设置规则时自动缓存规则,如果设置为true则会影响部分性能
  7. rule : 是整个路由最关键的配置,用于配置路由规则,… => … 在这里 => 前面的就是表示消费者方的匹配规则,可以不填(代表全部)。 => 后方则必须填写,表示当请求过来时,如果选择提供者的配置

3.6 服务动态降级

3.6.1 什么是服务降级

  1. 服务降级,当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务有策略的降低服务级别,以释放服务器资源,保证核心任务的正常运行

3.6.1 为什么要服务降级

  1. 服务降级,这是防止分布式服务发生雪崩效应
  2. 雪崩:当一个请求发生超时,一直等待着服务响应,那么在高并发情况下,很多请求都是因为这样一直等着响应,直到服务资源耗尽产生宕机,而宕机之后会导致分布式其他服务调用该宕机的服务也会出现资源耗尽宕机,这样下去将导致整个分布式服务都瘫痪,这就是雪崩

3.6.1 dubbo服务降级实现方式

方式一:dubbo管理控制台配置服务降级

  1. 屏蔽(mock=force:return+null)
    表示消费方对该服务的方法调用都直接返回null值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响

  2. 容错(mock=fail:return+null)
    表示消费方对该服务的方法调用在失败后,再返回null值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响

方式二:指定返回简单值或者null

  1. xml配置方式案例
<dubbo:reference id="xxService" check="false" interface="com.xx.XxService"
timeout="3000" mock="return null" />

<dubbo:reference id="xxService2" check="false" interface="com.xx.XxService2"
timeout="3000" mock="return 1234" />
  1. 注解方式案例
// 返回空
@Reference(mock="return null") 
// 返回简单值
@Reference(mock="return 简单值")
// 支持这种表达式
@Reference(mock="force:return null")

方式三:使用java代码动态写入配置中心

RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://IP:端口"));
registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null"));

方式四:整合hystrix

4、Dubbo源码分析

4.1 源码下载和编译

  1. dubbo的项目在github中的地址为: 地址连接

  2. 进入需要进行下载的地址,执行命令:

git clone https://github.com/apache/dubbo.git
  1. 为了防止master中代码不稳定,进入dubbo项目,可以切入到最近的release分支git
cd dubbo
checkout 2.7.8-release
  1. 进行本地编译,进入dubbo项目,进行编译操作
mvn clean install -DskipTests
  1. 使用IDE引入项目

4.2 架构整体设计

4.2.1 Dubbo调用关系说明

  1. 调用关系图
    在这里插入图片描述

  2. 组成部分
    Provider 暴露服务的服务提供方
     1)Protocol:负责提供者和消费者之间的协议交互数据
     2)Service:真实的业务服务,可以理解为接口和实现
     3)Container:Dubbo的运行环境
    Consumer 调用远程服务的服务消费方
     1)Protocol:负责提供者和消费者之间的协议交互数据
     2)Cluster:感知提供者端的服务列表信息
     3)Proxy:提供者的服务调用代理类,由它接管Consumer中接口调用逻辑
    Registry 注册中心(服务发现、路由配置)
    Monitor 监控中心(提供者和消费者的数据统计)

  3. 启动和执行流程
    1)提供者启动:容器负责加载Service信息,并通过Protocol注册到注册中心Registry
    2)消费者启动:通过监听提供者列表来感知提供者信息,并在提供者信息改变时,通过注册中心及时通知消费者
    3)消费者发起请求:调用Proxy代理,利用Cluster模块选择真实的要发送给的提供者信息,交由Consumer端的Protocol把信息发送给提供者,提供者同样需要通过Protocol模块来处理消费者的信息,最后由真正的服务提供者Service来进行处理

4.2.2 整体的调用链路

  1. 调用链路图
    在这里插入图片描述

  2. 链路图说明
    1)上方淡绿色代表服务提供者
    2)下发淡蓝色代表服务消费者
    3)蓝色长方形框代表的是接口
    4)绿色长方形框代表的是实现
    5)蓝色虚线箭头代表的初始化
    6)红色实线箭头代表服务调用

  3. 调用流程
    1)消费者通过interface方法进行调用,统一交由消费端的Proxy;Proxy由ProxyFactory来进行代理对象的创建,使用到了jdk动态代理和javassist动态代理
    2)请求交给Filter,做统一的过滤请求
    3)核心的Invoker调用逻辑
     3.1)通过Directory去注册中心获取信息,最终通过list方法获取所有的Invoker
     3.2)通过Cluster模块,根据选择的具体路由规则来选取Invoker列表
     3.3)通过LoadBalance模块,根据负载均衡策略,选择一个具体的Invoker来处理我们的请求
     3.4)如果执行中出现错误,并且Consumer中配置了重试机制,则会重新尝试执行
    4)经过Filter,进行执行功能的前后封装,Invoker选择具体的执行协议
    5)客户端进行编码和序列化,然后发送数据
    6)到达Provider的Server,对接受的数据进行反编码和反序列化
    7)使用Exporter选择执行器
    8)交给Filter进行一个提供者端的过滤,到达Invoker执行器
    9)通过Invoker调用接口的具体实现,然后返回

4.2.3 Dubbo源码整体设计

  1. 设计图
    在这里插入图片描述

  2. 设计图说明
    1)图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口
    2)图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中Service和Config层为API,其它各层均为SPI
    3)图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类
    4)图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法

  3. 分层(1-6-3)
    1)Business业务逻辑层
     1.1)Service业务层:包括业务代码,如接口、实现类,直接面向开发者
    2)RPC远程过程调用层
     2.1)Config配置层:对外提供配置,以ServiceConfig和ReferenceConfig为核心,可以直接初始化配置类,也可以解析配置文件生成
     2.2)Proxy服务代理层:无论是生产者还是消费者,框架都会产生一个代理类,整个过程对上层透明,业务层对远程调用无感
     2.3)Registry注册中心层:封装服务地址的注册与发行,以URL为中心
     2.4)Cluster路由层(集群容错层):对多个提供者的路由和负载均衡,并桥接注册中心,以Invoker为核心
     2.5)Monitor监控层:RPC调用相关的信息在这一层完成,如调用次数、成功失败的情况、调用时间等
     2.6)Protocol远程调用层:封装RPC调用,无论是服务的暴露还是服务的引用都在Protocol中作为功能入口,负责Invoker的整个生命周期,Dubbo中的所有模块都向Invoker靠拢
    3)Remoting远程数据传输层
     3.1)Exchange信息交换层:封装请求和响应的模式,如把同步请求转成异步请求
     3.2)Transport网络传输层:统一网络传输的接口,如把netty和mina统一为一个网络传输接口
     3.3)Serialize数据序列化层:负责管理整个框架中的数据传输的序列化和反序列化

4.3 服务注册与消费源码剖析

4.3.1 注册中心Zookeeper剖析

  1. 注册中心是Dubbo的重要组成部分,主要用于服务的注册与发现,我们可以选择Redis、Nacos、Zookeeper作为Dubbo的注册中心,Dubbo推荐用户使用Zookeeper作为注册中心

  2. 注册中心Zookeeper目录结构

+- dubbo
| +- com.lagou.service.HelloService
| | +- consumers
| | | +- consumer://192.168.1.26/com.yuyz.server.HelloService?application=service-consumer&category=consumers&check=false&dubbo=2.0.2&init=false&interface=com.yuyz.server.HelloService&methods=sayHello&pid=10560&qos.accept.foreign.ip=false&qos.enable=true&qos.port=33333&release=2.7.5&side=consumer&sticky=false&timestamp=1608820662845
| | +- providers
| | | +- dubbo://192.168.1.26:20880/com.yuyz.server.HelloService?anyhost=true&application=service-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.yuyz.server.HelloService&methods=sayHello&pid=13576&release=2.7.5&side=provider&timestamp=1608820635457
| | +- configuration
| | +- routers
  1. 在/dubbo目录结构下有四个配置项
     1)consumers: 当前服务下面所有的消费者列表(URL)
     2)providers: 当前服务下面所有的提供者列表(URL)
     3)configuration: 当前服务下面的配置信息信息,provider或者consumer会通过读取这里的配置信息来获取配置
     4)routers: 当消费者在进行获取提供者的时,会通过这里配置好的路由来进行适配匹配规则

  2. zookeeper中的节点图
    在这里插入图片描述
     1)提供者会在providers目录下进行自身的进行注册
     2)消费者会在consumers目录下进行自身注册,并且监听provider目录,以此通过监听提供者增加或者减少,实现服务发现
     3)Monitor模块会对整个服务级别做监听,用来得知整体的服务情况,以此就能更多的对整体情况做监控

4.3.2 服务的注册过程分析

4.3.2.1 服务注册(暴露)过程图

在这里插入图片描述

  1. 具体服务到Invoker的转换:ServiceConfig类拿到对外提供服务的实际类ref(如:HelloServiceImpl),然后通过ProxyFactory接口实现类中的getInvoker方法使用ref生成一个AbstractProxyInvoke实例
  2. Invoker到Exporter的转换:涉及到RegistryService接口和RegistryFactory接口,以及注册provider到注册中心流程的过程
4.3.2.1 具体服务到Invoker源码分析
  1. ServerConfg代码解读:三个关键成员变量protocol、proxyFactory、ref
private static final Protocol protocol = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

private static final ProxyFactory PROXY_FACTORY = (ProxyFactory)ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();

// ServiceConfigBase类中,泛型
protected T ref;
  1. ServiceConfig.doExportUrlsFor1Protocol方法总核心代码解读:调用ProxyFactory的getInvoker方法获取Invoker对象,传入的ref为ServerConfig类中拿到的对外提供服务的实际类生成的
Invoker<?> invoker = PROXY_FACTORY.getInvoker(this.ref, this.interfaceClass, registryURL.addParameterAndEncoded("export", url.toFullString()));
                            DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
4.3.2.2 Invoker转换为Exporter源码分析
  1. RegistryService代码解读:主要是对指定的路径进行注册,解绑,监听和取消监听,查询操作。是注册中心中最为基础的类
public class RegistryService {
  /** 进行对URL的注册操作,比如provider,consumer,routers等 */
  void register(URL url);

  /** 解除对指定URL的注册,比如provider,consumer,routers等 */
  void unregister(URL url);

  /** 增加对指定URL的路径监听,当有变化的时候进行通知操作 */
  void subscribe(URL url, NotifyListener listener);

  /** 解除对指定URL的路径监听,取消指定的listener */
  void unsubscribe(URL url, NotifyListener listener);

  /** 查询指定URL下面的URL列表,比如查询指定服务下面的consumer列表 */
  List<URL> lookup(URL url);
}
  1. RegistryFactory代码解读:是通过他来生成真实的注册中心。通过这种方式,也可以保证一个应用中可以使用多个注册中心。通过不同的protocol参数,来选择不同的协议
@SPI("dubbo")
public interface RegistryFactory {

	/**  获取注册中心地址 */
    @Adaptive({"protocol"})
    Registry getRegistry(URL url);
}
  1. RegistryProtocol代码解读:负责管理整个注册中心相关协议。并且统一对外提供服务。以RegistryProtocol.export方法作为入口,这个方法主要的作用就是将我们需要执行的信息注册并且导出
@Override
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    
    // 获取注册中心的地址
    URL registryUrl = getRegistryUrl(originInvoker);
    // 获取当前提供者需要注册的地址
    URL providerUrl = getProviderUrl(originInvoker);

	// 获取进行注册override协议的访问地址
    final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
    final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
    // 增加override的监听器
    overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);

	// 根据现有的override协议,对注册地址进行改写操作
    providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
    // 对当前的服务进行本地导出,完成后即可在看到本地的20880端口号已经启动,并且暴露服务
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

    // 获取真实的注册中心, 比如我们常用的ZookeeperRegistry
    final Registry registry = getRegistry(originInvoker);
    
    // 获取当前服务需要注册到注册中心的providerURL,主要用于去除一些没有必要的参数
    final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);

    // 获取当前url是否需要进行注册参数
    boolean register = providerUrl.getParameter(REGISTER_KEY, true);
    if (register) {
    	// 将当前的提供者注册到注册中心上去,完成后注册中心provider子节点中就会新增一个服务提供者
        register(registryUrl, registeredProviderUrl);
    }

    // 对override协议进行注册,用于在接收到override请求时做适配,这种方式用于适配2.6.x及之前的版本(混用)
    registerStatedUrl(registryUrl, registeredProviderUrl, register);

	
	// 设置当前导出中的相关信息
    exporter.setRegisterUrl(registeredProviderUrl);
    exporter.setSubscribeUrl(overrideSubscribeUrl);

    registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);

    notifyExport(exporter);
    // 返回导出对象(对数据进行封装)
    return new DestroyableExporter<>(exporter);
}
  1. RegistryProtocol.register方法解读:从RegistoryFactory中获取注册中心,并且进行地址注册
public void register(URL registryUrl, URL registeredProviderUrl) {
  // 获取注册中心
  Registry registry = registryFactory.getRegistry(registryUrl);
  // 对当前的服务进行注册
  registry.register(registeredProviderUrl);
  // ProviderModel表示服务提供者模型,此对象中存储了与服务提供者相关的信息。
  // 比如服务的配置信息,服务实例等。每个被导出的服务对应一个ProviderModel。
  ProviderModel model = ApplicationModel.getProviderModel(registeredProviderUrl.getServiceKey());
  model.addStatedUrl(new ProviderModel.RegisterStatedURL(
      registeredProviderUrl,
      registryUrl,
      true
 ));
}
  1. 在分析的register方法之前,先来介绍一下Registry中的类目录结构
    在这里插入图片描述
    1)RegistryService 就是对外提供注册机制的接口
    2)Registry也同样是一个接口,是对RegistryService的集成,并且继承了Node接口,说明注册中心也是基于URL去做的
    3)AbstractRegistry是对注册中心的封装,其主要会对本地注册地址的封装,主要功能在于远程注册中心不可用的时候,可以采用本地的注册中心来使用
    4)FailbackRegistry从名字中可以看出来,失败自动恢复,后台记录失败请求,定时重发功能。
    5)红线框里面是真实的第三方渠道实现,例如ZookeeperRegistry

  2. FailbackRegistry.register解读:

public void register(URL url) {
  	// 上层调用,主要用于保存已经注册的地址列表
  	super.register(url);
	
	// 将一些错误的信息移除(确保当前地址可以在出现一些错误的地址时可以被删除)
	removeFailedRegistered(url);
	removeFailedUnregistered(url);
  	try {
		// 发送给第三方渠道进行注册操作
	    doRegister(url);
	 } catch (Exception e) {
	    Throwable t = e;
	    // 记录日志
	    boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
	        && url.getParameter(Constants.CHECK_KEY, true)
	        && !CONSUMER_PROTOCOL.equals(url.getProtocol());
	    boolean skipFailback = t instanceof SkipFailbackWrapperException;
	    if (check || skipFailback) {
	      if (skipFailback) {
	        t = t.getCause();
	     }
	      throw new IllegalStateException("Failed to register " + url + " toregistry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t);
	   } else {
	      logger.error("Failed to register " + url + ", waiting for retry, cause: " + t.getMessage(), t);
	   }
	    // 后台异步进行重试,也是Failback比较关键的代码
	    addFailedRegistered(url);
	 }
}
  1. ZookeeperRegistry.doRegister方法解析:进行创建地址
public void doRegister(URL url) {
    try {
    	// 在注册中心创建节点
        this.zkClient.create(this.toUrlPath(url), url.getParameter("dynamic", true));
    } catch (Throwable var3) {
        throw new RpcException("Failed to register " + url + " to zookeeper " + this.getUrl() + ", cause: " + var3.getMessage(), var3);
    }
}
  1. ZookeeperRegistry.toUrlPath方法解析:获取urlpath
private String toUrlPath(URL url) {
	// 分类地址 + url字符串
    return this.toCategoryPath(url) + "/" + URL.encode(url.toFullString());
}

private String toCategoryPath(URL url) {
	// 服务名称 + category(在当前的例子中是providers)
    return this.toServicePath(url) + "/" + url.getParameter("category", "providers");
}

private String toServicePath(URL url) {
	// 接口地址
    String name = url.getServiceInterface();
    // 根节点 + 接口地址
    return "*".equals(name) ? this.toRootPath() : this.toRootDir() + URL.encode(name);
}

4.3.3 URL规则详解和服务本地缓存

4.3.3.1 URL规则详解
  1. URL地址
protocol://host:port/path?key=value&key=value

// 案例
provider://192.168.20.1:20883/com.yuyz.service.HelloService?
anyhost=true&application=service-provider2&bind.ip=192.168.20.1&bind.port=20883&category=configurators&check=false
&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.lagou.service
  1. URL组成部分
    1)protocol: 协议,一般像我们的provider或者consumer在这里都是人为具体的协议
    2)host: 当前provider或者其他协议所具体针对的地址,比较特殊的像override协议所指定的host就是 0.0.0.0代表所有的机器都生效
    3)port: 和上面相同,代表所处理的端口号
    4)path: 服务路径,在provider或者consumer等其他中代表着我们真实的业务接口
    5)key=value: 这些则代表具体的参数,这里我们可以理解为对这个地址的配置。比如我们provider中需要具体机器的服务应用名,就可以是一个配置的方式设置上去

  2. Dubbo中的URL与java中的URL的区别
    1)Dubbo中的URL提供了针对于参数的parameter的增加和减少(支持动态更改)
    2)Dubbo中提供缓存功能,对一些基础的数据做缓存

4.3.3.2 服务本地缓存

本地缓存说明

  1. dubbo调用者需要通过注册中心(例如:ZK)注册信息,获取提供者,但是如果频繁往从ZK获取信息,肯定会存在单点故障问题,所以dubbo提供了将提供者信息缓存在本地的方法
  2. Dubbo在订阅注册中心的回调处理逻辑当中会保存服务提供者信息到本地缓存文件当中(同步/异步两种方式),以URL纬度进行全量保存
  3. Dubbo在服务引用过程中会创建registry对象并加载本地缓存文件,会优先订阅注册中心,订阅注册中心失败后会访问本地缓存文件内容获取服务提供信息

4.3.4 Dubbo消费过程分析

4.3.4.1 服务消费过程图

在这里插入图片描述

  1. ReferenceConfig类的init方法调用createProxy(),期间使用Protocol调用refer方法生成Invoker实例
  2. 使用ProxyFactory把Invoker转换为客户端需要的接口

// TODO:源码剖析

4.4 Dubbo扩展SPI源码剖析

  1. ExtensionLoader类是所有Dubbo中SPI的入口
  2. ExtensionLoader.getExtensionLoader方法:获取扩展点加载器,并加载所对应的所有的扩展点实现
  3. ExtensionLoader.getExtension方法:根据name获取扩展的指定实现

4.4.1 getExtensionLoader加载过程

  1. ExtensionLoader的实例化解读
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {

  // 必须传入类型
  if (type == null) {
    throw new IllegalArgumentException("Extension type null");
 	}
  // 必须是接口类型
  if (!type.isInterface()) {
    throw new IllegalArgumentException("Extension type (" + type + ") is notan interface!");
 	}
  // 必须包含SPI的注解
  if (!withExtensionAnnotation(type)) {
    throw new IllegalArgumentException("Extension type (" + type + ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
 	}
  // 尝试从已经加载过的数据中去读取(缓存功能)
  ExtensionLoader<T> loader = (ExtensionLoader<T>)EXTENSION_LOADERS.get(type);
  if (loader == null) {
    // 如果没有的话,才会进行初始化,并且放入到缓存汇总
    EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
    loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
 	}
  return loader;
}

// 判断接口是否有@SPI注解
private static <T> boolean withExtensionAnnotation(Class<T> type) {
  // 包含`@SPI`注解在接口上
  return type.isAnnotationPresent(SPI.class);
}
  1. ExtensionLoader的构造器函数解读:主要是对type进行赋值操作,然后获取ExtensionFactory对象
private ExtensionLoader(Class<?> type) {
  this.type = type;
  // 这里需要对对象的工厂做额外的创建,可以看到扩展的工厂也是一个扩展点
  objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}
  1. ExtensionFactory解读:通过传入扩展点类型和真正的名称来获取扩展的
@SPI
public interface ExtensionFactory {

    /**
     * Get extension.
     *
     * @param type object type.
     * @param name object name.
     * @return object instance.
     */
    <T> T getExtension(Class<T> type, String name);
}
  1. 在dubbo-common/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.common.extension.ExtensionFactory中看到有三个实现的提供
spring=org.apache.dubbo.config.spring.extension.SpringExtensionFactory
adaptive=org.apache.dubbo.common.extension.factory.AdaptiveExtensionFactory
spi=org.apache.dubbo.common.extension.factory.SpiExtensionFactory
  1. AdaptiveExtensionFactory分析:使用@Adaptive标记的。最主要的作用是进行代理其他的ExtensionFactory。其中比较重要的方法是getSupportedExtensions方法,获取所有支持的扩展信息实现
@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {

    private final List<ExtensionFactory> factories;

    public AdaptiveExtensionFactory() {
    		// 获取针对ExtensionFactory扩展加载器
        ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
        List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
        // 获取支持的扩展
        for (String name : loader.getSupportedExtensions()) {
        		// 将所有的ExtensionFactory进行缓存
            list.add(loader.getExtension(name));
        }
        factories = Collections.unmodifiableList(list);
    }

    @Override
    public <T> T getExtension(Class<T> type, String name) {
        for (ExtensionFactory factory : factories) {
        		// 交给每个真实的ExtensionFactory来处理
            T extension = factory.getExtension(type, name);
            if (extension != null) {
                return extension;
            }
        }
        return null;
    }
}
  1. 获取所有支持的扩展信息实现方法ExtensionLoader.getSupportedExtensions分析:比较关键的方法在于getExtensionClasses方法
public Set<String> getSupportedExtensions() {
		// 获取所有的扩展类信息
		Map<String, Class<?>> clazzes = getExtensionClasses();
		// 返回所有的扩展点名称
		return Collections.unmodifiableSet(new TreeSet<>(clazzes.keySet()));
}
  1. getExtensionClasses的实现解析:防止重复被加载
private Map<String, Class<?>> getExtensionClasses() {
	// 从缓存中获取已加载的扩展类
	Map<String, Class<?>> classes = cachedClasses.get();
	// 双重检查
	if (classes == ?null) {
		// 为空的话,则锁住,标识只会被执行一次
		synchronized (cachedClasses) {
			classes = cachedClasses.get();
			if (classes == null) {
				// 进行加载信息 加载扩展类
				classes = loadExtensionClasses();
				cachedClasses.set(classes);
			}
		}
	}
	return classes;
}
  1. loadExtensionClasses方法解析:加载当前SPI的默认实现,加载这个类的所有扩展点实现,并且按照name和Class对象的形式存储
private Map<String, Class<?>> loadExtensionClasses() {
		// 加载默认扩展的实现名称
    cacheDefaultExtensionName();

    // 获取其中每一种实现的名称和对应的classes
		// 具体的目录请参照下面的所有目录
    Map<String, Class<?>> extensionClasses = new HashMap<>();
		
    for (LoadingStrategy strategy : strategies) {
        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
    }
    return extensionClasses;
}
  1. cacheDefaultExtensionName方法解析:主要用于读取注解中value值来获取到默认的名称
private void cacheDefaultExtensionName() {
		// 获取当前类是否包含SPI注解,一般走到这里都是拥有这个注解的
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation == null) {
        return;
    }

		// 来获取其的value值,这个值主要的作用是设置这个SPI中的默认扩展名
		// 比如LoadBalance的默认实现就是random。就是通过这里进行的设置
    String value = defaultAnnotation.value();
    if ((value = value.trim()).length() > 0) {
        String[] names = NAME_SEPARATOR.split(value);
        if (names.length > 1) {
            throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
                    + ": " + Arrays.toString(names));
        }
        if (names.length == 1) {
            cachedDefaultName = names[0];
        }
    }
}
  1. loadDirectory方法解析:主要功能是从这个文件夹中寻找真正的文件列表,并且对其中的文件内容解析并且放入到extensionClasseMap中
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type,
                           boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) {
    // 文件名称规则: 路径/包名.接口名
    String fileName = dir + type;
    try {
    		// 寻找classloader和url列表
        Enumeration<java.net.URL> urls = null;
        ClassLoader classLoader = findClassLoader();

        // 如果需要的话, 需要先从当前类的ClassLoader中寻找
        if (extensionLoaderClassLoaderFirst) {
            ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
            if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
                urls = extensionLoaderClassLoader.getResources(fileName);
            }
        }

				// 如果找不到任何的URL列表,则继续尝试去其当前线程的ClassLoader中寻找
        if (urls == null || !urls.hasMoreElements()) {
            if (classLoader != null) {
                urls = classLoader.getResources(fileName);
            } else {
                urls = ClassLoader.getSystemResources(fileName);
            }
        }
				
				// 如果存在文件的话
        if (urls != null) {
            while (urls.hasMoreElements()) {
            		// 遍历每一个资源文件,并且进行加载资源信息到extensionClasses, 主要功能是读取文件内容
                java.net.URL resourceURL = urls.nextElement();
                loadResource(extensionClasses, classLoader, resourceURL, overridden, excludedPackages);
            }
        }
    } catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                type + ", description file: " + fileName + ").", t);
    }
}
  1. loadResource方法解析:主要是用于读取文件操作,并且将方法交由loadClass来加载类信息
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
                          java.net.URL resourceURL, boolean overridden, String... excludedPackages) {
    try {
    		// 读取文件
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
            		// 截取文件#前面的内容
                final int ci = line.indexOf('#');
                if (ci >= 0) {
                    line = line.substring(0, ci);
                }
                line = line.trim();
                
                // 如果有内容的话
                if (line.length() > 0) {
                    try {
                        String name = null;
                        
                        // 则进行加载key=value的形式数据
                        int i = line.indexOf('=');
                        if (i > 0) {
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0 && !isExcluded(line, excludedPackages)) {
                        		// 对类信息进行加载操作
                            loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name, overridden);
                        }
                    } catch (Throwable t) {
                        IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
                        exceptions.put(line, e);
                    }
                }
            }
        }
    } catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                type + ", class file: " + resourceURL + ") in " + resourceURL, t);
    }
}
  1. loadClass方法解析:最终进行完成类映射的地方
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
                       boolean overridden) throws NoSuchMethodException {
    
    // 当前扩展点的实现,必须是当前扩展接口的实现才可以
    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("Error occurred when loading extension class (interface: " +
                type + ", class line: " + clazz.getName() + "), class "
                + clazz.getName() + " is not subtype of interface.");
    }
    
    // 如果是包含了Adaptive注解,则认为是需要对扩展点包装的方法,这里只做了存储操作,存储至cachedAdaptiveClass中
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        cacheAdaptiveClass(clazz, overridden);
    } else if (isWrapperClass(clazz)) {
    		// 判断是否是wapper类型, 是否构造函数中有该接口类型的传入
				// wrapper类型的意思是,对当前的扩展点实现封装功能处理
        cacheWrapperClass(clazz);
    } else {
        clazz.getConstructor();
        
        // 寻找他是否已经定义过了名称, 主要是获取当前类的org.apache.dubbo.common.Extension注解,如果有的话就使用这个名称,否则的话就是用当前类的简单名称
        if (StringUtils.isEmpty(name)) {
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
            }
        }

				// 否则的话,就对这个名称和class做映射关系
        String[] names = NAME_SEPARATOR.split(name);
        if (ArrayUtils.isNotEmpty(names)) {
        		// 如果当前类拥有Activate注解,则将其进行添加到cachedActivates对象中,意味着需要执行
            cacheActivateClass(clazz, names[0]);
            // 进行名称映射保存
            for (String n : names) {
                cacheName(clazz, n);
                saveInExtensionClass(extensionClasses, clazz, n, overridden);
            }
        }
    }
}

4.4.2 根据name获取扩展点的方法getExtension

  1. getExtension 方法解析:根据name对扩展点进行处理和进行加锁来创建真实的引用,其中都是有使用缓存来处理
public T getExtension(String name, boolean wrap) {
    if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Extension name == null");
    }
    // 获取当前SPi的默认扩展实现类
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    
    // 获取当前类的holder,实现原理和cachedClasses的方式相同,都是建立同一个引用后再进行加锁
    final Holder<Object> holder = getOrCreateHolder(name);
    Object instance = holder.get();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
            		// 真正进行创建实例
                instance = createExtension(name, wrap);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}
  1. getOrCreateHolder方法解析:
private Holder<Object> getOrCreateHolder(String name) {
		// 获取当前名称和对象Holder的映射关系
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
    		// 如果不存在的话,则使用putIfAbsent的原子操作来设置值,这个值可以保证多线程的额情况下有值的时候不处理,没有值进行保存
        cachedInstances.putIfAbsent(name, new Holder<>());
        // 获取真实的holder处理器
        holder = cachedInstances.get(name);
    }
    return holder;
}
  1. createExtension方法解析:根据扩展的class名称来进行创建实例的类。是创建扩展点类的主要实现
private T createExtension(String name, boolean wrap) {
		// 从配置文件中加载所有的扩展类 可以得到配置项名称 到配置类的映射关系
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
    		// 获取是否已经有实例了
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
        		// 没有的话,同样适用putIfAbsent的方式来保证只会创建一个对象并且保存
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        
        // 注入其他扩展点的实体,用于扩展点和其他的扩展点相互打通
        injectExtension(instance);

        if (wrap) {
						// 进行遍历所有的包装类信息,分别对包装的类进行包装实例化,并且返回自身引用
            List<Class<?>> wrapperClassesList = new ArrayList<>();
            if (cachedWrapperClasses != null) {
                wrapperClassesList.addAll(cachedWrapperClasses);
                wrapperClassesList.sort(WrapperComparator.COMPARATOR);
                Collections.reverse(wrapperClassesList);
            }

            if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
                for (Class<?> wrapperClass : wrapperClassesList) {
                		// 同样进行注册其他扩展点的功能
                    Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
                    if (wrapper == null
                            || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
                        instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                    }
                }
            }
        }
				// 对扩展点进行初始化操作
        initExtension(instance);
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                type + ") couldn't be instantiated: " + t.getMessage(), t);
    }
}
  1. injectExtension方法解析:注入其他扩展点的实体
private T injectExtension(T instance) {

    if (objectFactory == null) {
        return instance;
    }

    try {
    		// 遍历其中的所有方法
        for (Method method : instance.getClass().getMethods()) {
        		// 校验:以"set"开头,参数长度为1,是公开的方法
            if (!isSetter(method)) {
                continue;
            }

						// 如果设置了取消注册,则不进行处理
            if (method.getAnnotation(DisableInject.class) != null) {
                continue;
            }
            
            // 获取参数类型,并且非基础类型(String, Integer等类型)
            Class<?> pt = method.getParameterTypes()[0];
            if (ReflectUtils.isPrimitives(pt)) {
                continue;
            }

            try {
            		// 获取需要set的扩展点名称
                String property = getSetterProperty(method);
                // 比如有一个方法为setRandom(LoadBalance loadBalance),那么则以为着需要加载负载均衡中名为random的扩展点
                Object object = objectFactory.getExtension(pt, property);
                if (object != null) {
                    method.invoke(instance, object);
                }
            } catch (Exception e) {
                logger.error("Failed to inject via method " + method.getName()
                        + " of interface " + type.getName() + ": " + e.getMessage(), e);
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}

4.4.3 Adaptive功能实现原理

Adaptive的主要功能是对所有的扩展点进行封装为一个类,通过URL传入参数的时动态选择需要使用的扩展点。其底层的实现原理就是动态代理

  1. getAdaptiveExtension方法解析:
public T getAdaptiveExtension() {
		// 和原先是用相同的方式,进行Holder和加锁的方式来保证只会被创建一次
    Object instance = cachedAdaptiveInstance.get();
    if (instance == null) {
    		// 如果直接已经有创建并且错误的情况,则直接返回错误信息,防止重复没必要的创建
        if (createAdaptiveInstanceError != null) {
            throw new IllegalStateException("Failed to create adaptive instance: " +
                    createAdaptiveInstanceError.toString(),
                    createAdaptiveInstanceError);
        }

        synchronized (cachedAdaptiveInstance) {
            instance = cachedAdaptiveInstance.get();
            if (instance == null) {
                try {
                		// 这里真实的进行创建操作
                    instance = createAdaptiveExtension();
                    cachedAdaptiveInstance.set(instance);
                } catch (Throwable t) {
                    createAdaptiveInstanceError = t;
                    throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
                }
            }
        }
    }
    return (T) instance;
}
  1. createAdaptiveExtension方法解析:主要是进行了一些方法封装
private T createAdaptiveExtension() {
    try {
    		// 这里使用`getAdaptiveExtensionClass`方法进行构建类并且执行实例化
				// 然后和普通的其他class相同,依旧使用injectExtension进行扩展
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    } catch (Exception e) {
        throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
    }
}

private Class<?> getAdaptiveExtensionClass() {
		// 确保已经加载了所有的扩展类信息
    getExtensionClasses();
    
    // 如果已经加载过了,则直接返回
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    
    // 进行构建操作
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
  1. createAdaptiveExtensionClass方法解析:主要是进行生成Adaptive的代码,并且进行编译生成class
private Class<?> createAdaptiveExtensionClass() {
		// 实例化一个新的Adaptive的代码生成器,并且进行代码生成
    String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
    
    // 获取类加载器
    ClassLoader classLoader = findClassLoader();
    
    //通过扩展点,寻找编译器, 目前有Java自带的编译器和Javassist的编译器
    org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
   	
   	// 编译并且生成class
    return compiler.compile(code, classLoader);
}
  1. AdaptiveClassLoaderCodeGenerator.generate()方法解析:进行实现真正的代码生成
public String generate() {
    // 如果没有任何方法标记为Adaptive,则不做处理
    if (!hasAdaptiveMethod()) {
        throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
    }

		// 进行编写代码
    StringBuilder code = new StringBuilder();
    
    // 生成包信息
    code.append(generatePackageInfo());
    // 生成引用信息
    code.append(generateImports());
    // 生成类声明
    code.append(generateClassDeclaration());

		// 生成每一个方法
    Method[] methods = type.getMethods();
    for (Method method : methods) {
        code.append(generateMethod(method));
    }
    
    // 输出最后的一个"}"来结束当前类
    code.append("}");

    if (logger.isDebugEnabled()) {
        logger.debug(code.toString());
    }
    return code.toString();
}
  1. generateMethod方法解析:依赖反射机制去进行方法封装,最终拼接为一个最终字符串
private String generateMethod(Method method) {
		// 方法返回类型
    String methodReturnType = method.getReturnType().getCanonicalName();
    // 方法名称
    String methodName = method.getName();
    // 生成方法内容
    String methodContent = generateMethodContent(method);
    // 生辰参数列表
    String methodArgs = generateMethodArguments(method);
    // 方法抛出的异常
    String methodThrows = generateMethodThrows(method);
    // 格式化为一个字符串
    return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
}
  1. generateMethodContent方法解析:是整个Adaptive中最为核心的代码,包括获取扩展点名称并且执行
private String generateMethodContent(Method method) {
		// 获取Adaptive注解,只支持含有Adaptive注解方法处理
    Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
    StringBuilder code = new StringBuilder(512);
    if (adaptiveAnnotation == null) {
    		// 没有该注解,直接抛出异常
        return generateUnsupported(method);
    } else {
    		// 获取URL参数的所在位置
        int urlTypeIndex = getUrlTypeIndex(method);

        if (urlTypeIndex != -1) {
            // 增加判断url不为空的代码
            code.append(generateUrlNullCheck(urlTypeIndex));
        } else {
            // 获取这个方法中的所有参数列表
						// 寻找每个参数中是否有"get"开头的方法,并且返回值是URL的
						// 如果有则同样认定为找到,否则抛出异常
            code.append(generateUrlAssignmentIndirectly(method));
        }

				// 获取扩展点的适配名称
        String[] value = getMethodAdaptiveValue(adaptiveAnnotation);

				// 判断是否有参数是Invocation类
				// 这里判断的主要目的在于,拥有Invocation时,则获取扩展名称的方式发生改变
				// 存在Invocation时,通过getMethodParameter,否则通过getParameter来执行
				// getMethodParameter是dubboURL中特有的,用于将"test.a"转换为"testA"的形式
        boolean hasInvocation = hasInvocationArgument(method);
				
				// 增加有Invocation类时的不为空判断
        code.append(generateInvocationArgumentNullCheck(method));

				// 生成获取扩展点名称的方法
        code.append(generateExtNameAssignment(value, hasInvocation));
        
        // 检查扩展点不能为空
        code.append(generateExtNameNullCheck(value));

				// 获取扩展点实现
        code.append(generateExtensionAssignment());

        // 返回扩展点中的真实调用
        code.append(generateReturnAndInvocation(method));
    }
    return code.toString();
}

4.5 集群容错源码剖析

4.5.1 集群容错组件

  1. 集群容错的所有组件包括:Cluster、Cluster Invoker、Directory、Router和LoadBalance

  2. 关系图
    在这里插入图片描述

  3. 集群工作过程可分为两个阶段:
    1)第一阶段:在服务消费者初始化期间,集群Cluster实现类为服务消费者创建Cluster Invoker实例,即merge操作
    2)第二阶段:服务消费者进行远程调用时。Cluster Invoker首先会调用Directory的list方法列举Invoker列表,并调用Router的route方法进行路由,过滤掉不符合路由规则的Invoker然后通过LoadBalance从Invoker列表中选择一个Invoker。最后FailoverClusterInvoker会将参数传给LoadBalance选择出的Invoker实例的invoke方法,进行真正的远程调用

  4. Dubbo的容错方式:
    1)Failover Cluster(失败自动切换):失败时会重试其它服务器
    2)Failfast Cluster(快速失败):请求失败后快速返回异常结果,不重试
    3)Failsafe Cluster(失败安全):出现异常直接忽略,会对请求做负载均衡
    4)Failback Cluster(失败自动恢复):请求失败后,会自动记录请求到失败队列中
    5)Forking Cluster(并行调用多个服务提供者):其中有一个返回,则立即返回结果

4.5.2 信息缓存接口Directory

  1. Directory 接口解析:可以通过Directory来找到指定服务中的提供者信息列表
public interface Directory<T> extends Node {

	// 获取服务的类型
    Class<T> getInterface();

	// 根据本次调用的信息来获取所有可以被执行的提供者信息
    List<Invoker<T>> list(Invocation invocation) throws RpcException;
		
	// 获取所有的提供者信息
    List<Invoker<T>> getAllInvokers();
}
  1. Directory实现类AbstractDirectory中的list方法解析:真正实现是子类的doList方法
public List<Invoker<T>> list(Invocation invocation) throws RpcException {
    if (destroyed) {
        throw new RpcException("Directory already destroyed .url: " + getUrl());
    }
	// 交给子类进行处理
    return doList(invocation);
}
  1. 实现子类RegistryDirectory的doList方法解析:这里的实现也相对比较简单,主要依靠routerChain去决定真实返回的提供者列表
public List<Invoker<T>> doList(Invocation invocation) {
	// 当没有提供者的时候会直接抛出异常
    if (forbidden) {
        throw new RpcException();
    }

    if (multiGroup) {
        return this.invokers == null ? Collections.emptyList() : this.invokers;
    }

    List<Invoker<T>> invokers = null;
    try {
        // 交给路由chain去处理并且获取所有的invokers
        invokers = routerChain.route(getConsumerUrl(), invocation);
    } catch (Throwable t) {
        logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
    }

    return invokers == null ? Collections.emptyList() : invokers;
}
  1. RegistryProtocol的refer方法解析:Invoker生成的部分关键代码
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
    // 获取注册中心的地址URL(主要用于转换协议),比如我们是使用的zookeeper,那么他就会转换为zookeeper://
    url = getRegistryUrl(url);
    
    // 获取注册中心配置信息
    Registry registry = registryFactory.getRegistry(url);
    if (RegistryService.class.equals(type)) {
        return proxyFactory.getInvoker((T) registry, type, url);
    }

    // 适用于多个分组时使用
    Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
    String group = qs.get(GROUP_KEY);
    if (group != null && group.length() > 0) {
        if ((COMMA_SPLIT_PATTERN.split(group)).length > 1 || "*".equals(group)) {
            return doRefer(Cluster.getCluster(MergeableCluster.NAME), registry, type, url);
        }
    }

    Cluster cluster = Cluster.getCluster(qs.get(CLUSTER_KEY));
    
    // 真正进行构建invoker和我们上面的Directory
    return doRefer(cluster, registry, type, url);
}
  1. RegistryProtocol的doRefer方法解析:产生Directory并且注册和监听,产生routerChain
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
    // 实例化Directory
    RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
    
    // 设置注册中心和所使用的协议
    directory.setRegistry(registry);
    directory.setProtocol(protocol);
    
    // 生成监听路径URL
    Map<String, String> parameters = new HashMap<String, String>(directory.getConsumerUrl().getParameters());
    URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
    if (directory.isShouldRegister()) {
    		
    	// 在Directory中设置监听的consumerurl地址
        directory.setRegisteredConsumerUrl(subscribeUrl);
        
        // 在注册中心中注册消费者URL,也就是我们之前的Zookeeper的node中看到的consumer://
   		registry.register(directory.getRegisteredConsumerUrl());
    }
    
    // 构建路由链
    directory.buildRouterChain(subscribeUrl);
    
    // 进行监听所有的的provider
    directory.subscribe(toSubscribeUrl(subscribeUrl));

	// 加入到集群中
    Invoker<T> invoker = cluster.join(directory);
    List<RegistryProtocolListener> listeners = findRegistryProtocolListeners(url);
    if (CollectionUtils.isEmpty(listeners)) {
        return invoker;
    }

    RegistryInvokerWrapper<T> registryInvokerWrapper = new RegistryInvokerWrapper<>(directory, cluster, invoker);
    for (RegistryProtocolListener listener : listeners) {
        listener.onRefer(this, registryInvokerWrapper);
    }
    return registryInvokerWrapper;
}
  1. 回到RouterChain的route方法解析:
public List<Invoker<T>> route(URL url, Invocation invocation) {
    // 所有的invoker列表
    List<Invoker<T>> finalInvokers = invokers;
    // 依次交给所有的路由规则进行选取路由列表
    for (Router router : routers) {
        finalInvokers = router.route(finalInvokers, url, invocation);
    }
    return finalInvokers;
}

4.5.3 路由规则实现原理

分析RouterChain中的Router是如何实现的,主要对Router的实现类ConditionRouter进行分析

  1. ConditionRouter类中两个重要的属性
// 是否满足判断条件
protected Map<String, MatchPair> whenCondition;

// 当满足判断条件时如何选择invokers
protected Map<String, MatchPair> thenCondition;
  1. 每一个 MatchPair都有如下两个属性,分别表示满足的条件和不满足的具体条件
// 满足的条件
final Set<String> matches = new HashSet<String>();

// 不满足的条件
final Set<String> mismatches = new HashSet<String>();
  1. ConditionRouter的route方法解析:如何选择Invoker,主要在于判断(matchWhen)和选择(matchThen)的逻辑
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
        throws RpcException {
    // 不启用的时,则直接返回提供者的列表
    if (!enabled) {
        return invokers;
    }

	// 如果不存在任何invoker则直接返回
    if (CollectionUtils.isEmpty(invokers)) {
        return invokers;
    }
    try {
    	// 判断是否满足判断条件,不满足直接返回列表
        if (!matchWhen(url, invocation)) {
            return invokers;
        }
        List<Invoker<T>> result = new ArrayList<Invoker<T>>();
        if (thenCondition == null) {
            logger.warn("The current consumer in the service blacklist. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey());
            return result;
        }
        
        // 依次判断每一个invoker的url是否满足条件
        for (Invoker<T> invoker : invokers) {
            if (matchThen(invoker.getUrl(), url)) {
                result.add(invoker);
            }
        }
        
        // 如果不为空则直接返回
        if (!result.isEmpty()) {
            return result;
        } else if (force) {
        	// 如果为空,并且必须要走这个条件时,则直接返回空
            logger.warn("The route result is empty and force execute. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey() + ", router: " + url.getParameterAndDecoded(RULE_KEY));
            return result;
        }
    } catch (Throwable t) {
        logger.error("Failed to execute condition router rule: " + getUrl() + ", invokers: " + invokers + ", cause: " + t.getMessage(), t);
    }
    return invokers;
}
  1. ConditionRouter中的matchWhen方法和matchThen方法:两者最终底层都是调用的matchCondition方法
boolean matchWhen(URL url, Invocation invocation) {
    // 1. 如果判断条件为空则直接认定为匹配
		// 2. 如果条件匹配则认定为匹配
    return CollectionUtils.isEmptyMap(whenCondition) || matchCondition(whenCondition, url, null, invocation);
}

private boolean matchThen(URL url, URL param) {
		// 判断条件不能为空并且匹配条件规则时才返回
    return CollectionUtils.isNotEmptyMap(thenCondition) && matchCondition(thenCondition, url, param, null);
}
  1. ConditionRouter的init方法解析:生成整个路由规则,其中比较关键的方法为parseRule
public void init(String rule) {
  try {
  	 // 必须包含规则配置
  	 if (rule == null || rule.trim().length() == 0) {
		throw new IllegalArgumentException("Illegal route rule!");
     }
     rule = rule.replace("consumer.", "").replace("provider.", "");
      
	 // 根据"=>"来判断when或者then条件
     int i = rule.indexOf("=>");
     String whenRule = i < 0 ? null : rule.substring(0, i).trim();
     String thenRule = i < 0 ? rule.trim() : rule.substring(i + 2).trim();
      
     // 分别根据"=>"来生成前后的规则
     Map<String, MatchPair> when = StringUtils.isBlank(whenRule) || "true".equals(whenRule) ? new HashMap<String, MatchPair>() : parseRule(whenRule);
     Map<String, MatchPair> then = StringUtils.isBlank(thenRule) || "false".equals(thenRule) ? null : parseRule(thenRule);
     
     this.whenCondition = when;
     this.thenCondition = then;
  } catch (ParseException e) {
     throw new IllegalStateException(e.getMessage(), e);
  }
}
  1. ConditionRouter的parseRule方法解析
private static Map<String, MatchPair> parseRule(String rule) throws ParseException {
    Map<String, MatchPair> condition = new HashMap<String, MatchPair>();
    if (StringUtils.isBlank(rule)) {
        return condition;
    }
    
    // 当前所操作的数据
	// 用于后面循环中使用,标识上一次循环中所操作的信息
    MatchPair pair = null;
    Set<String> values = null;
    
    // 转化每一个条件
	// 这里分别会对每一次的分割做匹配
	// host = 1.1.1.* & host != 1.1.1.2 & method=sayHello
    final Matcher matcher = ROUTE_PATTERN.matcher(rule);
    while (matcher.find()) {
    	// 分隔符
        String separator = matcher.group(1);
        // 内容
        String content = matcher.group(2);
        // 如果不存在分隔符,则认为是首个判断
        if (StringUtils.isEmpty(separator)) {
            pair = new MatchPair();
            // 则直接放入当前condition
            condition.put(content, pair);
        }
        // 如果是"&"则代表并且
        else if ("&".equals(separator)) {
        	// 如果当前的when或者then中不包含该判定条件则添加则放入,否则当前的condition就需要拿出来
            if (condition.get(content) == null) {
                pair = new MatchPair();
                condition.put(content, pair);
            } else {
                pair = condition.get(content);
            }
        }
        // 如果是等于的比较
        else if ("=".equals(separator)) {
            if (pair == null) {
                throw new ParseException();
            }
			// 如果是等于的比较,则需要将值放入matches中
            values = pair.matches;
            values.add(content);
        }
        // The Value in the KV part.
        else if ("!=".equals(separator)) {
            if (pair == null) {
                throw new ParseException();
            }
			// 如果为不等于,则需要放入到不等于中
            values = pair.mismatches;
            values.add(content);
        }
        // 如果values是多个的话
        else if (",".equals(separator)) { // Should be separated by ','
            if (values == null || values.isEmpty()) {
                throw new ParseException();
            }
            // 则分别加入到values列表中
            values.add(content);
        } else {
            throw new ParseException();
        }
    }
    return condition;
}

4.5.4 Cluster组件

Dubbo中的关键组件Cluster,它主要用于代理真正的Invoker执行时做处理,提供了多种容错方案

  1. Cluster接口
// 默认使用failover作为实现
@SPI(Cluster.DEFAULT)
public interface Cluster {
    String DEFAULT = FailoverCluster.NAME;

	// 生成一个新的invoker
    @Adaptive
    <T> Invoker<T> join(Directory<T> directory) throws RpcException;
}
  1. 实现类AbstractCluster的join方法
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
    // 使用子类doJoin来真正生成Invoker,并且使用拦截器的方式进行一层封装
    return buildClusterInterceptors(doJoin(directory), directory.getUrl().getParameter(REFERENCE_INTERCEPTOR_KEY));
}

// 对invoker进行封装
private <T> Invoker<T> buildClusterInterceptors(AbstractClusterInvoker<T> clusterInvoker, String key) {
    AbstractClusterInvoker<T> last = clusterInvoker;
    
    // 获取所有的拦截器
    List<ClusterInterceptor> interceptors = ExtensionLoader.getExtensionLoader(ClusterInterceptor.class).getActivateExtension(clusterInvoker.getUrl(), key);

    if (!interceptors.isEmpty()) {
        for (int i = interceptors.size() - 1; i >= 0; i--) {
        	// 对拦截器进行一层封装
            final ClusterInterceptor interceptor = interceptors.get(i);
            final AbstractClusterInvoker<T> next = last;
            last = new InterceptorInvokerNode<>(clusterInvoker, interceptor, next);
        }
    }
    return last;
}
  1. 实现类FailoverCluster的doJoin方法解析:这里面比较简单,只是进行new了一个新的
    Invoker
public class FailoverCluster extends AbstractCluster {

    public final static String NAME = "failover";

    @Override
    public <T> AbstractClusterInvoker<T> doJoin(Directory<T> directory) throws RpcException {
        return new FailoverClusterInvoker<>(directory);
    }
}
  1. AbstractClusterInvoker的invoke方法解析:观察Invoker接口得知,其中最关键的方式是invoke方法。我们也可以看到,他也是通过Abstract进行了一层封装
public Result invoke(final Invocation invocation) throws RpcException {
	// 检查是否已经关闭了
    checkWhetherDestroyed();

    // 拷贝当前RPCContext中的附加信息到当前的invocation中
    Map<String, Object> contextAttachments = RpcContext.getContext().getObjectAttachments();
    if (contextAttachments != null && contextAttachments.size() != 0) {
        ((RpcInvocation) invocation).addObjectAttachments(contextAttachments);
    }

	// 找寻出所有支持的invoker,已经路由过的
    List<Invoker<T>> invokers = list(invocation);
    
    // 初始化负载均衡器
    LoadBalance loadbalance = initLoadBalance(invokers, invocation);
    
    // 用于适配异步请求使用
    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
    
    // 交给子类进行真正处理请求
    return doInvoke(invocation, invokers, loadbalance);
}
  1. FailoverClusterInvoker中的doInvoke方法解析:通过for循环的形式来达到重试次数的目的,并且每次重试否会重新走一遍路由等规则
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    
    // 如果没有任何的invoker则抛出异常
    List<Invoker<T>> copyInvokers = invokers;
    checkInvokers(copyInvokers, invocation);
    
    String methodName = RpcUtils.getMethodName(invocation);
    
    // 获取这个方法最大的重试次数
    int len = getUrl().getMethodParameter(methodName, RETRIES_KEY, DEFAULT_RETRIES) + 1;
    if (len <= 0) {
        len = 1;
    }
    
    // 通过for循环的形式表示可以重试的次数
    RpcException le = null; 
    List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyInvokers.size());
    Set<String> providers = new HashSet<String>(len);
    for (int i = 0; i < len; i++) {

        if (i > 0) {
        	// 每次都执行一次是否关闭当前consumer的判断
            checkWhetherDestroyed();
            // 重新获取一遍invoker列表
            copyInvokers = list(invocation);
            // 再次进行一次存在invoker的检查
            checkInvokers(copyInvokers, invocation);
        }
        
        // 选择具体的invoker(交给负载均衡)
        Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
        
        // 增加到已经执行过得invoker列表中
        invoked.add(invoker);
        RpcContext.getContext().setInvokers((List) invoked);
        try {
        
        	// 让其真正的去进行执行操作
            Result result = invoker.invoke(invocation);
            if (le != null && logger.isWarnEnabled()) {
                logger.warn("");
            }
            return result;
        } catch (RpcException e) {
        	// 如果是业务异常则直接抛出
            if (e.isBiz()) {
                throw e;
            }
            le = e;
        } catch (Throwable e) {
            le = new RpcException(e.getMessage(), e);
        } finally {
            providers.add(invoker.getUrl().getAddress());
        }
    }
    // 如果重试了指定次数后依旧失败,则直接认定为失败
    throw new RpcException();// 次数的异常信息省略
}

4.5.5 负载均衡实现原理

  1. LoadBalance接口解析:默认选择了随机算法
// 默认使用随机算法
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
		
		// 进行选择真正的invoker
    @Adaptive("loadbalance")
    <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}
  1. 实现类AbstractLoadBalance的select方法解析:主要就是处理只有一个invoker的情况
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
		// 如果不存在任何的invoker则直接返回
    if (CollectionUtils.isEmpty(invokers)) {
        return null;
    }
    
    // 如果还有一个invoker则直接返回,不需要执行负载均衡
    if (invokers.size() == 1) {
        return invokers.get(0);
    }
    
    // 交给子类进行实现
    return doSelect(invokers, url, invocation);
}
  1. 随机算法实现RandomLoadBalance的doSelect方法:比较关键在于权重的概念。通过权重选取了不同的机器
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    // 总计的invoker列表数量
    int length = invokers.size();
    /// 默认每个invoker的权重都是相同的
    boolean sameWeight = true;
    // 所有的权重列表
    int[] weights = new int[length];
    // 首个invoker的权重信息
    int firstWeight = getWeight(invokers.get(0), invocation);
    weights[0] = firstWeight;
    // 计算总共的权重,并且把每一个invoker的权重进行设置到列表中
    int totalWeight = firstWeight;
    for (int i = 1; i < length; i++) {
        int weight = getWeight(invokers.get(i), invocation);
        // save for later use
        weights[i] = weight;
        // Sum
        totalWeight += weight;
        if (sameWeight && weight != firstWeight) {
            sameWeight = false;
        }
    }
    
    // 如果权重不相同
    if (totalWeight > 0 && !sameWeight) {
     	// 通过总共的权重来随机分配
        int offset = ThreadLocalRandom.current().nextInt(totalWeight);
  		// 看看最终落到哪一个机器上去
        for (int i = 0; i < length; i++) {
            offset -= weights[i];
            if (offset < 0) {
                return invokers.get(i);
            }
        }
    }
	// 如果权重都是相同的话,则随机选取一个即可
    return invokers.get(ThreadLocalRandom.current().nextInt(length));
}

4.5.6 Invoker执行逻辑

  1. Invoker接口
public interface Invoker<T> extends Node {
	// 当前执行器的服务接口是哪一个
    Class<T> getInterface();

	// 执行请求操作
    Result invoke(Invocation invocation) throws RpcException;
}
  1. 实现类AbstractInvoker的方法
public Result invoke(Invocation inv) throws RpcException {
    // 判断系统是否已经关闭
    if (destroyed.get()) {
        logger.warn("");
    }
    RpcInvocation invocation = (RpcInvocation) inv;
    invocation.setInvoker(this);
    
    // 设置所有的RPCContext中的附加信息
    if (CollectionUtils.isNotEmptyMap(attachment)) {
        invocation.addObjectAttachmentsIfAbsent(attachment);
    }

    Map<String, Object> contextAttachments = RpcContext.getContext().getObjectAttachments();
    if (CollectionUtils.isNotEmptyMap(contextAttachments)) {
        invocation.addObjectAttachments(contextAttachments);
    }

	// 获取执行的模式
    invocation.setInvokeMode(RpcUtils.getInvokeMode(url, invocation));
    
    // 设置执行id,主要用于适配异步模式使用
    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);

    AsyncRpcResult asyncResult;
    try {
    	// 交给子类进行真正的执行
        asyncResult = (AsyncRpcResult) doInvoke(invocation);
    } catch (InvocationTargetException e) { 
    	// 业务异常
        Throwable te = e.getTargetException();
        if (te == null) {
            asyncResult = AsyncRpcResult.newDefaultAsyncResult(null, e, invocation);
        } else {
            if (te instanceof RpcException) {
                ((RpcException) te).setCode(RpcException.BIZ_EXCEPTION);
            }
            asyncResult = AsyncRpcResult.newDefaultAsyncResult(null, te, invocation);
        }
    } catch (RpcException e) {
    	// RPC阶段出现了异常
        if (e.isBiz()) {
            asyncResult = AsyncRpcResult.newDefaultAsyncResult(null, e, invocation);
        } else {
            throw e;
        }
    } catch (Throwable e) {
        asyncResult = AsyncRpcResult.newDefaultAsyncResult(null, e, invocation);
    }
    
    // 设置执行的结果信息
    RpcContext.getContext().setFuture(new FutureAdapter(asyncResult.getResponseFuture()));
    return asyncResult;
}
  1. DubboInvoker的doInvoke方法
protected Result doInvoke(final Invocation invocation) throws Throwable {
    RpcInvocation inv = (RpcInvocation) invocation;
    final String methodName = RpcUtils.getMethodName(invocation);
    inv.setAttachment(PATH_KEY, getUrl().getPath());
    inv.setAttachment(VERSION_KEY, version);

	// 传输的客户端
    ExchangeClient currentClient;
    if (clients.length == 1) {
        currentClient = clients[0];
    } else {
        currentClient = clients[index.getAndIncrement() % clients.length];
    }
    try {
    	// 是否返回值,也就是相当于发送了一个指令,不在乎服务端的返回,通常适用于异步请求
        boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
        
        // 获取超时的配置
        int timeout = calculateTimeout(invocation, methodName);
        if (isOneway) {
        	// 如果不需要返回值信息(异步)
            boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
            // 发送命令
            currentClient.send(inv, isSent);
            // 告知为异步的结果
            return AsyncRpcResult.newDefaultAsyncResult(invocation);
        } else {
        	// 获取真正执行的线程池(ThreadPool中的SPI)
            ExecutorService executor = getCallbackExecutor(getUrl(), inv);
            
            // 发送请求并且等待结果
            CompletableFuture<AppResponse> appResponseFuture =
                    currentClient.request(inv, timeout, executor).thenApply(obj -> (AppResponse) obj);
            // 在2.6.x中使用,设置完成的额结果信息
            FutureContext.getContext().setCompatibleFuture(appResponseFuture);
            // 创建新的结果信息并且返回
            AsyncRpcResult result = new AsyncRpcResult(appResponseFuture, inv);
            result.setExecutor(executor);
            return result;
        }
    } catch (TimeoutException e) {
        throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
    } catch (RemotingException e) {
        throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
    }
}
  1. ExchangeChannel接口中的request方法解析:执行exchangeClient.request方法位于ExchangeChannel中
// 真实的发送请求信息
// request: RPCInvocation
// timeout: 超时
// executor: 业务线程池
CompletableFuture<Object> request(Object request, int timeout, ExecutorService executor) throws RemotingException;
  1. 实现类HeaderExchangeClient的request方法:
public CompletableFuture<Object> request(Object request, int timeout, ExecutorService executor) throws RemotingException {
	return channel.request(request, timeout, executor);
}
  1. 实现类HeaderExchangeChannel的request方法:
public CompletableFuture<Object> request(Object request, int timeout, ExecutorService executor) throws RemotingException {
    if (closed) {
        throw new RemotingException(this.getLocalAddress(), null, "Failed to send request " + request + ", cause: The channel " + this + " is closed!");
    }
    // 创建一个新的request对象
    Request req = new Request();
    req.setVersion(Version.getProtocolVersion());
    req.setTwoWay(true);
    req.setData(request);
    // 创建一个执行结果的回调信息处理
    DefaultFuture future = DefaultFuture.newFuture(channel, req, timeout, executor);
    try {
    		// 交给真正的业务渠道进行处理
				// 这里的渠道是交给Transporter这个SPI进行创建的,其中我们的NettyChannel就是在这里产生的
        channel.send(req);
    } catch (RemotingException e) {
    		// 请求出现异常则取消当前的请求封装
        future.cancel();
        throw e;
    }
    return future;
}

4.6 网络通信原理剖析

  1. dubbo协议采用固定长度的消息头(16字节)和不定长度的消息体来进行数据传输
  2. 消息头定义了底层框架(netty)在IO线程处理时需要的信息

4.6.1 数据包结构讲解

  1. IP、TCP、Dubo的消息头
    在这里插入图片描述

  2. Dubbo消息头详情
    1)Magic-MagicHigh&MagicLow(16bits) 标识协议版本号,Dubbo协议:0xdabb
    2)SerializationID(5bit) 标识序列化类型:比如fastjson的值为6。
    3)Event(1bit) 标识是否是事件消息,例如,心跳事件。如果这是一个事件,则设置为1。
    4)2Way(1bit) 仅在Req/Res为1(请求)时才有用,标记是否期望从服务器返回值。如果需要来自服务器的返回值,则设置为1。
    5)Req/Res(1bit) 标识是请求或响应。请求:1;响应:0。
    6)Status(8bits) 仅在Req/Res为0(响应)时有用,用于标识响应的状态。
      20-OK
      30-CLIENT_TIMEOUT
      31-SERVER_TIMEOUT
      40-BAD_REQUEST
      50-BAD_RESPONSE
      60-SERVICE_NOT_FOUND
      70-SERVICE_ERROR
      80-SERVER_ERROR
      90-CLIENT_ERROR
      100-SERVER_THREADPOOL_EXHAUSTED_ERROR
    7)RequestID(64bits) 标识唯一请求。类型为long。
    8)DataLength(32bits) 序列化后的内容长度(可变部分),按字节计数。int类型

  3. Dubbo消息体详情
    被特定的序列化类型(由序列化ID标识)序列化后,每个部分都是一个byte[]或者byte
    如果是请求包 ( Req/Res = 1),则每个部分依次为:
    1)Dubbo version
    2)Service name
    3)Service version
    4)Method name
    5)Method parameter types
    6)Method arguments
    7)Attachments
    如果是响应包(Req/Res = 0),则每个部分依次为:
    1)返回值类型(byte),标识从服务器端返回的值类型:
    2)返回空值:RESPONSE_NULL_VALUE 2
    3)正常响应值: RESPONSE_VALUE 1
    4)异常:RESPONSE_WITH_EXCEPTION 0
    5)返回值:从服务端返回的响应bytes

  4. 优缺点
    优点
    1)协议设计上很紧凑,能用1个bit表示的,不会用一个byte来表示,比如boolean类型的标识
    2)请求、响应的header一致,通过序列化器对content组装特定的内容,代码实现起来简单

缺点
1)类似于 http 请求,通过header就可以确定要访问的资源,而Dubbo需要涉及到用特定序列化协议才可以将服务名、方法、方法签名解析出来
2)通过req/res是否是请求后,可以精细定制协议,去掉一些不需要的标识和添加一些特定的标识。比如status,twoWay标识可以严格定制,去掉冗余标识
3)Dubbo会将服务名com.alibaba.middleware.xxx 转换为Lcom/alibaba/middleware/xxx; 理论上是不必要的,最后追加一个;即可
4)Dubbo协议没有预留扩展字段,没法新增标识,扩展性不太好,比如新增响应上下文的功能,只有改协议版本号的方式,但是这样要求客户端和服务端的版本都进行升级,对于分布式场景很不友好

4.6.2 数据协议ExchangeCodec详解

  1. ExchangeCodec类的常量定义
// 消息头的长度
protected static final int HEADER_LENGTH = 16;
// 标示为0-15位
protected static final short MAGIC = (short) 0xdabb;
protected static final byte MAGIC_HIGH = Bytes.short2bytes(MAGIC)[0];
protected static final byte MAGIC_LOW = Bytes.short2bytes(MAGIC)[1];
// 消息头中的内容
protected static final byte FLAG_REQUEST = (byte) 0x80;
protected static final byte FLAG_TWOWAY = (byte) 0x40;
protected static final byte FLAG_EVENT = (byte) 0x20;
protected static final int SERIALIZATION_MASK = 0x1f;
  1. ExchangeCodec类中encode方法解析:用于将数据发送到ByteBuffer中
public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
    // 处理请求对象
    if (msg instanceof Request) {
        encodeRequest(channel, buffer, (Request) msg);
    } else if (msg instanceof Response) {
    	// 处理响应
        encodeResponse(channel, buffer, (Response) msg);
    } else {
    	// 其他的交给上级处理,用于telnet模式
        super.encode(channel, buffer, msg);
    }
}
  1. ExchangeCodec类中的encodeRequest方法
protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
	// 请求的序列化类型
    Serialization serialization = getSerialization(channel);
    // 写入header信息
    byte[] header = new byte[HEADER_LENGTH];
    // 模数0-15位
    Bytes.short2bytes(MAGIC, header);

    // 标记为请求
    header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());
		
	// 是否是单向还是双向的(异步)
    if (req.isTwoWay()) {
        header[2] |= FLAG_TWOWAY;
    }
    
    // 是否为事件(心跳)
    if (req.isEvent()) {
        header[2] |= FLAG_EVENT;
    }

    // 写入当前的请求ID
    Bytes.long2bytes(req.getId(), header, 4);

    // 保存当前写入的位置,将其写入的位置往后面偏移,保留出写入内容大小的位置,先进行写入body内容
    int savedWriteIndex = buffer.writerIndex();
    buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
    ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
    ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
    // 按照数据内容的不同,来写入不同的内容
    if (req.isEvent()) {
        encodeEventData(channel, out, req.getData());
    } else {
        encodeRequestData(channel, out, req.getData(), req.getVersion());
    }
    out.flushBuffer();
    if (out instanceof Cleanable) {
        ((Cleanable) out).cleanup();
    }
    bos.flush();
    bos.close();
    
    // 记录body中写入的长度
    int len = bos.writtenBytes();
    checkPayload(channel, len);
    // 将其写入到header中的位置中
    Bytes.int2bytes(len, header, 12);

    // 发送到buffer中
    buffer.writerIndex(savedWriteIndex);
    buffer.writeBytes(header); 
    buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
}
  1. 子类DubboCodec中的encodeRequestData方法
protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
    RpcInvocation inv = (RpcInvocation) data;

	// 写入版本
    out.writeUTF(version);
    // 接口全名称
    String serviceName = inv.getAttachment(INTERFACE_KEY);
    if (serviceName == null) {
        serviceName = inv.getAttachment(PATH_KEY);
    }
    out.writeUTF(serviceName);
    
    // 接口版本号
    out.writeUTF(inv.getAttachment(VERSION_KEY));
	// 写入方法名称
    out.writeUTF(inv.getMethodName());
    // 调用参数描述信息
    out.writeUTF(inv.getParameterTypesDesc());
    // 所有的请求参数写入
    Object[] args = inv.getArguments();
    if (args != null) {
        for (int i = 0; i < args.length; i++) {
            out.writeObject(encodeInvocationArgument(channel, inv, i));
        }
    }
    // 写入所有的附加信息
    out.writeAttachments(inv.getObjectAttachments());
}
  1. ExchangeCodec类中encodeResponse方法:
protected void encodeResponse(Channel channel, ChannelBuffer buffer, Response res) throws IOException {
    int savedWriteIndex = buffer.writerIndex();
    try {
        Serialization serialization = getSerialization(channel);
        // 和之前的参数一致
        byte[] header = new byte[HEADER_LENGTH];
        
        Bytes.short2bytes(MAGIC, header);
        // set request and serialization flag.
        header[2] = serialization.getContentTypeId();
        if (res.isHeartbeat()) {
            header[2] |= FLAG_EVENT;
        }
        // 写入状态码
        byte status = res.getStatus();
        header[3] = status;
        // 写入内容
        Bytes.long2bytes(res.getId(), header, 4);

		// 和Request一样的内容写入方式,先写入内容,再写入长度
        buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
        ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
        ObjectOutput out = serialization.serialize(channel.getUrl(), bos);

        if (status == Response.OK) {
            if (res.isHeartbeat()) {
                encodeEventData(channel, out, res.getResult());
            } else {
                encodeResponseData(channel, out, res.getResult(), res.getVersion());
            }
        } else {
        	// 这里不太一样的地方在于,如果错误的时候,则直接将错误信息写入,不需要再交由序列化
            out.writeUTF(res.getErrorMessage());
        }
        out.flushBuffer();
        if (out instanceof Cleanable) {
            ((Cleanable) out).cleanup();
        }
        bos.flush();
        bos.close();
				
		// 一样的写入模式
        int len = bos.writtenBytes();
        checkPayload(channel, len);
        Bytes.int2bytes(len, header, 12);
        // write
        buffer.writerIndex(savedWriteIndex);
        buffer.writeBytes(header); // write header.
        buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
    } catch (Throwable t) {
        // 写入出现异常
        buffer.writerIndex(savedWriteIndex);
        
        if (!res.isEvent() && res.getStatus() != Response.BAD_RESPONSE) {
            Response r = new Response(res.getId(), res.getVersion());
            r.setStatus(Response.BAD_RESPONSE);
						
						// 如果是超过内容长度则重新设置内容大小并写入
            if (t instanceof ExceedPayloadLimitException) {
                logger.warn(t.getMessage(), t);
                try {
                    r.setErrorMessage(t.getMessage());
                    channel.send(r);
                    return;
                } catch (RemotingException e) {
                    logger.warn("Failed to send bad_response info back: " + t.getMessage() + ", cause: " + e.getMessage(), e);
                }
            } else {
                logger.warn("Fail to encode response: " + res + ", send bad_response info instead, cause: " + t.getMessage(), t);
                try {
                    r.setErrorMessage("Failed to send response: " + res + ", cause: " + StringUtils.toString(t));
                    channel.send(r);
                    return;
                } catch (RemotingException e) {
                    logger.warn("Failed to send bad_response info back: " + res + ", cause: " + e.getMessage(), e);
                }
            }
        }
        // 其他的则抛出异常,省去代码
    }
}
  1. 子类DubboCodec中的encodeResponseData方法
protected void encodeResponseData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
    Result result = (Result) data;
    // 是否支持返回attachment参数
    boolean attach = Version.isSupportResponseAttachment(version);
    Throwable th = result.getException();
    if (th == null) {
    		// 如果没有异常信息,则直接写入内容
        Object ret = result.getValue();
        if (ret == null) {
            out.writeByte(attach ? RESPONSE_NULL_VALUE_WITH_ATTACHMENTS : RESPONSE_NULL_VALUE);
        } else {
            out.writeByte(attach ? RESPONSE_VALUE_WITH_ATTACHMENTS : RESPONSE_VALUE);
            out.writeObject(ret);
        }
    } else {
        out.writeByte(attach ? RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS : RESPONSE_WITH_EXCEPTION);
        out.writeThrowable(th);
    }

    if (attach) {
        // returns current version of Response to consumer side.
        result.getObjectAttachments().put(DUBBO_VERSION_KEY, Version.getProtocolVersion());
        out.writeAttachments(result.getObjectAttachments());
    }
}
  1. ExchangeCodec类中的decode方法
public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
		// 可读字节数
    int readable = buffer.readableBytes();
    // 选取可读字节数 HEADER_LENGTH中小的
    byte[] header = new byte[Math.min(readable, HEADER_LENGTH)];
    buffer.readBytes(header);
    return decode(channel, buffer, readable, header);
}

@Override
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
    // 检查魔数
    if (readable > 0 && header[0] != MAGIC_HIGH
            || readable > 1 && header[1] != MAGIC_LOW) {
        int length = header.length;
        if (header.length < readable) {
            header = Bytes.copyOf(header, readable);
            buffer.readBytes(header, length, readable - length);
        }
        for (int i = 1; i < header.length - 1; i++) {
            if (header[i] == MAGIC_HIGH && header[i + 1] == MAGIC_LOW) {
                buffer.readerIndex(buffer.readerIndex() - header.length + i);
                header = Bytes.copyOf(header, i);
                break;
            }
        }
        return super.decode(channel, buffer, readable, header);
    }
    // check length. 不完整的包 需要继续读取
    if (readable < HEADER_LENGTH) {
        return DecodeResult.NEED_MORE_INPUT;
    }

    // 获取数据长度
    int len = Bytes.bytes2int(header, 12);
    checkPayload(channel, len);

	// 需要继续读取
    int tt = len + HEADER_LENGTH;
    if (readable < tt) {
        return DecodeResult.NEED_MORE_INPUT;
    }

    ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);

    try {
    	// 解码数据
        return decodeBody(channel, is, header);
    } finally {
        if (is.available() > 0) {
            try {
                if (logger.isWarnEnabled()) {
                    logger.warn("Skip input stream " + is.available());
                }
                StreamUtils.skipUnusedStream(is);
            } catch (IOException e) {
                logger.warn(e.getMessage(), e);
            }
        }
    }
}
  1. ExchangeCodec类中的decodeBody方法
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
    byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
    // 获取请求ID
    long id = Bytes.bytes2long(header, 4);
    
    // 判断是请求还是响应
    if ((flag & FLAG_REQUEST) == 0) {
        // 说明是响应
        Response res = new Response(id);
        
        // 是否是event事件
        if ((flag & FLAG_EVENT) != 0) {
            res.setEvent(true);
        }
        // 获取请求的状态码
        byte status = header[3];
        res.setStatus(status);
        try {
        	// 进行数据内容解析
            ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
            if (status == Response.OK) {
                Object data;
                // 根据不同的类型来进行解析
                if (res.isHeartbeat()) {
                    data = decodeHeartbeatData(channel, in);
                } else if (res.isEvent()) {
                    data = decodeEventData(channel, in);
                } else {
                    data = decodeResponseData(channel, in, getRequestData(id));
                }
                res.setResult(data);
            } else {
                res.setErrorMessage(in.readUTF());
            }
        } catch (Throwable t) {
            res.setStatus(Response.CLIENT_ERROR);
            res.setErrorMessage(StringUtils.toString(t));
        }
        return res;
    } else {
        // 解析为请求
        Request req = new Request(id);
        req.setVersion(Version.getProtocolVersion());
        req.setTwoWay((flag & FLAG_TWOWAY) != 0);
        if ((flag & FLAG_EVENT) != 0) {
            req.setEvent(true);
        }
        try {
        	// 与响应相同,进行内容解析
            ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
            Object data;
            if (req.isHeartbeat()) {
                data = decodeHeartbeatData(channel, in);
            } else if (req.isEvent()) {
                data = decodeEventData(channel, in);
            } else {
                data = decodeRequestData(channel, in);
            }
            req.setData(data);
        } catch (Throwable t) {
            req.setBroken(true);
            req.setData(t);
        }
        return req;
    }
}

4.6.3 处理拆包和粘包问题

4.6.3.1 处理拆包问题
  1. 当发生TCP拆包问题时候,这里假设之前还没有发生过任何数据交互,系统刚刚初始化好,那么这个时候在InternalDecoder里面的buffer属性会是EMPTY_BUFFER。

  2. 当发生第一次inbound数据的时候,第一次在InternalDecoder里面接收的肯定是dubbo消息头的部分(这个由TCP协议保证)

  3. 由于发生了拆包情况,那么此时接收的inbound消息可能存在一下几种情况
      1)当前inbound消息只包含dubbo协议头的一部分
      2)当前inbound消息只包含dubbo的协议头
      3)当前inbound消息只包含dubbo消息头和部分payload消息

  4. 拆包原理解析:
      1)发生上面三种情况,都会触发ExchangeCodec返回NEED_MORE_INPUT,由于在DubboCountCodec对于返回NEED_MORE_INPUT会回滚读索引,所以此时的buffer里面的数据可以当作并没有发生过读取操作,并且DubboCountCodec的decode也会返回NEED_MORE_INPUT
      2)在InternalDecoder对于当判断返回NEED_MORE_INPUT,也会进行读索引回滚,并且退出循环,最后会执行finally内容,这里会判断inbound消息是否还有可读的
      3)由于在DubboCountCodec里面进行了读索引回滚,所以此时的buffer里面不是完整的inbound消息,等待第二次的inbound消息的到来,当第二次inbound消息过来的时候,再次经过上面的判断

  5. 源码分析:org.apache.dubbo.remoting.transport.netty.NettyCodecAdapter.InternalDecoder.messageReceived

4.6.3.1 处理粘包问题
  1. 当发生TCP粘包的时候,是tcp将一个dubbo协议栈放在一个tcp包中
  2. 有可能发生下面几种情况
      1)当前inbound消息只包含一个dubbo协议栈
      2)当前inbound消息包含一个dubbo协议栈,同时包含部分另一个或者多个dubbo协议栈内容
  3. 如果发生只包含一个协议栈,那么当前buffer通过ExchangeCodec解析协议之后,当前的buffer的readeIndex位置应该是buffer尾部,那么在返回到InternalDecoder中message的方法readable返回的是false,那么就会对buffer重新赋予EMPTY_BUFFER实体
  4. 针对包含一个以上的dubbo协议栈,当然也会解析出其中一个dubbo协议栈,但是经过ExchangeCodec解析之后,message的readIndex不在message尾部,所以message的readable方法返回的是true。那么则会继续遍历message,读取下面的信息
  5. 最终要么message刚好整数倍包含完整的dubbo协议栈,要不ExchangeCodec返回NEED_MORE_INPUT,最后将未读完的数据缓存到buffer中,等待下次inbound事件,将buffer中的消息合并到下次的inbound消息中,种类又回到了拆包的问题上

注:dubbo在处理tcp的粘包和拆包时是借助InternalDecoder的buffer缓存对象来缓存不完整的dubbo协议栈数据,等待下次inbound事件,合并进去。所以说在dubbo中解决TCP拆包和粘包的时候是通过buffer变量来解决的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值