Dubbo介绍
分布式
一个项目同时运行在多台服务器上,称之为集群部署。
分布式架构的核心理念,是按照不同领域的 拆分 系统。不同的子系统放在各自的项目里,部署在各自的集群中。拆分系统后,每个子系统的修改和上线,都是独立,不会相互影响。每个子系统由 web 层和 服务 层两部分组成。
服务化
分布式系统会带来错综复杂的依赖关系,错综复杂的依赖关系会导致系统越来越难以维护,而采用服务化可以解决这个问题。
服务化的一般步骤:
-
抽象核心服务:
服务化的先决条件是按照业务领域进行拆分,拆分过程中,需要梳理、分析业务;拆分的目标是抽象出核心服务。
所以 核心 服务,是与具体业务关联度低、通用性高的服务。
-
调用核心服务:
不同的计算机上运行的服务,相互之间要完成调用。最常用的解决方案是:Dubbo
服务化工程
创建多模块项目
Artifact 命名为 ××.all 表示这是一个多模块项目
一般步骤:
- 在项目中创建
application
子模块( 用于部署和启动工程) - 将包含启动类的
src
目录覆盖application
的空src
目录 - 修改
pom.xml
,将下面内容移到application/pom.xml
中
<build>
<plugins>
<plugin>
<artifactId>spring-boot-maven-plugin</artifactId>
<groupId>org.springframework.boot</groupId>
</plugin>
</plugins>
</build>
- 将
application.properties
移到application/src/main/resources
中
业务模块总集
一般步骤:
-
在项目中创建
shared.all
子模块 -
删除
shared.all/src
目录(因为该模块用于管理所有业务模块,没有代码) -
修改
shared.all/pom.xml
添加几个标签:-
<parent></parent>
内,增加<relativePath>../pom.xml</relativePath>
-
添加
<packaging>
、<groupId>
子模块的
<groupId>
默认是继承父模块的,可以自定义子模块的<groupId>
-
业务父模块
一般步骤:
-
在
shared.all
中创建xxx.shared
子模块 -
修改
xxx.shared/pom.xml
-
<parent></parent>
内,增加<relativePath>../pom.xml</relativePath>
-
添加
<packaging>
-
-
删除
xxx.shared/src
目录(该模块用于管理某个领域的业务模块,也没有代码)
业务功能模块
一般步骤:
- 在
xxx.shared
中创建xxx.api
、xxx.service
、xxx.service.impl
子模块 - 业务功能模块包含了代码,需要在项目启动时被加载。但由于跟主启动类
Application.java
不在同一个模块中,所以必须修改 application/pom.xml,告诉 application 在启动的同时需要加载其他模块
<dependencies>
<dependency>
<artifactId>xxx.api</artifactId>
<groupId>com.youkeda.exercise.shared</groupId>
<version>${project.version}</version>
</dependency>
</dependencies>
用户系统服务化
功能子模块划分原则
一个业务功能模块根据功能层级也需要划分子模块。
所谓功能层级,就是功能作用范围,大多数情况下由三个层级组成:服务接口、服务实现、API。
层级 | 范围 | 作用 |
---|---|---|
服务接口 | 1. 服务接口 2. 核心模型 3. 通用工具类 4. 参数类 | 外部系统需要依赖的 |
服务实现 | 1. 服务实现类 2. 整个 DAO | 不需要外部依赖 |
API | 1. control 2. api | 依赖服务接口,系统自动注入服务实现 |
依赖问题
因为 UserServiceImpl
实现了 UserService
接口,所以 user.service.impl 模块必须依赖 user.service模块,这就要在 user.service.impl/pom.xml 中增加依赖。并且因为服务器实现和操作数据库需要依赖到一些库,所以user.service.impl/pom.xml 不能缺少这些库的依赖。
由于 user.service.impl 依赖了 user.service ,所以 application/pom.xml
中依赖 user.service.impl 就可以了。系统会自动解决间接依赖的问题。
user.api 只依赖 user.service , 不能 依赖 user.service.impl
增加需要扫描的包
由于 Application
类的包路径是 com.youkeda.exercise
,与业务功能模块代码的包路径不一致(com.youkeda.comment
), Application
类就要使用 scanBasePackages
扫描相关的包。
@SpringBootApplication(scanBasePackages = {"com.youkeda.exercise","com.youkeda.comment"})
@MapperScan(basePackages = {"com.youkeda.comment.dao"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
MapperScan
注解用于让系统能够扫描到书写了SQL
的 xml 文件。
服务接口关键优化
在分布式场景下,对象实例在网络上传输,必须能够序列化。对象序列化必须实现 Serializable 接口。
不仅网络上传输的类需要实现序列化接口,传输的类中依赖的其它类,也都必须实现序列化接口。
系统提供的类都默认实现了序列化接口,所以主要还是检查自定义的类。
用Dubbo实现核心服务调用
Dubbo 的作用,是把一个服务注册到注册中心上。注册成功后,其他模块就可以从注册中心找到这个服务了,从而可以调用这个服务的各个方法。
引入依赖库
为 user.service.impl 子模块增加依赖,修改 user.service.impl/pom.xml 文件
<!-- dubbo依赖 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.7</version>
</dependency>
<!-- nacos 注册中心 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-nacos</artifactId>
<version>2.7.7</version>
</dependency>
修改配置
配置的作用主要是为 服务指定一个注册中心。
## 项目名称
dubbo.application.name = youkeda-user-app
## 注册中心地址
dubbo.registry.address = nacos://nacos.dev.youkeda.com:8848
## 服务实现类所在的包,会被自动扫描,如果有多个包,使用英文逗号分割
dubbo.scan.base-packages = com.youkeda.comment.service.impl
## UserService 的版本号,为自定义配置项
user.service.version = 1.0.0
## 每一个服务提供者,都应该单独使用一个端口。
dubbo.protocol.name=dubbo
dubbo.protocol.port=20881
修改代码
UserServiceImpl 使用新的注解:
@DubboService(version = "${user.service.version}")
@Service
public class UserServiceImpl implements UserService {
}
改为使用 @DubboService
注解,让系统能够自动扫描(配置文件中 dubbo.scan.base-packages 指定的包才会扫描)并识别到这是一个 Dubbo 服务。
总结
在一个新项目开始时,就可以采用服务化的结构。
当项目需要分布式部署时,只需要引入 Dubbo ,注册服务到注册中心即可。不需要大规模的改造。
即使项目不需要分布式部署,服务化清晰的结构,也可以让项目整体易于维护。
评论服务化
引用服务
用Dubbo注册的服务可以被其他服务引用
@Component
public class CommentServiceImpl implements CommentService {
@DubboReference(version = "${user.service.version}")
private UserService userService;
}
@DubboReference
表示让系统给 userService
注入 Dubbo 服务的实例。
user.service.version 的值,两个项目务必保持一致,一致才能调通
基本原理
Dubbo 采用了 Java 代理(proxy)技术,是一个 proxy 实例。代理实例的作用,可以把任何 UserService
的方法的调用,转发到提供服务的用户项目上,由用户项目上的真正的 UserServiceImpl
的实例执行完毕方法以后,再返回到调用服务的评论服务。

三个阶段:
系统启动
注册中心是一个独立的系统,所有系统启动后都可以连接注册中心。注册中心的核心工作就是管理各种服务。
SpringBoot 系统在启动的时候,分别会向注册中心注册服务和订阅服务。
服务提供者系统应该先启动,否则消费者系统启动时,会因为找不到服务而出错。
两个服务不能相互依赖,否则就不知道应该先启动哪个系统了。
系统运行时
如果消费者订阅了服务,那么提供者一有什么变化,都会异步通知给消费者更新。
服务调用
消费者调用提供者的服务,并不是通过注册中心中转的,而是 直接请求 提供者的。所以要求服务提供者和消费者在同一个局域网中,服务器计算机之间能够连通。
Dubbo优化
负载均衡
负载均衡的核心作用就是把用户的请求,均匀的分发到集群中每台服务器上。
Dubbo负载均衡实现
因为服务提供者可能是集群,所以Dubbo 负载均衡指的是 消费者 调用服务能够均匀落在每台具体的提供者的服务器上。
基本原理
Dubbo 负载均衡实现方式有以下几种:
- 随机负载均衡 (默认)
- 轮询负载均衡
- 最少活跃负载均衡
- 最短响应时间负载均衡
- 一致哈希负载均衡
- 随机负载均衡
用 @DubboService
注解配置提供的服务时,可以设置一个权重字段:
@DubboService(weight = 0)
绝大多数情况下,是不配置这个 weight (权重)字段,默认值就是 0 。此时为均等随机。
- 轮询负载均衡
就是 依次 的调用所有的 Provider 。权重高的,会优先被轮到。
随机负载均衡在大量调用的情况下,每台 Provider 被调用到的比例,与权重比例是趋于一致的。但少量调用就不一定了。
轮询负载均衡可以保证少量调用情况下,每台 Provider 被调用到的比例与权重比例一致。
- 最少活跃负载均衡
是指在调用时判断此时每个服务提供者此时正在处理的请求个数,选取最小的调用。
- 最短响应时间负载均衡
指预估出来每个处理完请求的提供者所需时间,然后又选择最少最短时间的提供者进行调用。
- 一致哈希负载均衡
一致性哈希算法的负载均衡保证了 相同的请求 (包括参数也完全相同)将会落到同一台服务器上。
修改负载均衡策略的方法
服务消费者 Consumer 决定了负载均衡策略。消费者通过策略计算出结果,决定调用具体哪一台 Provider 服务器。
在消费者端定义服务依赖的时候指定:
@DubboReference(loadbalance = "")
参数值 | 含义 |
---|---|
random | 随机负载均衡 |
roundrobin | 轮询负载均衡 |
leastactive | 最少活跃负载均衡 |
shortestresponse | 最短响应时间负载均衡 |
consistenthash | 一致哈希负载均衡 |
重试机制
@DubboReference(timeout = 1000, retries = 2)
注解的 timeout
参数用于设置超时时间,单位是 毫秒 。默认超时时间是 1000 毫秒。
注解的 retries
参数用于设置重试次数。默认重试次数是 2 次。重试次数是 不包括 第一次正常调用的。
注意
比较重要的写操作,最好写在一个独立的 interface
中(读写分离,提高性能)。写数据的调用,重试次数建议设置为 0 ,即不重试。因为往往写操作比较慢,还没有完全操作完毕就超时了(写操作可以适当延长超时时间,提高写入成功率),如果重试,会导致几份一摸一样的数据(无用的数据成为脏数据),连带着可能导致很多不可预料的错误。