Dubbo-高级特性篇
一、序列化
Java对象 -> 序列化 -> 流数据
流数据 -> 反序列化 -> Java对象
依赖一个公共的相同模块即保证序列化版本号在序列和反序列时相同。
- dubbo内部已经将序列化和反序列化的过程内部封装了
- 我们只需要在定义pojo类时实现serializable接口即可
- 一般会定义一个公共的pojo模块,让生产者和消费者都依赖该模块。内部已经将序列化和反序列化的过程内部封装了我们只需要在定义类时实现可序列化的接口即可一般会定义一个公共的POJO模块,让生产者和消费者都依赖该模块。
1)创建一个公共实体类依赖
public class User { private int id; private String username; private String password; public User() { } public User(int id, String username, String password) { this.id = id; this.username = username; this.password = password; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
2)在dubbo-interface里去添加一个查询用户的接口,首先需要引入dubbo-pojo的依赖。
<?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.itheima</groupId> <artifactId>dubbo-interface</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>com.itheima</groupId> <artifactId>dubbo-pojo</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
import com.itheima.pojo.User; public interface UserService { String sayHello(); /** * 查询用户 */ User findUserById(int id); }
3)在dubbo-service中实现findUserById方法
import com.itheima.pojo.User; import com.itheima.service.UserService; import org.apache.dubbo.config.annotation.Service; // 将这个类提供的方法(服务)对外发布。将访问地址(ip、端口、访问路径)注册到ZK注册中心 @Service public class UserServiceImpl implements UserService { public String sayHello() { return "Hello Dubbo RPC Zookeeper!~"; } @Override public User findUserById(int id) { User user = new User(1, "zhangsan", "123"); return user; } }
4)在dubbo-web中实现调用服务消费
import com.itheima.pojo.User; import com.itheima.service.UserService; import org.apache.dubbo.config.annotation.Reference; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { @Reference private UserService userService; @RequestMapping("/sayHello") public String sayHello() { return userService.sayHello(); } /** * 根据id查询用户信息 * @param id * @return */ @RequestMapping("/find") public User find(int id) { return userService.findUserById(id); } }
此时interface模块依赖pojo模块,web模块依赖service模块、web和service模块依赖interface模块。interface和pojo上都有修改,都需要进行mvn clean install。
重启dubbo-service和dubbo-web服务,访问http://localhost:8000/user/find.do?id=1
这个原因写的很明确,就是pojo实体类上需要实现Serializable接口,因为Java对象的数据需要在不同的机器上通过流来传输的。
5)所有的实体类都实现Serializable接口
import java.io.Serializable; /* 注意!!! 将来所有的pojo类都需要实现serializable接口 */ public class User implements Serializable { private int id; private String username; private String password; public User() { } public User(int id, String username, String password) { this.id = id; this.username = username; this.password = password; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
注意:被依赖的Java工程改动后,一定要install重新安装。
6)对修改的服务进行clean install重新安装,重启相关依赖服务,重新访问
二、地址缓存
面试题:注册中心挂了,服务是否能正常访问?
- 可以,因为dubbo服务消费者在第一次调用时,会从注册中心中获取服务提供者的地址,然后进行访问,之后会将服务提供方地址缓存到本地,以后在调用则不会访问注册中心,直接从本地缓存中去获取,不用再跟注册中心去交互。
- 当服务提供者地址发生变化时,注册中心会通知服务消费者需要缓存更新。
我们可以进行测试一下,将Linux服务器上的ZK关掉。
再次访问老的服务地址,是可以访问的。但是如果注册中心挂了,新的服务就无法注册了,还是需要进行修复的。
三、超时与重试机制
1. 超时机制
- 服务消费者在调用服务提供者的时候发生了阻塞、等待的情形,这个时候,服务消费者会一直等待下去。
- 在某个峰值时刻,大量的请求都在同时请求服务消费者,会造成线程的大量堆积,当服务器资源耗尽,势必会造成雪崩。
- dubbo利用超时机制来解决这个问题,设置一个超时时间,在这个时间段内,无法完成服务访问,则自动断开连接,将线程释放,这样就不会造成积压导致服务雪崩。
- 使用timeout属性配置超时时间,默认值1000,单位毫秒。
我们来模拟一下超时情况。在@Service
中配置timeout
超时时间)、retries
超时后重试的次数(默认2次)。
import com.itheima.pojo.User; import com.itheima.service.UserService; import org.apache.dubbo.config.annotation.Service; // 将这个类提供的方法(服务)对外发布。将访问地址(ip、端口、访问路径)注册到ZK注册中心 @Service(timeout = 3000, retries = 0) // 当前服务3秒超时,超时后,重试0次 public class UserServiceImpl implements UserService { public String sayHello() { return "Hello Dubbo RPC Zookeeper!~"; } @Override public User findUserById(int id) { User user = new User(1, "zhangsan", "123"); // 模拟数据库查询耗时5秒 try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } return user; } }
在Controller服务调用方模拟线程异步调用1秒打印一个数字。
import com.itheima.pojo.User; import com.itheima.service.UserService; import org.apache.dubbo.config.annotation.Reference; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { @Reference private UserService userService; @RequestMapping("/sayHello") public String sayHello() { return userService.sayHello(); } /** * 根据id查询用户信息 * @param id * @return */ int i = 1; @RequestMapping("/find") public User find(int id) { new Thread(new Runnable() { @Override public void run() { while (true) { System.out.println(i++); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); return userService.findUserById(id); } }
重启服务,访问测试
发现打印了4个数字然后报错timeout,第4个数字是因为service的线程睡眠时间影响,所以大概可以认为用了3秒触发超时。
@Reference(timeout = 1000) private UserService userService;
注意:如果两边都配置了服务超时时间,那么consumer优先provider,服务调用方的超时时间会覆盖服务提供方。
建议:在服务提供者上配置超时时间,因为定义服务的人写具体service逻辑时才能估计服务耗时多久。
2. 重试机制
- 设置了超时时间,在这个时间段内,无法完成服务访问,则自动断开连接。
- 如果出现网络抖动,则这一次请求就会失败。
- Dubbo提供重试机制来避免类似问题的发生。
- 通过retries属性来设置重试次数。默认为2次。
import com.itheima.pojo.User; import com.itheima.service.UserService; import org.apache.dubbo.config.annotation.Service; // 将这个类提供的方法(服务)对外发布。将访问地址(ip、端口、访问路径)注册到ZK注册中心 @Service(timeout = 3000, retries = 2) // 当前服务3秒超时,超时后,重试2次,一共3次 public class UserServiceImpl implements UserService { public String sayHello() { return "Hello Dubbo RPC Zookeeper!~"; } int i = 1; @Override public User findUserById(int id) { System.out.println("该服务一共被调用:" + i++ + "次。"); User user = new User(1, "zhangsan", "123"); // 模拟数据库查询耗时5秒 try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } return user; } }
四、多版本
- 灰度发布:当出现新功能时,会让一部分用户先使用新功能,用户反馈没问题时,再将所有用户迁移到新功能。
- dubbo中使用version属性来设置和调用同一个接口的不同版本。
import com.itheima.pojo.User; import com.itheima.service.UserService; import org.apache.dubbo.config.annotation.Service; @Service(version = "v1.0") // 指定当前服务的版本 public class UserServiceImpl implements UserService { public String sayHello() { return "Hello Dubbo RPC Zookeeper!~"; } @Override public User findUserById(int id) { System.out.println("old version is v1.0"); User user = new User(1, "zhangsan", "123"); return user; } }
import com.itheima.pojo.User; import com.itheima.service.UserService; import org.apache.dubbo.config.annotation.Service; @Service(version = "v2.0") // 指定当前服务的版本 public class UserServiceImpl2 implements UserService { public String sayHello() { return "Hello Dubbo RPC Zookeeper!~"; } @Override public User findUserById(int id) { System.out.println("new version is v2.0"); User user = new User(2, "lisi", "456"); return user; } }
web服务消费者注入指定版本的service。
@Reference(version = "v1.0") // 注入指定版本的service private UserService userService;
修改web服务注入为v2.0,重启web服务。
@Reference(version = "v2.0") // 注入指定版本的service private UserService userService;
五、负载均衡
负载均衡策略(4种):
- Random:按权重随机,默认值。按权重设置随机概率。
- RoundRobin:按权重轮询。
- LeastActive:最少活跃调用数,相同活跃数的随机。
- ConsistentHash:一致性Hash,相同参数的请求总是发到同一提供者。
Dubbo官方文档:https://cn.dubbo.apache.org/zh-cn/overview/core-features/load-balance/
1. Random
Random:按权重随机,默认值。按权重设置随机概率。
负载均衡是在服务器集群部署环境下操作的,我们可以让同一个服务启动三次来模拟多台服务器集群。但是需要注意:端口需要改变,否则会冲突。
服务器配置如下,每修改完一次端口启动一个服务。
- dubbo-service服务器1:
- tomcat插件的端口:9000
<dubbo:protocol port="20880"/> 默认值为20880
<dubbo:parameter key="qos.port" value="22222"/> qos.port默认值为22222
@Service(weight = 100)
- dubbo-service服务器2:
- tomcat插件的端口:9002
<dubbo:protocol port="20882"/>
<dubbo:parameter key="qos.port" value="44444"/>
@Service(weight = 200)
- dubbo-service服务器3:
- tomcat插件的端口:9003
<dubbo:protocol port="20883"/>
<dubbo:parameter key="qos.port" value="55555"/>
@Service(weight = 100)
- dubbo-web服务:
- tomcat插件的端口:8000
<dubbo:parameter key="qos.port" value="33333"/>
@Reference(loadbalance = "random")
import com.itheima.pojo.User; import com.itheima.service.UserService; import org.apache.dubbo.config.annotation.Service; @Service(weight = 100) public class UserServiceImpl implements UserService { public String sayHello() { return "1....."; } @Override public User findUserById(int id) { User user = new User(1, "zhangsan", "123"); return user; } }
import com.itheima.pojo.User; import com.itheima.service.UserService; import org.apache.dubbo.config.annotation.Service; @Service(weight = 200) public class UserServiceImpl implements UserService { public String sayHello() { return "2....."; } @Override public User findUserById(int id) { User user = new User(1, "zhangsan", "123"); return user; } }
import com.itheima.pojo.User; import com.itheima.service.UserService; import org.apache.dubbo.config.annotation.Service; @Service(weight = 100) public class UserServiceImpl implements UserService { public String sayHello() { return "3....."; } @Override public User findUserById(int id) { User user = new User(1, "zhangsan", "123"); return user; } }
在消费者配置接口负载均衡策略,4种策略名称可以通过搜索AbstractLoadBalance的4种实现类中定义的name值查看。
@Reference(loadbalance = "random") // 设置负载均衡策略 private UserService userService;
可能会遇到的问题:maven无法下载源码
通过IDEA进行下载源码,点进源码的.class文件,IDE会自动弹出Download Sources or Choose Sources当点击Download Sources时,会报如下错误:
Cannot download sources Sources not found for: org.apache.xxx
解决方法:通过命令行终端进入到工程文件目录里(pom.xml文件所在目录),执行如下命令:
mvn dependency:resolve -Dclassifier=sources
接下来他就会下载jar和源码了
通过权重配比,访问后可以发现,服务器1、2、3的访问概率大概为25%、50%、25%,实现了第一种Random按权重随机的负载均衡。
2. RoundRobin
RoundRobin:按权重轮询。
当weight分别为100、200、100时,访问4次的顺序可能为1,2,3,2。
3. LeastActive
LeastActive:最少活跃调用数,相同活跃数的随机。
每个服务维护一个活跃数计数器。当A机器开始处理请求,该计数器加1,此时A还未处理完成。若处理完毕则计数器减1。而B机器接受到请求后很快处理完毕。那么A,B的活跃数分别是1,0。当又产生了一个新的请求,则选择B机器去执行(B活跃数最小),这样使慢的机器A收到少的请求。
4. ConsistentHash
ConsistentHash:—致性Hash,相同参数的请求总是发到同一提供者。
六、集群容错
集群容错模式:
- Failover Cluster:失败重试。默认值。当出现失败,重试其它服务器,默认重试2次,使用retries配置。一般用于读操作。
- Failfast Cluster:快速失败,只发起一次调用,失败立即报错,不重试。通常用于写操作,因为写操作是非幂等性的,可能会造成数据库的数据产生错误情况。
- Failsafe Cluster:失败安全,出现异常时,直接忽略。返回一个空结果,一般用于写一些不太重要的操作,如日志。
- FailbackCluster:失败自动恢复,后台记录失败请求,定时重发,直到成功。重要操作可以用这个模式。
- Forking Cluster:并行调用多个服务器,只要一个成功即返回。但是这个模式性能比较低。
- Broadcast Cluster : 广播调用所有提供者,逐个调用,任意一台报错则报错。适用于服务之间同步性要求高的,需要保持都一致。
这里演示第一种集群容错模式Failover Cluster失败重试。
和之前的启动设置一样,也是设置不同端口启动三台服务器。
默认超时时间timeout为1秒,重试次数为2次。这里让服务器1和2模拟超时情况,服务器3上的服务是不超时的。
import com.itheima.pojo.User; import com.itheima.service.UserService; import org.apache.dubbo.config.annotation.Service; @Service public class UserServiceImpl implements UserService { public String sayHello() { return "hello dubbo"; } @Override public User findUserById(int id) { System.out.println("1....."); User user = new User(1, "zhangsan", "123"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } return user; } }
import com.itheima.pojo.User; import com.itheima.service.UserService; import org.apache.dubbo.config.annotation.Service; @Service public class UserServiceImpl implements UserService { public String sayHello() { return "hello dubbo"; } @Override public User findUserById(int id) { System.out.println("2....."); User user = new User(1, "zhangsan", "123"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } return user; } }
import com.itheima.pojo.User; import com.itheima.service.UserService; import org.apache.dubbo.config.annotation.Service; @Service public class UserServiceImpl implements UserService { public String sayHello() { return "hello dubbo"; } @Override public User findUserById(int id) { System.out.println("3....."); User user = new User(1, "zhangsan", "123"); return user; } }
搜索Cluster的实现类FailoverCluster,
import com.itheima.pojo.User; import com.itheima.service.UserService; import org.apache.dubbo.config.annotation.Reference; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { // 配置集群容错模式为failover失败重试(默认) @Reference(cluster = "failover") private UserService userService; @RequestMapping("/sayHello") public String sayHello() { return userService.sayHello(); } /** * 根据id查询用户信息 * @param id * @return */ @RequestMapping("/find") public User find(int id) { return userService.findUserById(id); } }
启动三个服务器和web服务,测试访问。
服务器1超时,服务器2重试。
服务器2超时,服务器3重试。
服务器3未超时执行完毕,返回查询数据。
其中中间过程web服务会报一个远程调用超时错误。
七、服务降级
在并发量较高的情况下,服务器的资源将要被跑满,此时就可以关闭掉一些不重要的服务,比如为了让支付服务不受到影响,可选择关闭广告与日志服务,释放掉一些资源,降低服务器压力,即对服务进行降级处理。
服务降级方式:
mock=force:return null
表示消费方对该服务的方法调用都直接返回null值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。mock=fail:return null
表示消费方对该服务的方法调用在失败后,再返回null值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。
@Reference(mock = "force:return null") // 暴力返回null,不再调用userService服务 private UserService userService;
@Reference(mock = "fail:return null") // 失败后返回null,如服务超时报错 private UserService userService;