Dubbo分布式框架学习(2)
学习链接
源自 - 千锋教育Dubbo教程,分布式服务框架dubbo源码级应用教程 - B站视频
dubbo-learn 所有代码在这里
dubbo官网文档
dubb官方使用示例代码 - github
文章目录
- 学习链接
- ⼀、架构演进
- 1.单体架构
- 2.垂直应⽤架构
- 3.分布式应⽤架构阶段
- 4.微服务架构阶段
- ⼆、注册中⼼
- 1.概述
- 2.搭建zookeeper注册中⼼
- 三、RPC及Dubbo
- 1.什么是RPC
- 2.⼿写RPC项⽬
- pom.xml
- 服务提供者
- Provider
- HelloService
- HelloServiceImpl
- RemoteMpRegister
- LocalRegister
- Protocol
- Url
- ProtocolFactory
- HttpProtocol
- HttpServer(手动启动tomcat)
- HttpClient
DubboProtocol- DispatcherServlet
- HttpServerHandler
- 服务消费者
- 总结
- 3.什么是dubbo
- 4.dubbo怎么实现远程通信?
- 5.dubbo初体验
- my-dubbo-interfaces 公共接口定义
- my-dubbo-service-provider 服务提供者
- my-dubbo-service-consumer 服务消费者
- 启动zookeeper
- 测试
- 服务代理的过程
- 6.dubbo内部结构
- 四、Springboot中使⽤dubbo
- 五、dubbo⽤法示例
- 1.version版本号
- 2.指定protocol协议
- 3.使⽤rest访问dubbo的服务
- 4.消费者通过url直连指定的服务提供者
- 5.服务超时
- 6.集群容错
- 7.服务降级
- 8.本地存根
- 9.参数回调
- 10.异步调用
- 六、dubbo的负载均衡策略
- 七、安装Dubbo admin监管平台
- 八、Dubbo的SPI可扩展机制
- 九、Dubbo源码剖析
⼀、架构演进
为什么现在的系统不⽤单体架构,⽽⽤微服务架构。
1.单体架构
为了提⾼系统的吞吐量,单体架构中的垂直升级和⽔平扩展,存在以下⼏个问题
- 提升的性能是有限的
- 成本过⾼,没有办法针对某⼀个具体的模块做性能的提升,因为单体,所有模块都是在⼀
起的。 - 更新、维护成本⾮常⾼,对于系统中要修改或增加的功能,整个发布的流程⾮常麻烦。
- 某⼀个模块出现了bug,就会影响整个系统。
2.垂直应⽤架构
根据业务的边界,对单体应⽤进⾏垂直拆分,将⼀个系统拆分成多个服务。⽐如⼀个管理系
统,可以拆分成权限系统、业务系统、数据系统等。
垂直应⽤存在的问题:
每个独⽴部署的服务之间,公共的部分需要部署多份。那么对公共部分的修改、部署、更新
都需要重复的操作,带来⽐较⼤的成本
为了解决这⼀问题,接下来就进⼊到分布式应⽤架构阶段。
3.分布式应⽤架构阶段
在这个阶段⾥,将服务内部公⽤的模块抽取出来,部署成⼀个独⽴的服务,那么需要解决服
务之间的⾼效通信问题。
但是分布式应⽤架构阶段会存在新的问题:
- 服务越来越多,这么多服务如何被发现?
- 服务越来越多,服务如何被治理?
- 服务之间如何实现⾼效通信?
4.微服务架构阶段
微服务架构阶段主要解决的⼏个问题:
- 服务的注册和发现:这么多服务如何注册。这么多服务如何被发现
- 服务之间的⾼效调⽤:使⽤rpc或者http进⾏通信
- 服务治理:服务权重、负载均衡、服务熔断、限流等等⼀些服务治理⽅⾯的问题
⼆、注册中⼼
1.概述
注册中⼼的选择有很多:
- ⾃研:redis
- zookeeper(dubbo的推荐):zk是⼀个分布式服务组件中的⼀个⾮常重要的组件,⾥⾯ 涉及到很多优秀的分布式设计思想,堪称⿐祖地位。
- nacos:nacos既可以作为注册中⼼使⽤,也可以作为分布式配置中⼼使⽤
- eureka:eureka是spring cloud netflix框架中著名的注册中⼼,⾥⾯的服务的续约、⼼ 跳等等的设计⾮常的经典。
2.搭建zookeeper注册中⼼
- 克隆⼀个虚拟机
- 安装jdk
- 解压zk的压缩包
- 进⼊到conf⽂件夹内,重命名:zoo_samples.cfg->zoo.cfg 进⼊到bin中,使⽤命令来操作zk
./zkServer.sh start # 启动zk
./zkServer.sh status # 查看zk状态,如果状态是:Mode: standalone 表示启动成功
./zkServer.sh stop # 关闭zk
三、RPC及Dubbo
1.什么是RPC
dubbo是⼀款⾼性能的rpc框架。什么是rpc呢?
rpc是⼀种协议:是⼀种远程过程调⽤(remote procudure call)协议
rpc协议是在应⽤层之上的协议,规定了通信双⽅进⾏通信的数据格式是什么样的,及数据如 何传输:
- 指明调⽤的类或接⼝
- 指明调⽤的⽅法及参数
2.⼿写RPC项⽬
完整代码:dubbo-learn / dubbo-init-demo
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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.qf</groupId>
<artifactId>dubbo-init-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.18</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.10</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.16.Final</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.3</version>
</dependency>
</dependencies>
</project>
服务提供者
Provider
public class Provider {
public static void main(String[] args) {
URL url = new URL("localhost",8080);
//模拟远程注册中心
RemoteMapRegister.regist(HelloService.class.getName(), url);
//指明服务的实现类
LocalRegister.regist(HelloService.class.getName(), HelloServiceImpl.class);
Protocol protocol = ProtocolFactory.getProtocol();
protocol.start(url);
}
}
HelloService
public interface HelloService {
String hello(String name);
}
HelloServiceImpl
public class HelloServiceImpl implements HelloService {
@Override
public String hello(String name) {
return "hello:" + name;
}
}
RemoteMpRegister
public class RemoteMapRegister {
private static Map<String, List<URL>> REGISTER = new HashMap<>();
public static void regist(String interfaceName, URL url){
List<URL> list = REGISTER.get(interfaceName);
if (list == null) {
list = new ArrayList<>();
}
list.add(url);
REGISTER.put(interfaceName, list);
//saveFile实现REGISTER在provider和consumer之间共享
saveFile();
}
public static List<URL> get(String interfaceName) {
REGISTER = getFile();
List<URL> list = REGISTER.get(interfaceName);
return list;
}
private static void saveFile() {
try {
FileOutputStream fileOutputStream = new FileOutputStream("classpath:register.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(REGISTER);
} catch (IOException e) {
e.printStackTrace();
}
}
private static Map<String, List<URL>> getFile() {
try {
FileInputStream fileInputStream = new FileInputStream("classpath:register.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
return (Map<String, List<URL>>) objectInputStream.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
LocalRegister
public class LocalRegister {
private static Map<String, Class> map = new HashMap<>();
public static void regist(String interfaceName, Class implClass) {
map.put(interfaceName, implClass);
}
public static Class get(String interfaceName) {
return map.get(interfaceName);
}
}
Protocol
public interface Protocol {
void start(URL url);
String send(URL url, Invocation invocation);
}
Url
@Data
@AllArgsConstructor
public class URL implements Serializable {
private String hostName;
private int port;
}
ProtocolFactory
public class ProtocolFactory {
public static Protocol getProtocol() {
//vm options:-DprotocolName=dubbo
String name = System.getProperty("protocolName");
if (name == null || name.equals("")) name = "http";
switch (name) {
case "http":
return new HttpProtocol();
case "dubbo":
return new DubboProtocol();
default:
break;
}
return new HttpProtocol();
}
}
HttpProtocol
public class HttpProtocol implements Protocol {
@Override
public void start(URL url) {
HttpServer httpServer = new HttpServer();
httpServer.start(url.getHostName(), url.getPort());//tomcat-》DispatcherServlet接收请求
}
@Override
public String send(URL url, Invocation invocation) {
HttpClient httpClient = new HttpClient();
return httpClient.send(url.getHostName(), url.getPort(),invocation);
}
}
HttpServer(手动启动tomcat)
public class HttpServer {
public void start(String hostname, Integer port) {
Tomcat tomcat = new Tomcat();
Server server = tomcat.getServer();
Service service = server.findService("Tomcat");
Connector connector = new Connector();
connector.setPort(port);
Engine engine = new StandardEngine();
engine.setDefaultHost(hostname);
Host host = new StandardHost();
host.setName(hostname);
String contextPath = "";
Context context = new StandardContext();
context.setPath(contextPath);
context.addLifecycleListener(new Tomcat.FixContextListener());
host.addChild(context);
engine.addChild(host);
service.setContainer(engine);
service.addConnector(connector);
tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet());
context.addServletMappingDecoded("/*", "dispatcher");
try {
tomcat.start();
tomcat.getServer().await();
} catch (LifecycleException e) {
e.printStackTrace();
}
}
}
HttpClient
public class HttpClient {
public String send(String hostname, Integer port, Invocation invocation) {
try {
URL url = new URL("http", hostname, port, "/");
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setRequestMethod("POST");
httpURLConnection.setDoOutput(true);
OutputStream outputStream = httpURLConnection.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(outputStream);
oos.writeObject(invocation);
oos.flush();
oos.close();
InputStream inputStream = httpURLConnection.getInputStream();
String result = IOUtils.toString(inputStream);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
DubboProtocol
public class DubboProtocol implements Protocol {
@Override
public void start(URL url) {
/* NettyServer nettyServer = new NettyServer();
nettyServer.start(url.getHostname(), url.getPort());*/
}
@Override
public String send(URL url, Invocation invocation) {
/* NettyClient nettyClient = new NettyClient();
return nettyClient.send(url.getHostname(),url.getPort(), invocation);*/
return "";
}
}
DispatcherServlet
public class DispatcherServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
new HttpServerHandler().handler(req, resp);
}
}
HttpServerHandler
public class HttpServerHandler {
public void handler(HttpServletRequest req, HttpServletResponse resp) {
try {
ObjectInputStream ois = new ObjectInputStream(req.getInputStream());
Invocation invocation = (Invocation)ois.readObject();
String interfaceName = invocation.getInterfaceName();//HelloService
Class implClass = LocalRegister.get(interfaceName);//HelloServiceImpl.hello(String name)
Method method = implClass.getMethod(invocation.getMethodName(), invocation.getParamTypes());
String result = (String) method.invoke(implClass.newInstance(), invocation.getParams());//HelloServiceImpl.hello(String name)
System.out.println("tomcat:" + result);
IOUtils.write(result.getBytes(), resp.getOutputStream());//写回给服务的消费者
} catch (Exception e) {
e.printStackTrace();
}
}
}
服务消费者
Consumer
public class Consumer {
public static void main(String[] args) {
HelloService helloService = ProxyFactory.getProxy(HelloService.class);
String name = helloService.hello("qf");
System.out.println(name);
}
}
ProxyFactory
public class ProxyFactory<T> {
@SuppressWarnings("unchecked")
public static <T> T getProxy(final Class interfaceClass) {
return (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{interfaceClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//Invocation封装的是这一次远程过程调用所需要的所有的参数
Invocation invocation = new Invocation(interfaceClass.getName(), method.getName(), method.getParameterTypes(), args);
//去注册中心获得服务地址列表
List<URL> urlList = RemoteMapRegister.get(interfaceClass.getName());
//负载均衡
URL url = LoadBalance.random(urlList);
Protocol protocol = ProtocolFactory.getProtocol();
String result = protocol.send(url, invocation);
return result;
}
});
}
}
总结
服务提供者将服务信息注册到Map中(注册中心),服务消费者从注册中心获取到服务提供者的信息,通过动态代理将请求参数封装为Invocation对象,发起Http协议(可以使用http协议,也可以使用其它协议)请求获取到数据之后返回
3.什么是dubbo
Apache Dubbo 是⼀款⾼性能、轻量级的开源服务框架。
Apache Dubbo |ˈdʌbəʊ| 提供了六⼤核⼼能⼒:⾯向接⼝代理的⾼性能RPC调⽤
,智能容错
和负载均衡
,服务⾃动注册和发现
,⾼度可扩展能⼒
,运⾏期流量调度
,可视化的服务治理 与运维
。
4.dubbo怎么实现远程通信?
服务消费者去注册中⼼订阅到服务提供者的信息。然后通过dubbo进⾏远程调⽤。
5.dubbo初体验
(注:以下代码在 dubbo官网都有: https://cn.dubbo.apache.org/zh-cn/docsv2.7/user/quick-start/)
my-dubbo-interfaces 公共接口定义
直接创建⼀个项⽬。项⽬⾥有⼀个接⼝。接⼝中定义了⼀个服务的内容。
SiteService
public interface SiteService {
String getName(String name);
}
my-dubbo-service-provider 服务提供者
pom.xml
- 注意dubbo的jar包里面,传递依赖了spring的jar依赖。
<?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>my-dubbo-demo</artifactId>
<groupId>com.qf</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>my-dubbo-service-provider</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.qf</groupId>
<artifactId>my-dubbo-interfaces</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.8.0-alpha2</version>
</dependency>
<!--dubbo-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.3</version>
</dependency>
<!--zk-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-client</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.13</version>
</dependency>
</dependencies>
</project>
SiteServiceImpl
public class SiteServiceImpl implements SiteService {
@Override
public String getName(String name) {
return "hello:"+name;
}
}
provider.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<!--定义服务的名称-->
<dubbo:application name="site-service-provider"/>
<!--指明注册中心的地址-->
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<!--指明使用的协议-->
<dubbo:protocol name="dubbo" port="20881" />
<!--指明要暴露的服务-->
<dubbo:service interface="com.qf.api.SiteService" ref="siteService" />
<!--服务具体的实现bean-->
<bean id="siteService" class="com.qf.service.impl.SiteServiceImpl" />
</beans>
provider
public class provider {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"provider.xml"});
context.start();
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
my-dubbo-service-consumer 服务消费者
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>my-dubbo-demo</artifactId>
<groupId>com.qf</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>my-dubbo-service-consumer</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.qf</groupId>
<artifactId>my-dubbo-interfaces</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.8.0-alpha2</version>
</dependency>
<!--dubbo-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.3</version>
</dependency>
<!--zk-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-client</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.13</version>
</dependency>
</dependencies>
</project>
consumer.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<!--定义应用名称-->
<dubbo:application name="site-service-consumer"/>
<!--定义注册中心的地址-->
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<!--生成一个当前SiteService的代理对象:当前这个SiteService的代理对象会注入到ioc容器中,要使用的话可以从ioc容器中拿出来-->
<dubbo:reference id="siteService" interface="com.qf.api.SiteService"/>
</beans>
Consumer
public class Consumer {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"consumer.xml"});
context.start();
//从ioc容器中获得SiteService服务提供者的代理对象
SiteService siteService = (SiteService) context.getBean("siteService");
String qf = siteService.getName("qf");
System.out.println(qf);
}
}
启动zookeeper
例如,在windows上,在zk所在的bin目录下打开cmd窗口,输出zkServer.cmd
,按enter 即可启动
测试
当启动服务提供者后,打开zkCli.cmd,可以查看到服务提供这的信息(这里创建的实际上是1个临时节点,当提供者关闭时,该节点为空)
运行Consumer,得到服务提供者返回的信息
服务代理的过程
6.dubbo内部结构
- dubbo提供了⼀个容器⽤来存放服务提供者(初始化)
- 服务提供者将服务名、及具体的服务地址、端⼝等信息注册到注册中⼼上(初始化) 服务消费者订阅需要的服务(初始化)
- 注册中⼼异步通知服务的变更情况
- 服务消费者同步的调⽤到服务提供者的服务
- 监控中⼼实时监控和治理当前的服务
注意:
- 同步:好⽐打电话,双⽅必须在线,才能完成
- 异步:好⽐发微信语⾳,上游发完就结束了,不需要等待对⽅执⾏完。
四、Springboot中使⽤dubbo
springboot中使⽤dubbo也是⼀样,需要建⽴接⼝层、服务提供者、服务消费者。
创建接口层
SiteService
public interface SiteService {
String getName(String name);
}
创建服务提供者
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.qf</groupId>
<artifactId>my-boot-service-provider</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>my-boot-service-provider</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>com.qf</groupId>
<artifactId>my-boot-qf-interfaces</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Dubbo Spring Boot Starter -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
<version>2.7.3</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>com.qf.my.boot.service.provider.MyBootServiceProviderApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
application.yml
server:
port: 9001
dubbo:
application:
name: site-service-boot-provider
registry:
address: zookeeper://127.0.0.1:2181
protocol:
name: dubbo
port: 20882
SiteServiceImpl
在服务提供者的实现类上打上注解来⾃于dubbo的@Service
import com.qf.boot.api.SiteService;
import org.apache.dubbo.config.annotation.Service;
@Service
public class SiteServiceImpl implements SiteService {
@Override
public String getName(String name) {
return "hello boot:" + name;
}
}
MyBootServiceProviderApplication
在启动类上打上注解@EnableDubbo
@EnableDubbo
@SpringBootApplication
public class MyBootServiceProviderApplication {
public static void main(String[] args) {
SpringApplication.run(MyBootServiceProviderApplication.class, args);
}
}
创建服务消费者
引⼊依赖(与提供者相同)
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.qf</groupId>
<artifactId>my-boot-service-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>my-boot-service-consumer</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>com.qf</groupId>
<artifactId>my-boot-qf-interfaces</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Dubbo Spring Boot Starter -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
<version>2.7.3</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>com.qf.my.boot.service.consumer.MyBootServiceConsumerApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
application.yml
server:
port: 8001
dubbo:
application:
name: site-service-boot-consumer
registry:
address: zookeeper://127.0.0.1:2181
SiteController
使⽤@Reference注解订阅服务,注意这个注解来⾃于dubbo
@RestController
@RequestMapping("/site")
public class SiteController {
// <dubbo:reference : 两件事情:
/*
1.服务启动时向注册中心订阅SiteService服务地址列表
2.生成SiteService服务的代理对象(注意:是会生成1个代理对象,而非单独的注入!!!)
*/
@Reference
private SiteService siteService;
@GetMapping("/name")
public String getName(String name){
return siteService.getName(name);
}
}
MyBootServiceConsumerApplication
@EnableDubbo
@SpringBootApplication
public class MyBootServiceConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(MyBootServiceConsumerApplication.class, args);
}
}
启动服务,访问接⼝
服务启动源码的剖析
以@EnableDubbo注解为入口分析
五、dubbo⽤法示例
1.version版本号
版本号⽤处是对于同⼀个接⼝,具有不同的服务实现。
服务提供者1
@Service(version = "default")
public class DefaultSiteServiceImpl implements SiteService {
@Override
public String siteName(String name) {
return "default:"+name;
}
}
服务提供者2
@Service(version = "async")
public class AsyncSiteServiceImpl implements SiteService {
@Override
public String siteName(String name) {
return "async:" + name;
}
}
服务消费者
@Controller
public class VersionDubboConsumer {
@Reference(version = "async" /* "default" */)
private SiteService siteService;
@GetMapping("/name")
public String getName(String name){
return siteService.getName(name);
}
}
(此处遇到导入dubbo-qf-demo中无法正确识别my-dubbo-interfaces这个模块,解决的办法是:File -> New -> Module frome Existing Sources…,建1个模块)
2.指定protocol协议
dubbo框架可以对协议进⾏扩展,⽐如使⽤:
- rest
- http
- dubbo
- ⾃定义协议
在配置⽂件中配置协议:
# 应⽤名称
spring.application.name=my-dubbo-provider
# 应⽤服务 WEB 访问端⼝
server.port=8080
# Base packages to scan Dubbo Component: @org.apache.dubbo.config.annotation.Service==>@EnableDubbo
dubbo.scan.base-packages=com.qf.my.dubbo.provider
dubbo.application.name=${spring.application.name}
## Dubbo Registry
dubbo.registry.address=zookeeper://172.16.253.55:2181
# Dubbo Protocol
#dubbo.protocol.name=dubbo
#dubbo.protocol.name=dubbo
# @Path
#dubbo.protocol.name=rest
#dubbo.protocol.name=rest
# 特别注意:如果使用rest协议,那么服务提供者必须使用@Path注解,否则启动时,就会报错
dubbo.protocols.protocol1.id=rest
dubbo.protocols.protocol1.name=rest
dubbo.protocols.protocol1.port=8090
dubbo.protocols.protocol1.host=0.0.0.0
dubbo.protocols.protocol2.id=dubbo1
dubbo.protocols.protocol2.name=dubbo
dubbo.protocols.protocol2.port=20882
dubbo.protocols.protocol2.host=0.0.0.0
dubbo.protocols.protocol3.id=dubbo2
dubbo.protocols.protocol3.name=dubbo
dubbo.protocols.protocol3.port=20883
dubbo.protocols.protocol3.host=0.0.0.0
在暴露服务时指明要使⽤的协议:
(注意:如果配置文件中(如上所示)配置了多个协议,但是下方的@Service注解,又未指定具体的协议,那么会针对每个协议都起1个服务)
// protocol2 表示使用protocol2所指定的协议提供服务,言外之意就是消费方要使用protocol2所指定的协议来调用该服务
@Service(version = "default",protocol = "protocol2")
public class DefaultSiteServiceImpl implements SiteService {
@Override
public String siteName(String name) {
return "default:"+name;
}
}
3.使⽤rest访问dubbo的服务
服务提供者
pom.xml
对比《springboot中使用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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.qf</groupId>
<artifactId>my-boot-service-provider</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>my-boot-service-provider</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>com.qf</groupId>
<artifactId>my-boot-qf-interfaces</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Dubbo Spring Boot Starter -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
<version>2.7.3</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-http</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-metadata-report-zookeeper</artifactId>
<version>2.7.3</version>
</dependency>
<!-- Dubbo rest protocol -->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<version>3.0.19.Final</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>com.qf.my.boot.service.provider.MyBootServiceProviderApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
application.yml
指定1个protocol1的协议,它使用的是rest协议
dubbo:
application:
name: site-service-boot-provider
registry:
address: zookeeper://127.0.0.1:2181
protocol:
name: dubbo
port: 20881
protocols:
protocol1:
id: rest
name: rest
port: 8090
host: 0.0.0.0
protocol2:
id: dubbo1
name: dubbo
port: 20882
host: 0.0.0.0
protocol3:
id: dubbo2
name: dubbo
port: 20883
host: 0.0.0.0
SiteServiceImpl
import com.qf.boot.api.SiteService;
import org.apache.dubbo.config.annotation.Service;
import org.apache.dubbo.rpc.protocol.rest.support.ContentType;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
// 1、指定了protocol1所指定的协议,由于是rest协议,必须要使用@Path注解,否则启动时会报错
// 2、如果此处未指定具体的协议,那么也必须使用@Path注解,因为配置文件中只要配置了rest协议,所以也会起1个对应的rest服务
@Service(protocol = {"protocol1", "protocol3"})
@Path("site")
public class SiteServiceImpl implements SiteService {
@Override
@GET
@Path("name")
@Produces({ContentType.APPLICATION_JSON_UTF_8, ContentType.TEXT_PLAIN_UTF_8})
public String getName(@QueryParam("name") String name) {
return "hello boot:" + name;
}
}
服务消费方
仍然使用《springboot中使用dubbo》的配置和代码
@RestController
@RequestMapping("/site")
public class SiteController {
//<dubbo:reference : 两件事情:1.服务启动时向注册中心订阅SiteService服务地址列表 2.生成SiteService服务的代理对象
@Reference
private SiteService siteService;
@GetMapping("/name")
public String getName(String name){
return siteService.getName(name);
}
}
测试
4.消费者通过url直连指定的服务提供者
(代码使用《springboot中使用dubbo》中的,但作如下修改)
-
服务提供者的配置⽂件中声明三个dubbo协议
server: port: 9001 dubbo: application: name: site-service-boot-provider registry: address: zookeeper://127.0.0.1:2181 protocols: protocol1: id: dubbo1 name: dubbo port: 20881 host: 0.0.0.0 protocol2: id: dubbo2 name: dubbo port: 20882 host: 0.0.0.0 protocol3: id: dubbo3 name: dubbo port: 20883 host: 0.0.0.0
-
服务提供者暴露服务,
未指定协议,则会暴露三个服务,每个协议对应⼀个服务!
@Service(version= "default") // 版本为default public class DefaultSiteServiceImpl implements SiteService { @Override public String siteName(String name) { URL url = RpcContext.getContext().getUrl(); // 方便确认是哪个协议对应的服务返回的 return String.format("%s:%s, Hello, %s", url.getProtocol(), url.getPort(), name); } }
-
服务消费者端通过url指定某⼀个服务
(当服务消费者切换不同的url时,会返回对应协议的服务的结果。这说明服务提供方不指定协议,则会每个协议都会启动1个服务)@RestController @RequestMapping("/site") public class SiteController { @Reference( id = "siteService", version = "default", // 协议:ip:端口/服务接口全限定名:版本号 // url = "dubbo://192.168.134.5:20881/com.qf.boot.api.SiteService:default" url = "dubbo://192.168.134.5:20882/com.qf.boot.api.SiteService:default" // url = "dubbo://192.168.134.5:20883/com.qf.boot.api.SiteService:default" ) private SiteService siteService; @GetMapping("/name") public String getName(String name){ return siteService.getName(name); } }
5.服务超时
服务提供者和服务消费者都可以配置服务超时时间(默认时间为1秒
):
-
服务提供者的超时时间:执⾏该服务的超时时间。如果超时,则会打印超时⽇志 (warn),但服务会正常执⾏完。
@Service(version = "timeout", timeout = 4000) public class TimeoutSiteServiceImpl implements SiteService { @Override public String siteName(String name) { try { //如果服务执行时间超过了指定的超时时间则会打印warn日志 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("serving..."); return "timeout site service:"+name; } }
-
服务消费者的超时时间:从发起服务调⽤到收到服务响应的整个过程的时间。如果超时, 则进⾏重试,重试失败抛异常
@RestController @RequestMapping("/site") public class SiteController { @Reference(version = "timeout", timeout = 3000) private SiteService siteService; @GetMapping("/name") public String getName(String name){ return siteService.getName(name); } }
(服务者配置超时时间,对所有的消费者有效,消费者配置超时时间只对自己有效,二者同时存在时,按消费者自己的计算)
6.集群容错
在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。
-
failover:(默认,推荐)
- 当出现失败时,会进⾏重试其它服务器,默认重试2次,⼀共三次调⽤。但是会出现幂等性问题。 虽然会出现幂等性问题,但是依然推荐使⽤这种容错机制,在业务层⾯解决幂等性问题:
- ⽅案⼀:把数据的业务id作为数据库的联合主键,此时业务id不能重复。
- ⽅案⼆(推荐):使⽤分布式锁来解决重复消费问题
- 当出现失败时,会进⾏重试其它服务器,默认重试2次,⼀共三次调⽤。但是会出现幂等性问题。 虽然会出现幂等性问题,但是依然推荐使⽤这种容错机制,在业务层⾯解决幂等性问题:
-
failfast:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
-
failsafe:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
-
failback:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
-
forking:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=“2” 来设置最大并行数。
-
broadcast:广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
结论:如果使⽤dubbo,不推荐把重试关掉,⽽是在⾮幂等性操作的场景下,服务提供者⽅ 要做幂等性的解决⽅案(保证)。
7.服务降级
可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略。
服务消费者通过Mock指定服务超时后执⾏的策略:
@RestController
@RequestMapping("/site")
public class MockDubboConsumer {
// 在调用失败后(包括超时失败)
@Reference(version = "timeout", timeout = 1000, mock = "fail:return halo")
private SiteService siteService;
@GetMapping("/name")
public String getName(String name){
return siteService.getName(name);
}
}
-
mock=force:return null
表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。 -
还可以改为
mock=fail:return null
表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。
服务提供方代码示例
@Service(version = "timeout", timeout = 6000)
public class SiteServiceImpl implements SiteService {
@Override
public String getName(@QueryParam("name") String name) {
System.out.println("收到请求");
try {
TimeUnit.SECONDS.sleep(4000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "timeout: " + name;
}
}
8.本地存根
远程服务后,客户端通常只剩下接⼝,⽽实现全在服务器端,但提供⽅有些时候想在客户端 也执⾏部分逻辑,⽐如:做 ThreadLocal 缓存,提前验证参数,调⽤失败后伪造容错数据等 等,此时就需要在 API 中带上 Stub,客户端⽣成 Proxy 实例,会把 Proxy 通过构造函数传给 Stub 1,然后把 Stub 暴露给⽤户,Stub 可以决定要不要去调 Proxy。
(这个比上面的服务降级更容易控制)
1、在服务消费者的包里,定义SiteServiceStub
@Slf4j
public class SiteServiceStub implements SiteService {
private SiteService siteService;
public SiteServiceStub(SiteService siteService) {
this.siteService = siteService;
}
@Override
public String getName(String name) {
// 参数校验
if (StringUtils.isEmpty(name)) {
return "";
}
try {
return siteService.getName(name);
} catch (Exception e) {
// 异常处理
log.info("getName发生错误, e:{}", e);
return " stub handle error " + name;
}
}
}
2、服务消费者调⽤服务,开启本地存根
@Slf4j
@RestController
@RequestMapping("/site")
public class SiteController {
// 默认是1s超时,而服务提供者需要4s才能返回,因此重试2次,一共3次后,由本地存根SiteServiceSub对异常进行处理
// 注意,此处不要使用mock,否则效果演示不出来,因为异常被mock处理了
// stub属性也可以写true,表示在SiteService所在的包下直接找SiteServiceStub实现类
@Reference(version = "timeout", /* mock = "fail:return errorr~~",*/ stub = "com.qf.my.boot.service.consumer.controller.SiteServiceStub")
private SiteService siteService;
@GetMapping("/name")
public String getName(String name){
log.info("getName");
return siteService.getName(name);
}
}
3、服务提供者SiteServiceImpl
@Service(version = "timeout", timeout = 6000)
public class SiteServiceImpl implements SiteService {
@Override
public String getName(@QueryParam("name") String name) {
System.out.println("收到请求");
try {
TimeUnit.SECONDS.sleep(4000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "timeout: " + name;
}
}
4、测试:访问http://localhost:8001/site/name?name=zzhua666
,由于客户端默认1s就要结果,而服务端处理需要4s,所以客户端会重试2次,一共3次,最后1次失败后,在SiteServiceStub本地存根(包装了对代理对象的远程调用)的异常处理处被处理
9.参数回调
参数回调方式与调用本地 callback 或 listener 相同,只需要在 Spring 的配置文件中声明哪个参数是 callback 类型即可。Dubbo 将基于长连接生成反向代理,这样就可以从服务器端调用客户端逻辑。
简⽽⾔之,就是服务端可以调⽤客户端的逻辑
。
配置继续使用《Springboot中使⽤dubbo》
公共接口
CallbackService
public interface CallbackService {
String sayHello(String name, String key, CallbackListener listener);
}
CallbackListener
public interface CallbackListener {
void changed(String msg);
}
CallbackListenerImpl
public class CallbackListenerImpl implements CallbackListener, Serializable {
@Override
public void changed(String msg) {
System.out.println("callback1:" + msg);
}
}
服务提供者
CallbackServiceImpl
@Service(
version = "callback_v1",
callbacks = 3,
connections = 1,
methods = {
@Method(
name = "sayHello",
arguments = {@Argument(index = 2, callback = true)}
)
}
)
public class CallbackServiceImpl implements CallbackService {
public String sayHello(String name, String key, CallbackListener listener) {
listener.changed(getChanged(key)); // 发送变更通知
return "Hello: " + name;
}
private String getChanged(String key) {
return "Changed: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
}
服务消费者
CallbackController
@RestController
@RequestMapping("callback")
public class CallbackController {
@Reference(version = "callback_v1")
private CallbackService callbackService;
@RequestMapping("callback1")
public String test01(String name) {
callbackService.sayHello(name, "foo.bar", new CallbackListenerImpl());
return "ok";
}
}
(就是消费者序列化了1个对象过去,然后服务提供者反序列化出这个对象,然后调用这个对象的方法。那个@Method注解即时不加,也不会报错。所以这只是服务消费者将参数传过去给服务提供者调用的概念)
10.异步调用
从 2.7.0 开始,Dubbo 的所有异步编程接口开始以 CompletableFuture 为基础
基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小。
简⽽⾔之,消费者通过异步调⽤,不⽤等待服务提供者返回结果就⽴即完成任务,待有结果 后再执⾏之前设定好的监听逻辑。(类似netty的promise)
基础代码使用《Springboot中使⽤dubbo》
公共接口
AsyncService
import java.util.concurrent.CompletableFuture;
public interface AsyncService {
String sayHello(String name);
CompletableFuture<String> doAsync(String name);
}
服务提供者
AsyncServiceImpl
@Slf4j
@Service(version = "async")
public class AsyncServiceImpl implements AsyncService {
@Override
public String sayHello(String name) {
return "hello: " + name;
}
@Override
public CompletableFuture<String> doAsync(String name) {
log.info("doAsync start");
try {
TimeUnit.MILLISECONDS.sleep(name.length() * 500L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("doAsync end");
return CompletableFuture.supplyAsync(() -> {
return "async: " + name;
});
}
}
服务消费者
AsyncController
@Slf4j
@RestController
public class AsyncController {
// 默认1s超时,这里改为2s
@Reference(version = "async", timeout = 2000)
private AsyncService asyncService;
@RequestMapping("/async01")
public String async01(String name) {
return asyncService.sayHello(name);
}
@RequestMapping("/async02")
public String async02(String name) {
log.info("async02 start");
CompletableFuture<String> future = asyncService.doAsync(name);
future.whenComplete((result, throwable) -> {
if (throwable == null) {
log.info("异步处理完成..." + result);
} else {
log.error("异步处理出错..., e: {}", throwable);
}
});
log.info("async02 end");
return "ok";
}
}
(可以看到消费者调用提供者的方法后,并不会阻塞当前处理请求的线程,当服务提供者返回了数据之后,执行异步掉哦那个)
六、dubbo的负载均衡策略
在上⼀章节中,已经涉及到dubbo的负载均衡概念:⼀个服务接⼝具有三个服务提供者。
1.负载均衡策略
dubbo的负载均衡是发⽣在服务提供者端,负载均衡策略⼀共有以下四种:
- 随机(默认的策略):random
- 轮询: roundrobin
- 最⼩活跃调⽤数:leastactive
- ⼀致性hash: consistenthash
2.dubbo中如何配置负载均衡策略
-
服务提供者
@Service(version = "default",loadbalance = "roundrobin") public class DefaultSiteServiceImpl implements SiteService { @Override public String siteName(String name) { return "default:"+name; } }
-
服务消费者:如果两边都配置了负载均衡策略,则以消费者端为准。
@EnableAutoConfiguration public class DefaultDubboConsumer { @Reference(version = "default", loadbalance = "roundrobin") private SiteService siteService; public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(DefaultDubboConsumer.class); SiteService siteService = (SiteService) context.getBean(SiteService.class); String name = siteService.siteName("q-face"); System.out.println(name); } }
测试
使用《Springboot中使⽤dubbo》的配置,仅作如下修改
服务提供者
如果@Service不指定协议,那么针对每个协议都会生成对应的服务
@Service
public class SiteServiceImpl implements SiteService {
@Override
public String siteName(String name) {
URL url = RpcContext.getContext().getUrl();
// 方便确认是哪个协议对应的服务返回的
return String.format("%s:%s, Hello, %s", url.getProtocol(), url.getPort(), name);
}
}
dubbo:
application:
name: dubbo-provider
registry:
address: zookeeper://127.0.0.1:2181
protocol:
name: dubbo
port: 20881
protocols:
protocol1:
id: dubbo2
name: dubbo
port: 20882
protocol2:
id: dubbo3
name: dubbo
port: 20883
@SpringBootApplication
@EnableDubbo
public class Provider {
public static void main(String[] args) {
SpringApplication.run(Provider.class, args);
}
}
服务消费者
@RestController
@RequestMapping("site")
public class SiteController {
@Reference(loadbalance = "roundrobin")
private SiteService siteService;
@RequestMapping("name")
public String siteName(String name) {
return siteService.siteName(name);
}
}
server:
port: 8001
dubbo:
application:
name: dubbo-consumer
consumer:
registry: zookeeper://127.0.0.1:2181
@SpringBootApplication
@EnableDubbo
public class Consumer {
public static void main(String[] args) {
SpringApplication.run(Consumer.class, args);
}
}
测试效果
3.⼀致性hash的实现
-
服务端的实现
@Service(version = "default",loadbalance = "roundrobin") public class DefaultSiteServiceImpl implements SiteService { @Override public String siteName(String name) { URL url = RpcContext.getContext().getUrl(); return String.format("%s:%s, Hello, %s", url.getProtocol(), url.getPort(), name); } }
-
消费端的实现
@EnableAutoConfiguration public class LoadBalanceDubboConsumer { @Reference(version = "default", loadbalance = "consistenthash") private SiteService siteService; public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(DefaultDubboConsumer.class); SiteService siteService = (SiteService) context.getBean(SiteService.class); for (int i = 0; i < 100; i++) { String name = siteService.siteName("q-face" + i % 6); System.out.println(name); } } }
4.最少活跃调⽤数的实现
最少活跃调⽤数:相同活跃数的随机,活跃数指调⽤前后计数差。使慢的提供者收到更少请 求,因为越慢的提供者的调⽤前后计数差会越⼤。
在服务消费者端记录当前服务器⽬前被调⽤的数量(消费者⾃⼰维护着这个数据)。具体的 执⾏过程如下:
-
消费者在本地缓存所有服务提供者
-
消费者在调⽤某⼀个服务时,会选择本地的所有服务提供者中,属性active值最⼩的那个 服务提供者。
-
选定该服务提供者后,并对其active属性+1
-
开始调⽤该服务
-
完成调⽤后,对该服务提供者的active属性-1
整个过程,如果active的值越⼤,说明该服务提供者的响应性能越差,因此越少调⽤。
七、安装Dubbo admin监管平台
docker构建
1.使⽤docker安装
docker run -d \
-p 8080:8080 \
-e dubbo.registry.address=zookeeper://172.16.253.55:2181 \
-e dubbo.admin.root.password=root \
-e dubbo.admin.guest.password=guest \
chenchuxin/dubbo-admin
2.访问
http://127.0.0.1:8080
源码构建
可参考:https://cn.dubbo.apache.org/zh-cn/docsv2.7/admin/ops/introduction/
目前的管理控制台已经发布 0.1 版本,结构上采取了前后端分离的方式,前端使用 Vue 和 Vuetify 分别作为 Javascript 框架和UI框架,后端采用 Spring Boot 框架。既可以按照标准的 Maven 方式进行打包,部署,也可以采用前后端分离的部署方式,方便开发,功能上,目前具备了服务查询,服务治理(包括 Dubbo 2.7 中新增的治理规则)以及服务测试三部分内容。
Maven方式部署
安装
git clone https://github.com/apache/dubbo-admin.git
cd dubbo-admin
mvn clean package
cd dubbo-admin-distribution/target
java -jar dubbo-admin-0.1.jar
访问
http://localhost:8080
前后端分离部署
前端
cd dubbo-admin-ui
npm install
npm run dev
后端
cd dubbo-admin-server
mvn clean package
cd target
java -jar dubbo-admin-server-0.1.jar
访问
http://localhost:8081
配置文件为:dubbo-admin-server/src/main/resources/application.properties
admin.config-center=zookeeper://127.0.0.1:2181
admin.registry.address=zookeeper://127.0.0.1:2181
admin.metadata-report.address=zookeeper://127.0.0.1:2181
测试
1、下载源码:https://github.com/apache/dubbo-admin.git,导入idea
2、在dubbo-admin-server的pom.xml中把dubbo-admin-ui给注释掉(跳过测试:-DskipTests=true)
3、dubbo-admin-ui使用vscode打开,npm install安装依赖,注意,这里nvm use v16.17.0切换node版本为:v16.17.0,其它版本可能会报错。npm run serve启动
点击详情,查看到元数据可能无法查看
1、首先,dubbo-admin-server的application.properties中配置dubbo.metadata-report.address=${admin.registry.address}
2、生产者添加配置dubbo.metadata-report.address=${dubbo.registry.address}
3、正常显示
八、Dubbo的SPI可扩展机制
1.Java的SPI(Service Provider Interface)机制
Java中提供了DriverManager、Connection、Statement接⼝,来约定了JDBC规范。
但针对 于MySQL或Oracle数据库来说,需要指明具体的驱动包,⽐如MySQL:mysql-connector-java-5.7.25.jar 包中的META-INF/services下的java.sql.Driver⽂件
,⽂件 中指明了具体的驱动类
com.mysql.cj.jdbc.Driver
这样Java会读取该jar包下的该⽂件,那java怎么找到该⽂件呢?因为java程序需要该类: java.sql.Driver,所以是到找⽂件名是java.sql.Driver的⽂件。——相当于是加载了Driver接⼝的 具体的实现类
测试java的SPI
Cat接口
public interface Cat {
String getName();
}
BlackCat
public class BlackCat implements Cat{
@Override
public String getName() {
return "black";
}
}
WhiteCat
public class WhiteCat implements Cat{
@Override
public String getName() {
return "white";
}
}
com.qf.cat
1个文件可配置多个实现类
com.qf.BlackCat
com.qf.WhiteCat
TestSPI
public class TestSPI {
public static void main(String[] args) {
ServiceLoader<Cat> cats = ServiceLoader.load(Cat.class);
for (Cat cat : cats) {
System.out.println(cat.getName());
}
}
}
2.SPI机制的缺点
⽂件中的所有类都会被加载且被实例化。没有办法指定某⼀个类来加载和实⼒化。此时 dubbo的SPI可以解决
3.dubbo的SPI机制
dubbo⾃⼰实现了⼀套SPI机制来解决Java的SPI机制存在的问题。
dubbo源码中有很多的项⽬,每个项⽬被打成⼀个jar包。⽐如代码中通过@Service注解的属 性protocol="c1"找到application.properties的c1协议是rest,那么就会去rest项⽬中找该项 ⽬中的META-INF中对应的⽂件,再找到指定的类来加载。
rest=org.apache.dubbo.rpc.protocol.rest.RestProtocol
通过这种机制,在项⽬中新增⼀个协议也⾮常⽅便:
- 项⽬中新增协议jar
- 在application.properties中加⼊协议名称
- 在新增的项⽬的META-INF/services⽂件中加⼊配置
dubbo的spi机制简单实现
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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.qf</groupId>
<artifactId>dubbo-spi</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-common</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-http</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-dubbo</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-api</artifactId>
<version>2.7.3</version>
</dependency>
</dependencies>
</project>
Cat接口
public interface Cat {
String getName();
}
BlackCat
public class BlackCat implements Cat{
@Override
public String getName() {
return "black";
}
}
WhiteCat
public class WhiteCat implements Cat{
@Override
public String getName() {
return "white";
}
}
CatWrapper
public class CatWrapper implements Cat {
private Cat cat;
public CatWrapper(Cat cat) {
this.cat = cat;
}
@Override
public String getName() {
System.out.println("cat wrapper");
return cat.getName();
}
}
com.qf.cat
文件在resources/META-INFO/dubbo文件夹下,1个文件可配置多个实现类
black=com.qf.BlackCat
white=com.qf.WhiteCat
com.qf.CatWrapper
TestSPI
public class TestSPI {
public static void main(String[] args) {
//获得扩展点加载器
ExtensionLoader<Cat> extensionLoader = ExtensionLoader.getExtensionLoader(Cat.class);
Cat cat = extensionLoader.getExtension("white");
System.out.println(cat.getName());
}
}
TestSPI2
public class TestSPI2 {
public static void main(String[] args) {
//拿到的是ProtocolFilterWrapper包装类,实际被包装的是HttpProtocol
ExtensionLoader<Protocol> extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol protocol = extensionLoader.getExtension("http");
System.out.println(protocol);
}
}
九、Dubbo源码剖析
1. Dubbo服务调⽤过程
服务提供者的集群集群⾥⾯有⼏个服务提供者,就有⼏个invoker,invoker理解成调⽤⼀个服 务提供者需要的完整的细节,封装成的对象
那集群是怎么知道有⼏个服务提供者——从注册中⼼获得,注册中⼼获得的数据封装在 RegistryDirectory对象中。那么RegistryDirectory怎么得到注册中⼼中的url地址呢?必须有 ⼀个zk客户端:ZookeeperRegistry
RegistryDirectory⾥包含了ZookeeperRegistry,RegistryDirectory维护了所有的invoker调 ⽤器,调⽤器通过RegsitryDirectory(ZookeeperRegistry)的⽅法获得的。
AbstractClusterInvoker⾥包含了RegistryDirectory,换句话说,RegistryDirectory被 AbstractClusterInvoker所使⽤。真正执⾏的是AbstractClusterInvoker中的invoker⽅法,负 载均衡也在⾥⾯。
proxy是由JavassistProxyFactory⽣成的,拼装代码来⽣成的。代理对象通过 JavassistProxyFactory中的InvokerInvocationHandler ⽣成⼀个代理对象,来发起对集群的 调⽤。
InvokerInvocationHandler⾥封装了RpcInvocation,RpcInvocation⾥封装的是这⼀次请求 所需要的所有参数。
这个invoker如果⽤的是dubbo协议,那么就是DubboInvoker(还有http RMI等协议) 源码中的invoker.invoke()中的invoker,如果是dubbo协议,那么就是DubboInvoker。
2.关于DubboInvoker的装饰
AsyncToSyncInvoker
异步转同步:dubbo 2.7.3 引⼊了InvokeMode(1.SYNC同步, 2.ASYNC异步, 3.FUTURE调⽤ future.get()时会造成线程阻塞)
在消费者端进⾏调⽤时先判断是否是同步调⽤,如果是同步的话,通过asyncResult.get()获得 结果。
如果是异步的话,直接返回Result对象(CompetableFuture)。
ProtocolFilterWrapper
Dubbo内容提供了⼤量内部实现,⽤来实现调⽤过程额外功能, 如向监控中⼼发送调⽤数 据, Tps限流等等, 每个filer专注⼀块功能。⽤户同样可以通过Dubbo的SPI扩展机制现在⾃ ⼰的功能。
ProtocolFilterWrapper:在服务的暴露与引⽤的过程中构建调⽤过滤器链。
ListenerInvokerWrapper
dubbo在服务暴露(exporter)以及销毁暴露(unexporter)服务的过程中提供了回调窗⼝,供⽤ 户做业务处理。
ListenerInvokerWrapper装饰exporter, 在构造器中遍历listeners,构建export的监听链。
3. 权重轮询算法
假如⽬前有三台服务器A、B、C,它们的权重分别是6、2、2,那也就意味着在10次调⽤中,轮询的结果应该为:AAAAAABBCC
但如果把B和C穿插在A中,轮询的结果会更加的平滑,⽐如:ABACAABACA
此时可以通过如下设计来实现:
- 每台服务器确定两个权重变量:weight、currentWeight
- weight固定不变:初始化指定了值
- currentWeight每次调整,初始化为0:currentWeight = currentWeight+weight
- 从集群中选择currentWeight最⼤的服务器作为选择结果。并将该最⼤服务器的 currentWeight减去各服务器的weight总数
- 调整currentWeight = currentWeight+weight,开始新⼀轮的选择
- 以此往复,经过10次⽐较厚currentWeight都为0