Kaze
Kaze
Published on 2022-05-18 / 53 Visits
0
0

Dubbo

Dubbo介绍

分布式

一个项目同时运行在多台服务器上,称之为集群部署。

分布式架构的核心理念,是按照不同领域的 拆分 系统。不同的子系统放在各自的项目里,部署在各自的集群中。拆分系统后,每个子系统的修改和上线,都是独立,不会相互影响。每个子系统由 web 层和 服务 层两部分组成。

服务化

分布式系统会带来错综复杂的依赖关系,错综复杂的依赖关系会导致系统越来越难以维护,而采用服务化可以解决这个问题。

服务化的一般步骤:

  1. 抽象核心服务:

    服务化的先决条件是按照业务领域进行拆分,拆分过程中,需要梳理、分析业务;拆分的目标是抽象出核心服务。

    所以 核心 服务,是与具体业务关联度低、通用性高的服务。

  2. 调用核心服务:

    不同的计算机上运行的服务,相互之间要完成调用。最常用的解决方案是:Dubbo

服务化工程

创建多模块项目

Artifact 命名为 ××.all 表示这是一个多模块项目

一般步骤:

  1. 在项目中创建application子模块( 用于部署和启动工程)
  2. 将包含启动类的src目录覆盖application空src目录
  3. 修改pom.xml,将下面内容移到application/pom.xml
<build>
     <plugins>
          <plugin>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <groupId>org.springframework.boot</groupId>
           </plugin>
      </plugins>
</build>
  1. application.properties移到application/src/main/resources

业务模块总集

一般步骤:

  1. 在项目中创建shared.all子模块

  2. 删除shared.all/src目录(因为该模块用于管理所有业务模块,没有代码)

  3. 修改shared.all/pom.xml添加几个标签:

    • <parent></parent> 内,增加 <relativePath>../pom.xml</relativePath>

    • 添加 <packaging><groupId>

    子模块的<groupId> 默认是继承父模块的,可以自定义子模块的<groupId>

业务父模块

一般步骤:

  1. shared.all中创建xxx.shared子模块

  2. 修改xxx.shared/pom.xml

    • <parent></parent> 内,增加 <relativePath>../pom.xml</relativePath>

    • 添加 <packaging>

  3. 删除xxx.shared/src目录(该模块用于管理某个领域的业务模块,也没有代码)

业务功能模块

一般步骤:

  1. xxx.shared中创建xxx.apixxx.servicexxx.service.impl子模块
  2. 业务功能模块包含了代码,需要在项目启动时被加载。但由于跟主启动类 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不需要外部依赖
API1. 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 注解用于让系统能够扫描到书写了 SQLxml 文件。

服务接口关键优化

在分布式场景下,对象实例在网络上传输,必须能够序列化。对象序列化必须实现 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 的实例执行完毕方法以后,再返回到调用服务的评论服务。

image-20220517214033229

三个阶段:

系统启动

注册中心是一个独立的系统,所有系统启动后都可以连接注册中心。注册中心的核心工作就是管理各种服务。

SpringBoot 系统在启动的时候,分别会向注册中心注册服务和订阅服务。

服务提供者系统应该先启动,否则消费者系统启动时,会因为找不到服务而出错。

两个服务不能相互依赖,否则就不知道应该先启动哪个系统了。

系统运行时

如果消费者订阅了服务,那么提供者一有什么变化,都会异步通知给消费者更新。

服务调用
消费者调用提供者的服务,并不是通过注册中心中转的,而是 直接请求 提供者的。所以要求服务提供者和消费者在同一个局域网中,服务器计算机之间能够连通。

Dubbo优化

负载均衡

负载均衡的核心作用就是把用户的请求,均匀的分发到集群中每台服务器上。

Dubbo负载均衡实现

因为服务提供者可能是集群,所以Dubbo 负载均衡指的是 消费者 调用服务能够均匀落在每台具体的提供者的服务器上。

基本原理

Dubbo 负载均衡实现方式有以下几种:

  • 随机负载均衡 (默认)
  • 轮询负载均衡
  • 最少活跃负载均衡
  • 最短响应时间负载均衡
  • 一致哈希负载均衡
  1. 随机负载均衡

@DubboService 注解配置提供的服务时,可以设置一个权重字段:

@DubboService(weight = 0)

绝大多数情况下,是不配置这个 weight (权重)字段,默认值就是 0 。此时为均等随机。

  1. 轮询负载均衡

就是 依次 的调用所有的 Provider 。权重高的,会优先被轮到。

随机负载均衡在大量调用的情况下,每台 Provider 被调用到的比例,与权重比例是趋于一致的。但少量调用就不一定了。

轮询负载均衡可以保证少量调用情况下,每台 Provider 被调用到的比例与权重比例一致。

  1. 最少活跃负载均衡

是指在调用时判断此时每个服务提供者此时正在处理的请求个数,选取最小的调用。

  1. 最短响应时间负载均衡

指预估出来每个处理完请求的提供者所需时间,然后又选择最少最短时间的提供者进行调用。

  1. 一致哈希负载均衡

一致性哈希算法的负载均衡保证了 相同的请求 (包括参数也完全相同)将会落到同一台服务器上。

修改负载均衡策略的方法

服务消费者 Consumer 决定了负载均衡策略。消费者通过策略计算出结果,决定调用具体哪一台 Provider 服务器。

在消费者端定义服务依赖的时候指定:

@DubboReference(loadbalance = "")
参数值含义
random随机负载均衡
roundrobin轮询负载均衡
leastactive最少活跃负载均衡
shortestresponse最短响应时间负载均衡
consistenthash一致哈希负载均衡

重试机制

@DubboReference(timeout = 1000, retries = 2)

注解的 timeout 参数用于设置超时时间,单位是 毫秒 。默认超时时间是 1000 毫秒。

注解的 retries 参数用于设置重试次数。默认重试次数是 2 次。重试次数是 不包括 第一次正常调用的。

注意

比较重要的写操作,最好写在一个独立的 interface 中(读写分离,提高性能)。写数据的调用,重试次数建议设置为 0 ,即不重试。因为往往写操作比较慢,还没有完全操作完毕就超时了(写操作可以适当延长超时时间,提高写入成功率),如果重试,会导致几份一摸一样的数据(无用的数据成为脏数据),连带着可能导致很多不可预料的错误。


Comment