微服务架构可以说是如何将功能分解成一系列服务的一种架构模式。

微服务架构从结构上来看就是将一个应用拆分成多个松耦合的服务,这些服务之间通过某种协议(REST、RPC等)进行互相协作,完成原单体架构下的业务功能,但提供更灵活的部署模式,更容易扩展,降低了开发、运维上的复杂度。对于微服务来说,其中一个关键点就是各服务之间的松耦合,各服务之间通过一种“标准”的协议进行沟通,不需要理解对方服务的实现逻辑、实现方式,只要在对方所提供的服务接口没有变化的情况下不会影响自己所提供的服务功能即可。总而言之,微服务核心思路就是分而治之。

微服务架构具有以下优点:

  • 松耦合:基于微服务架构的应用是一系列小服务集合,这些服务之间通过非具体实现的接口及非专有通信协议进行通信(比如REST),这样只要原接口没有改变,就不会对服务消费者造成任何影响
  • 抽象:一个微服务对其数据结构和数据源拥有绝对的控制权,只有该服务才可以对数据做出修改,其他微服务只有通过该服务才能够访问数据。因此,该服务可以很方便地对所能提供的数据进行有效控制
  • 独立:每个微服务都可以在不影响其他微服务的情况下进行编译、打包和部署,这是单体架构应用所无法做到的
  • 应对用户需求的多样性:微服务架构可以让我们轻松应对不同客户的特殊需求,通过定义良好的接口,可以让不同的微服务承担不同的职责,同时快速部署上线能力可以让用户需求尽早实现
  • 更高可用性和弹性:微服务架构可以认为是一个去中心化的应用,每一个微服务都可以随时上线或下线。这样当某个微服务出现问题时只需要将其下线即可,其他同类型的微服务将承担其功能,对外仍旧可以提供服务,不会造成整个服务无法正常工作

微服务粒度

微服务拆分原则

从面向对象的开发理论中进行借鉴。Robert C. Martin在《敏捷软件开发:原则、模式与实践》一书中提出了面向对象开发的一系列原则与模式,其中有以下两个原则可以在微服务拆分的时候借鉴:

  • 单一职责原则(SRP)
  • 共同封闭原则(CCP)

不应使用微服务架构的情形

  • 构建分布式架构非常吃力时
  • 服务器蔓延时
  • 采用小型应用、快速产品原型时
  • 对数据事务的一致性有一定要求时

提供者与消费者

提供者:暴露接口给其他服务调用

消费者:调用其他服务暴露的接口

服务的角色是相对的,既可以是提供者也可以是消费者。

Ribbon负载均衡

Ribbon的负载均衡规则是一个叫做IRule的接口来定义的,每一个子接口都是一种规则

更改负载均衡策略

通过定义IRule实现可以修改负载均衡规则,有两种方式:

  1. 在服务的启动类中,定义一个新的IRule

    @Bean
    public IRule randomRule() {
     return new RandomRule();
    }
  2. 配置文件:在服务的application.yml文件中,添加新的配置也可以修改规则

    userservice:
      ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule 
    # 负载均衡规

饥饿加载

Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:

ribbon:
  eager-load:
    enabled: true # 开启饥饿加载
    clients: userservice # 指定这个服务饥饿加载

nacos注册中心

Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高。

下载nacos

GitHub主页:https://github.com/alibaba/nacos

安装后使用终端进入bin目录:执行命令:startup.cmd -m standalone

默认占用8848端口。

开启后在浏览器访问:http://127.0.0.1:8848/nacos/index.html

默认的账号和密码都是nacos。

服务注册到nacos

  1. 添加依赖

父工程中添加:

        <!--           添加springcloud-alilbaba管理依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.2.6.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

添加nacos客户端依赖:

<!-- nacos客户端依赖包 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    // 可能需要指定版本吧。。。
</dependency>
  1. 添加配置

    spring:
      cloud:
        nacos:
          server-addr: 8848
  2. 启动测试

服务分级存储模型

Nacos服务分级存储模型:

  1. 一级是服务,例如userservice
  2. 二级是集群,例如杭州或上海
  3. 三级是实例,例如杭州机房的某台部署了userservice的服务器

服务跨集群调用:

服务调用尽可能选择本地集群的服务,跨集群调用延迟较高本地集群不可访问时,再去访问其它集群。

修改application.yml,添加如下内容设置集群属性:

spring:
  cloud:
    nacos:
      server-addr: localhost:8848  #nacos 服务端地址
      discovery:
        cluster-name: HZ # 配置集群名称,也就是机房位置,例如:HZ,杭州

根据集群负载均衡

例如在设置集群属性后,在order-service中设置负载均衡的IRule为NacosRule,这个规则优先会寻找与自己同集群的服务(同在HZ的集群)。

# 在服务消费者配置服务提供者的服务名
userservice:
  ribbon:
     NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则

NacosRule负载均衡策略:

  • 优先选择同集群服务实例列表
  • 本地集群找不到提供者,才去其它集群寻找,并且会报警告
  • 确定了可用实例列表后,再采用随机负载均衡挑选实例

根据权重负载均衡

在Nacos控制台可以设置实例的权重值,首先选中实例后面的编辑按钮

将权重设置为0.1,测试可以发现被设置的服务访问到的频率大大降低。

环境隔离(namespace)

新建一个命令空间(dev)后会自动生成id,然后在需要加入命令空间的服务配置中加入生成的id

spring:
  application:
    name: orderserver
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ
        namespace: 5cf1df2c-48ca-4a0e-bffb-62358011c082 # id

此时dev这个命令空间里面的服务如果要访问非dev命令空间的服务此时因为namespace不同,会导致找不到其他服务(userservice),控制台会报错:java.lang.IllegalStateException: No instances available for userserver

临时实例和非临时实例

服务注册到Nacos时,可以选择注册为临时或非临时实例,通过下面的配置来设置:

spring:
  cloud:
    nacos:
      discovery:
        ephemeral: false # 设置为非临时实例

nacos配置管理

统一配置管理:

在Nacos中添加配置信息:配置文件的DataId:[服务名称]-[profile].[后缀名]例如:userserver-dev.yaml

pattern:
    dateformat: yyyy年-MM月-dd日

配置获取的步骤:

项目启动-》读取nacos中配置文件-》读取本地配置文件application.yaml-》创建spring容器-》加载bean。

  1. 引入Nacos配置管理客户端依赖

           <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
                <version>2021.0.1.0</version>
            </dependency>
  2. 在userserver服务的resource目标添加bootstrap.yml文件,这个文件是引导文件优先级高于本地application.yml文件

    spring:
      application:
        name: userserver # 服务名称
      profiles:
        active: dev #开发环境,这里是dev 
      cloud:
        nacos:
          server-addr: localhost:8848 # Nacos地址
          config:
            file-extension: yaml # 文件后缀名
  3. 验证是否使用配置管理中的配置:

    @Value("${pattern.dateformat}")
    private String dateformat;
    
    @GetMapping("now")
    public String now(){
        //格式化时间
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }

配置热更新

修改nacos中的配置后,无序重启服务,让配置生效,即热更新。

两种方式:

  1. @Value注入的变量所在的类上搭配@RefreshScope注解
  2. 配置类 + @ConfigurationProperties注解

多环境配置共享

微服务启动时会从nacos读取多个配置文件:

  • [spring.application.name]-[spring.profiles.active].yaml,例如:userservice-dev.yaml
  • [spring.application.name].yaml,例如:userservice.yaml

无论profile如何变化,[spring.application.name].yaml这个文件一定会加载,因此多环境共享配置可以写入这个文件

多种配置的优先级:

  • nacos中的配置:[服务名]-[环境].yaml->[服务器].yaml>本地配置

多服务共享配置

xxx

Nacos集群

http客户端Feign

Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign

安装使用feign客户端:

  1. 添加依赖:

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
  2. 在启动类添加@EnableFeignClients注解开启Feign功能
  3. 编写Feign客户端:

    • 新建一个接口加上@FeignClient(服务名) 注解

    public interface UserClient {

      @GetMapping("user/{id}")
      User getById(@PathVariable Long id);

    }

    
    - 注入使用该接口即可
    
    > 自定义Feign的配置
    
    Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:
    
    | **类型**               | **作用**         | 说明                                                   |
    | ---------------------- | ---------------- | ------------------------------------------------------ |
    | **feign.Logger.Level** | 修改日志级别     | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL         |
    | feign.codec.Decoder    | 响应结果的解析器 | http远程调用的结果做解析,例如解析json字符串为java对象 |
    | feign.codec.Encoder    | 请求参数编码     | 将请求参数编码,便于通过http请求发送                   |
    | feign.Contract         | 支持的注解格式   | 默认是SpringMVC的注解                                  |
    | feign.Retryer          | 失败重试机制     | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
    
    有两种方式:1.配置文件的方式。2.Java代码的方式
  4. 配置文件:

    1. 全局配置:

      feign:
        client:
          config:
            default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
              logger-level: full
    2. 局部配置:

      feign:
        client:
          config:
            orderserver: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
              logger-level: full
  5. Java代码方式,需要先声明一个Bean

    1. public class FeignClientConfiguration {
          @Bean
          public Logger.Level feignLevel() {
              return Logger.Level.FULL;
          }
    
    2. `@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)` :全局配置
    
    3. 如果是局部配置,则把它放到@FeignClient这个注解中:`@FeignClient(value = "userservice", configuration = FeignClientConfiguration.class)`

Feign性能优化

Feign底层的客户端实现:

  • URLConnection:默认实现,不支持连接池
  • Apache HttpClient :支持连接池
  • OKHttp:支持连接池

因此优化Feign的性能主要包括:

  • 使用连接池代替默认的URLConnection
  • 日志级别,最好用basic或none

性能优化-连接池配置:

  1. 引入HttpClient依赖:

    <!--httpClient的依赖 -->
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-httpclient</artifactId>
    </dependency>
  2. 配置:

    feign:
      httpclient:
        enabled: true
        connection-timeout: 3000
        max-connections: 100 # 最大连接数
        max-connections-per-route: 30 # 每个路径的最大连接数

Feign最佳实践

  1. 让controller和FeignClient继承同一接口
  2. 将FeignClient、POJO、Feign的默认配置都定义到一个项目中,供所有消费者使用

当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。有两种方式解决:

  1. 方式一:指定FeignClient所在包:

    1. @EnableFeignClients(basePackages = "cn.itcast.feign.clients")
  2. 方式二:指定FeignClient字节码对象:

    1. @EnableFeignClients(clients = UserClient.class)

网关gateway

为什么需要网关:

  • 身份认证和权限认证
  • 服务路由、负载均衡
  • 请求限流

在springCloud中技术的实现有两种:

  • gateway
  • zuul

Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。

搭建网关服务(新建一个Module):

  1. 引入网关依赖:

    <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
  2. 引入nacos注册发现依赖:

    <dependency>
     <groupId>com.alibaba.cloud</groupId>
     <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
     <version>2.2.1.RELEASE</version>
    </dependency>
  3. 编写路由及nacos地址

    server:
      port: 10010 # 网关端口
    spring:
      application:
        name: gatewayserver # 服务名
      cloud:
        nacos:
          server-addr: localhost:8848 # nacos地址
        gateway:
          routes:
    - id: user-server # 路由id, 自定义, 唯一即可
      uri: lb://userserver # 路由的目标地址  lb是负载均衡,后面跟服务名称
      predicates:
     - Path=/user/** # 按照路径匹配,匹配所有以/user开头

路由断言工厂 Route Predicates Factory

网关路由可以配置的内容包括:

  • 理由id:路由的唯一地址
  • uri:路由的目标地址支持,lb和http两种
  • predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地
  • filters:路由过滤器,请求路由或响应

我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件:

  • 例如Path=/user/**是按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的
  • 像这样的断言工厂在SpringCloudGateway还有十几个

Spring提供了11中基本的Predicates工厂:

Java-微服务技术栈

server:
  port: 10010
spring:
  application:
    name: gatewayserver
  cloud:
    nacos:
      server-addr: localhost:8848
    gateway:
      routes:
        - id: user-server # 路由id, 自定义, 唯一即可
          uri: lb://userserver # 路由的目标地址  lb是负载均衡,后面跟服务名称
          predicates:
            - Path=/user/** # 按照路径匹配,匹配所有以/user开头
            #匹配亚洲时间2022-11-29T17:42:47以后的请求
            - after=2022-11-29T17:42:47.000+08:00[Asia/Shanghai]

路由过滤器

GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。

Spring提供了31种不同的过滤器工厂(https://docs.spring.io/spring-cloud-gateway/docs/3.0.8/reference/html/#gatewayfilter-factories):

例如:

名称说明
AddRequestHeader给当前请求添加一个请求头
.....

注意:要用网关服务发请求路由到配置的服务中!!!

默认过滤器:

  • server:
      port: 10010
    spring:
      application:
        name: gatewayserver
      cloud:
        nacos:
          server-addr: localhost:8848
        gateway:
          routes:
            - id: user-server # 路由id, 自定义, 唯一即可
              uri: lb://userserver # 路由的目标地址  lb是负载均衡,后面跟服务名称
              predicates:
                - Path=/user/** # 按照路径匹配,匹配所有以/user开头
              filters: # 给这个服务添加过滤器
                - AddRequestHeader=X-Request-Id, request-id-123
          default-filters: # 默认过滤器-对所有路由生效
            - AddRequestHeader=sign, xn2001.com is eternal

    全局过滤器:GlobalFilter:

  • 全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现,
  • 定义方式是实现GlobalFilter接口:

    public class AuthorizeFilter implements GlobalFilter {

      @Override
      public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
          String authorization = exchange.getRequest().getQueryParams().getFirst("authorization");
          if (authorization.equals("authorization")) {
              System.out.println("已有请求参数authorization");
              return chain.filter(exchange);
          }
          exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
          return exchange.getResponse().setComplete();
      }

    }

过滤器执行顺序:

默认过滤器->路由过滤器->全局过滤器

请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器。

每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。

两种方式执行过滤器的优先顺序:

  • 全局过滤器通过加入@order(xx) 来设置
  • 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
  • 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行

跨域问题处理

跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题

解决方案:CORS

网关处理跨域采用的同样是CORS方案,并且只需要简单配置即可实现:

server:
  port: 10010
spring:
  application:
    name: gatewayserver
  cloud:
    nacos:
      server-addr: localhost:8848
    gateway:
      routes:
        - id: user-server # 路由id, 自定义, 唯一即可
          uri: lb://userserver # 路由的目标地址  lb是负载均衡,后面跟服务名称
          predicates:
            - Path=/user/** # 按照路径匹配,匹配所有以/user开头
            #匹配亚洲时间2022-11-29T17:42:47以后的请求
          filters:
            - AddRequestHeader=X-Request-Id, request-id-123
        - id: order-server
          uri: lb://order-server
          predicates:
            - Path=/order/**
          filters:
            - AddRequestHeader=name,qmou
      default-filters:
        - AddRequestHeader=sign, xn2001.com is eternal
      globalcors: # 跨越配置
        cors-configurations:
          '[**]':
            allowedOrigins:
              - "http://localhost:10010"  # 跨域的地址
            allowedMethods: # 跨域请求
              - "GET"
              - "POST"
              - "DELETE"
            allowed-headers: "*" # 运行携带的请求头
            allowCredentials: true # 是否允许Cookie
            maxAge: 30000 # 本次跨域的有效期
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题

docker

在ubuntu中安装:https://www.runoob.com/docker/ubuntu-docker-install.html

启动docker:sudo service docker start

停止docker:sudo service docker stop

重启docker:sudo service docker restart

DocKer基本操作

镜像相关命令:

  • 镜像一般分两部分组成:[repository]:[Tag] 例如:mysql:5.7
  • 在没有指定tag时,默认是latest,代表最新版本的镜像

docker pull : 从服务拉取镜像

docker images : 查看镜像

docker save [options(导出的位置)] image [image..]

docker load [-i(指定导入的文件)]

docker rmi 删除镜像

容器

创建一个nginx容器:

docker run --name some-nginx -d -p 8080:80 some-content-nginx

  • docker run:创建并运行一个容器
  • ssome-nginx: 自定义的容器名称
  • 8080:80:主机端口与容器端口映射,左边是主机端口,右侧是容器端口
  • -d:后台运行
  • some-content-nginx:镜像名称。例如nginx

创建后会返回容器Id。

查看容器状态:

  • docker ps
  • 添加-a参数查看所有状态的容器

查看容器日志的命令:

  • docker logs containerName
  • 添加 -f 参数可以持续查看日志

进入nginx容器,修改html文件内容

1.进入容器:docker exec -it mn bash

  • -it 给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互
  • mn:进入的容器名称
  • bash:进入容器后执行的命令,bash是一个linux终端交互命令

2.进入nginx的HTML所在目录 /user/share/nginx/html

3.修改内容:

  • sed -i 's#Welcome to nginx#Author:QuanMou#g' index.html

4.刷新查看

删除容器:

  • docker rm containerName
  • 不能删除正在运行中,使用 -f 参数可以强制删除

修改容器状态:

  • 暂停:docker pause containerName
  • 解除暂停:docker unpause containerName
  • 停止容器:docker start containerName
  • 开启容器:docker start containerName

数据卷

数据卷:是一个虚拟目录,指向宿主机文件系统中的某个目录。

docker volume [COMMAND]

docker volume 是数据卷操作,根据命令后面的command来确定下一步的操作

  • create:创建一个colume
  • inspect:显示一个或多个volume信息
  • ls:列出所有的volume
  • prune:删除未使用的colume
  • rm:删除一个或多个指定的colume

例如:

  1. 添加一个html volume:docker volume create html
  2. 查看volume:docker volume ls
  3. 查看html volume的详细信息:docker volume inspect html

数据卷的作用:

  • 将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全

挂载数据卷

在创建容器时,通过-v 参数来挂载数据卷到某个容器目录

docker run --name nx -p 89:80 -v html:/root/htm -d 镜像名称
  • -v: html数据卷挂载到容器 /root/htm 目录

将html数据卷挂载到nginx容器中:

  1. 运行容器并挂载:docker run --name nx1 -p 89:80 -v html:/user/share/nginx/html -d nginx
  2. 在宿主中添加修改文件会发现容器中的文件发生改变

如果容器运行时volume不存在,会自动被创建出来。

将宿主目录直接挂载到容器目录

目录挂载和数据卷挂载语法是类型的。

  • -v [宿主机目录]:[容器内目录]
  • -v [宿主机文件]:[容器内文件]

自定义镜像

镜像结构:

  • 镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。

什么是dockerfile

  • Dockerfile就是一个文本文件,其中包含一个个的指令(Instruction),用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer
指令说明示例
FROM指定基础镜FROM centos:6
ENV设置环境变量,可在后面指令使用ENV key value
COPY拷贝本地文件到镜像的指定目录COPY ./mysql-5.7.rpm /tmp
RUN执行Linux的shell命令,一般是安装过程的命令RUN yum install gcc
EXPOSE指定容器运行时监听的端口,是给镜像使用者看的EXPOSE:8800
ENTRYPOINT镜像中应用的启动命令,容器运行时调用ENTRYPOINT java -jar xx.jar

详细语法说明,请参考官网文档: https://docs.docker.com/engine/reference/builder

案例:基于Ubuntu镜像构建一个新镜像,运行一个java项目

创建一个Dockerfile文件,使用上面的指令指定配置。然后对Dockerfile这个文件进行构建。

# 指定基础镜像
FROM ubuntu:16.04

# config JDK install path
ENV JAVA_DIR=/java/local

# copy jdk And java Project packge
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar

# install jdk
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8

# config evn varibale
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin

# export port
EXPOSE 8090

# project entrypoint 
ENTRYPOINT java -jar /tmp/app.jar

构建镜像命令:docker build -t name .

  • -t后面指定新的镜像的标签名(tag)
  • . 最后的一个点指明docker context为当前目录。docker默认会从build context中查找 Dockerfile文件,我们也可以通过-f参数指定Dockerfile的位置

更便捷的构建java项目:使用openjdk:8 作为基础镜像。

FROM openjdk:8
COPY ./app.jar /tmp/app.jar
EXPOSE 8090
ENTRYPOINT java -jar /tmp/app.jar

DockerCompose

Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器!

  • 使用 Docker Compose 可以轻松、高效的管理容器,它是一个用于定义和运行多容器 Docker 的应用程序工具

Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。

version: "3.8"
services:
  mysql:
    image: mysql:5.7.25
    environment:
     MYSQL_ROOT_PASSWORD: 123 
    volumes:
     - "/tmp/mysql/data:/var/lib/mysql"
     -  "/tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf"
  web:
    build: .
    ports:
     - "8090:8090"

DockerCompose的详细语法参考官网:https://docs.docker.com/compose/compose-file/

准备操作:

  • 安装:curl -L https://github.com/docker/compose/releases/download/1.23.1/docker-compose-uname -s-uname -m > /usr/local/bin/docker-compose
  • 可以把文件移动到 /usr/local/bin
  • 修改文件权限:chmod +x /usr/local/bin/docker-compose
  • Base自动补全命令:curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose

如果这里出现错误,需要修改自己的hosts文件:

echo "199.232.68.133 raw.githubusercontent.com" >> /etc/hosts

使用docker-compose -v 检查是否安装成功。

Docker-Componse部署微服务集群:

新建docker-compose.yaml

version: "3.2"

services:
  nacos:
    image: nacos/nacos-server 
    environment:
      MODE: standalone
    ports:
      - "8848:8848"
  mysql:
    image: mysql:5.7.25
    environment:
      MYSQL_ROOT_PASSWORD: 123
    volumes:
      - "$PWD/mysql/data:/var/lib/mysql"
      - "$PWD/mysql/conf:/etc/mysql/conf.d/"
  userservice:
    build: ./user-service
  orderservice:
    build: ./order-service
  gateway:
    build: ./gateway
    ports:
      - "10010:10010"

构建:docker-compose up -d 构建所有容器并在后台运行。

  • 查看日志:docker-compose logs -f
  • 重启容器;docker-compose restart 容器名 ..

拉取镜像->根据镜像生成容器。

自定义镜像(Dockerfile->docker build -t xxx)->根据自定义镜像生成容器。

Docker 镜像仓库

镜像仓库( Docker Registry )有公共的和私有的两种形式:

  • 公共仓库:例如Docker官方的 Docker Hub,国内也有一些云服务商提供类似于 Docker Hub 的公开服务,比如 网易云镜像服务、DaoCloud 镜像服务、阿里云镜像服务等
  • 除了使用公开仓库外,用户还可以在本地搭建私有 Docker Registry。企业自己的镜像最好是采用私有Docker Registry来实现。

使用DockerCompose部署带有图象界面的DockerRegistry,命令如下:

docker-compose.yml

version: '3.0'
services:
  registry:
    image: registry
    volumes:
      - ./registry-data:/var/lib/registry
  ui:
    image: joxit/docker-registry-ui:static
    ports:
      - 8080:80
    environment:
      - REGISTRY_TITLE=权某人的私有仓库
      - REGISTRY_URL=http://registry:5000
    depends_on: # 依赖上面的镜像
      - registry

运行:docker-compose up -d

访问8080这个端口。

配置Docker信任地址

我们的私服采用的是http协议,默认不被Docker信任,所以需要做一个配置:

# 打开要修改的文件
vi /etc/docker/daemon.json
# 添加内容:
"insecure-registries":["http://192.168.150.101:8080"]
# 重加载
systemctl daemon-reload
# 重启docker
systemctl restart docker

在私有镜像拉取获取推送镜像

推送镜像到私有镜像服务必须先tag,步骤如下:

  1. 重新tag本地镜像,名称前缀为私有仓库的地址:192.168.150.101:8080/

docker tag nginx:latest 192.168.150.101:8080/nginx:1.0

  1. 推送镜像

docker push 192.168.150.101:8080/nginx:1.0

  1. 拉取镜像

docker pull 192.168.150.101:8080/nginx:1.0

  • 推送本地镜像到仓库前都必须重命名(docker tag)镜像,以镜像仓库地址为前缀
  • 镜像仓库推送前需要把仓库地址配置到docker服务的daemon.json文件中,被docker信任

rabbitMQ

同步通信

异步通信

MQ (MessageQueue),中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。、

常见的消息队列实现:

  • RabbitMQ、ActiveMQ、RocktMQ、Kafka

RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网地址:https://www.rabbitmq.com/

使用Docker拉取镜像

docker pull rabbitmq:3-management

运行下面的命令来运行MQ:

docker run \
 -e RABBITMQ_DEFAULT_USER=quanMou \  # 用户名
 -e RABBITMQ_DEFAULT_PASS=123321 \ # 密码
 --name mq \  # 容器名
 --hostname mq1 \   # 主机
 -p 15672:15672 \  # 控制面板的端口
 -p 5672:5672 \  # 连接端口
 -d \  # 后台运行
 rabbitmq:3-management # 镜像
docker run \
 -e RABBITMQ_DEFAULT_USER=quanMou \  
 -e RABBITMQ_DEFAULT_PASS=123321 \ 
 --name mq \  
 --hostname mq1 \   
 -p 15672:15672 \  
 -p 5672:5672 \  
 -d \  
 rabbitmq:3-management 

然后运行本机15672端口。输入用户名quanMou,密码123321登录到rabbitmq面板。

官方的简单消息队列demo:

Publisher:

public class PublisherTest {
    @Test
    public void testPublisherSimpQueue() throws IOException, TimeoutException {

////      创建连接
        ConnectionFactory factory = new ConnectionFactory();
//      设置主机、端口、虚拟主机、用户名、密码
        factory.setHost("192.168.204.139");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("QuanMou");
        factory.setPassword("123");
//      建立连接
        Connection connection = factory.newConnection();
//       创建通道
        Channel channel = connection.createChannel();
//      创建队列
        String queueName = "simp.queue"; //队列名
        channel.queueDeclare(queueName,false,false,false,null);
//       发送消息
        String message = "Hello RabbitMQ!!";
        channel.basicPublish("",queueName,null,message.getBytes());
        System.out.println("发送消息成功:" + message);
//       关闭通道、连接
        channel.close();
        connection.close();

    }
}

consumer:

public class ConsumerTest {

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.204.139");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("QuanMou");
        factory.setPassword("123");
        String queueName = "simple.queue";
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(queueName,false,false,false,null);
        channel.basicConsume(queueName, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumerTag" + consumerTag);
//                处理消息
                String message = new java.lang.String(body);
                System.out.println("接收的消息:" + message);

            }
        });
        System.out.println("等待处理消息");
    }
}

SpringAMQP

父工程引入amqp依赖:

        <!--AMQP依赖,包含RabbitMQ-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

在配置文件中配置主机、端口、密码等:

logging:
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
spring:
  rabbitmq:
    host: 192.168.204.139
    virtual-host: /
    username: QuanMou
    password: 123

编写代码:

发送消息:

@Component
@RunWith(SpringRunner.class)
@SpringBootTest
public class PublisherTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testAMQPPublisherSimpleQueue() {
        String queueName = "simp.queue";
        String message = "下单订单";
        rabbitTemplate.convertAndSend(queueName,message);
    }
}

消费:

@Component
public class SpringSonsumerListener {
    # 监听这个队列
    @RabbitListener(queues = "simp.queue")
    public void listenerSimpQueue(String msg) {
        System.out.println("消费者接收的消息" + msg);
    }
}

启动spring,默认加载这个容器。

Work Queue:工作队列

Work queue 工作队列,可以提高消息处理速度,避免队列消息堆积。

就是写多个监听方法,一个队列对应多个消费者。

修改application.yml文件,设置preFetch这个值,可以控制预取消息的上限:

spring:
  rabbitmq:
    host: 192.168.204.139
    virtual-host: /
    username: QuanMou
    password: 123
    listener:
      simple:
        prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息

发布、订阅

发布:publish;订阅:subscribe

发布订阅模式与之前案例的区别就是允许将同一消息发送给多个消费者。实现方式是加入了exchange(交换机)。常见exchange类型包括:

  • Fanout:广播
  • Direct:路由
  • Topic:话题

提供者把消息exchange(交换机)由交换机转发消息给队列。

注意:exchange负责消息路由,而不是存储,路由失败则消息丢失。

Fanout exchange

Fanout Exchange会将消息广播到每一个跟其绑定的队列。

使用SpirngAMQP颜色FanoutExchange的使用

  • 在Consumer服务中用代码声明队列,交换机并将其绑定到交换机中

    import org.springframework.amqp.core.Binding;
    import org.springframework.amqp.core.BindingBuilder;
    import org.springframework.amqp.core.FanoutExchange;
    import org.springframework.amqp.core.Queue;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    @Configuration
    public class FanoutQueue {

    // 声明队列

      @Bean
      public Queue OrderQueue() {
          return new Queue("order-queue");
      }
      @Bean
      public Queue RepositoryQueue() {
          return new Queue("repository-queue");
      }
    

    // 声明交换机

      @Bean
      public FanoutExchange FanoutExchange() {
          return new FanoutExchange("shop-fanout");
      }
    
      //   把Order队列绑定到交换机
      @Bean
      public Binding bindOrderQueue(@Qualifier("OrderQueue") Queue orderQueue, FanoutExchange FanoutExchange) {
          return BindingBuilder
                  .bind(orderQueue)
                  .to(FanoutExchange);
      }
    
      // 把repository队列绑定到交换机
      @Bean
      public Binding bindRepQueue(@Qualifier("RepositoryQueue") Queue repository, FanoutExchange FanoutExchange) {
          return BindingBuilder
                  .bind(repository)
                  .to(FanoutExchange);
      }

    }

  • 在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2

    public class ExchangeConsumer {

      @RabbitListener(queues = "order-queue")
      public void orderMes(String msg) {
          System.out.println("消费了" + msg);
      }
    
      @RabbitListener(queues = "repository-queue")
      public void repositoryMes(String msg) {
          System.out.println("消费了" + msg);
      }

    }

  • 在publisher服务发送消息到fanoutExchange

    • @Autowired
      private RabbitTemplate rabbitTemplate;
        
      @Test
      public void testExchangeQueue() {
          String exchangeName = "shop-fanout";
          String msg = "发送消息到广播路由";
          rabbitTemplate.convertAndSend(exchangeName,"",msg);
    
    ##### DirectExchange
    
    Direct Exchange 会将收到的消息根据规则路由到指定的Queue,因此称为路由模式
  • 没一个Queue都与Exchang设置一个BindingKey
  • 发布者发布消息时,指定消息的RoutingKey
  • Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
  1. 使用@RabbitListener 注解声明 Queue、Exchange、RoutingKey

        @RabbitListener(bindings = @QueueBinding(
                value = @Queue(name = "direct1.queue"),
                exchange = @Exchange(name = "blog-qh.direct",type = "direct"),
                key = {"blue"}
        ))
        public void DirectMsg1(String msg) {  // 监听direct1.queue队列
            System.out.println("DirectMsg1消费了" + msg);
        }
    
        @RabbitListener(bindings = @QueueBinding(
                value = @Queue(name = "direct2.queue"),
                exchange = @Exchange(name = "blog-qh.direct",type = "direct"),
                key = {"blue","red"}
        ))
        public void DirectMsg2(String msg) {  // 监听direct2.queue队列
            System.out.println("DirectMsg2消费了" + msg);
        }
  2. 发送消息到directExchange路由

        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        @Test
        public void testDirectExchagneQueue() {
            String exchangeName = "blog-qh.direct";
            String message = "Send Message to DirectExchange";
            rabbitTemplate.convertAndSend(exchangeName,"blue",message); // 第二个参数指定消息的RoutingKey
        }
    TopicExchange

    TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以 . 分割。

    Queue与Exchange指定BindingKey时可以使用通配符:

  • :代指0个或多个单词

  • *:代指一个单词
  1. 使用@RabbitListener声明queu、exchange,RoutingKey

        @RabbitListener(bindings = @QueueBinding(
                value = @Queue("topic1.queue"),
                exchange = @Exchange(name = "blog-qh.topic",type = "topic"),
                key = {"#.news"}
        ))
        public void testTopicExchangeMes1(String msg) {
            System.out.println("testTopicExchangeMes1消费了 " + msg);
   }
   @RabbitListener(bindings = @QueueBinding(
           value = @Queue("topic2.queue"),
           exchange = @Exchange(name = "blog-qh.topic",type = "topic"),
           key = {"china.#"}
   ))
   public void testTopicExchangeMes2(String msg) {
       System.out.println("testTopicExchangeMes2消费了 " + msg);
   }

2. 发送消息
   @Autowired
   private RabbitTemplate rabbitTemplate;
   @Test
   public void testTopicExchangeQueue() {
       String exchangeName = "blog-qh.topic";
       String msg = "Send Message to Topic";

// rabbitTemplate.convertAndSend(exchangeName,"china.news",msg);

       rabbitTemplate.convertAndSend(exchangeName,"USA.news",msg);

   }

## 消息转换器

Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。

如果要修改只需要定义一个MessageConverter 类型的Bean即可。推荐用JSON方式序列化,步骤如下:

1. 引入依赖
       <dependency>
           <groupId>com.fasterxml.jackson.core</groupId>
           <artifactId>jackson-databind</artifactId>
       </dependency>

2. 定义MessageConverter 
   @Bean
   public MessageConverter JsonMessageConverter() {
       return new Jackson2JsonMessageConverter();
   }

3. 例:传入一个map就可以得到一个json格式的消息。



# Elasticsearch

elasticsearch是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。

elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域。

elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。

- 文档:一条数据就是一个文档,es中是Json格式
- 字段:Json文档中的字段
- 索引:同类型文档的集合
- 映射:索引中文档的约束,比如字段名称、类型

elasticsearch与数据库的关系:

- 数据库负责事务类型操作
- elasticsearch负责海量数据的搜索、分析、计算

## 部署单点elasticsearch:

创建一个网络:`docker network create es-net`

拉取镜像:`docker pull elasticsearch:tag`

运行:

docker run -d \

--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \

elasticsearch:7.12.1


- `-e "cluster.name=es-docker-cluster"`:设置集群名称
- `-e "http.host=0.0.0.0"`:监听的地址,可以外网访问
- `-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"`:内存大小
- `-e "discovery.type=single-node"`:非集群模式
- `-v es-data:/usr/share/elasticsearch/data`:挂载逻辑卷,绑定es的数据目录
- `-v es-logs:/usr/share/elasticsearch/logs`:挂载逻辑卷,绑定es的日志目录
- `-v es-plugins:/usr/share/elasticsearch/plugins`:挂载逻辑卷,绑定es的插件目录
- `--privileged`:授予逻辑卷访问权
- `--network es-net` :加入一个名为es-net的网络中
- `-p 9200:9200`:端口映射配置

在浏览器中输入:http://你的主机ip:9200 即可看到elasticsearch的响应结果

## 部署kibana

kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。

拉取镜像:`docker pull kibana:tag `

部署容器(运行):

docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1


- `--network es-net` :加入一个名为es-net的网络中,与elasticsearch在同一个网络中
- `-e ELASTICSEARCH_HOSTS=http://es:9200"`:设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch
- `-p 5601:5601`:端口映射配置

查看日志,当查看到日志,说明成功:`docker logs kibana -f`

此时,在浏览器输入地址访问:http://192.168.204.139:5601,即可看到结果。

kibana中提供了一个DevTools界面,这个界面中可以编写DSL来操作elasticsearch。并且对DSL语句有自动补全功能。

## 安装IK分词器

离线安装:下载好ik分词器的压缩包,解压后放在es的数据卷plugins目录下。

重启es容器:`docker restart es`,查看日志:`docker logs es -f`

测试:

IK分词器包含两种模式:

- `ik_smart`:最少分片 细粒度
- `ik_max_word`:最大分片 粗粒度

### 扩展词词典

随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。比如:“奥力给”,“传智播客” 等。所以我们的词汇也需要不断的更新,IK分词器提供了扩展词汇的功能。

例如新增一些词语,或者屏蔽一些词语。

1. 打开ik分词器文件目录:`/config/IKAnalyzer.cfg.xml`

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>

   <comment>IK Analyzer 扩展配置</comment>
   <!--用户可以在这里配置自己的扩展字典 -->
   <entry key="ext_dict">ext.dic</entry>
    <!--用户可以在这里配置自己的扩展停止词字典-->
   <entry key="ext_stopwords">ext.stopwords.dic</entry>
   <!--用户可以在这里配置远程扩展字典 -->
   <!-- <entry key="remote_ext_dict">words_location</entry> -->
   <!--用户可以在这里配置远程扩展停止词字典-->
   <!-- <entry key="remote_ext_stopwords">words_location</entry> -->

</properties>


`ext_stopwords`这个文件已存在,我们直接打开添加词语即可。

新建`ext.dic`文件,添加词语即可。

重启es:`docker restart es`