微服务框架入门

微服务

微服务是一种经过良好架构设计的分布式架构方案,微服务架构特征:

  • 单一职责:微服务拆分粒度小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
  • 面向服务:微服务对外暴露业务接口
  • 自治:团队独立、技术独立、数据独立、部署独立

微服务技术对比

微服务这种方案需要技术框架来落地,全球的互联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是SpringCloud和阿里巴巴的Dubbo。


DubboSpringCloudSpringCloudAlibaba
注册中心zookeeper、RedisEureka、ConsulNacos、Eureka
服务远程调佣Dubbo协议Feign(HTTP协议)Dubbo、Feign
配置中心SpringCloudConfigSpringCloudConfig、Nacos
服务网关SpringCloudGateway、ZuulSpringCloudGateway、Zuul
服务监控和保护dubbo-admin,功能弱HystixSentinel

在这里插入图片描述

SpringCloud

  • SpringCloud是目前国内使用最广泛的微服务框架。
  • 官网
  • SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱既用体验
    在这里插入图片描述
  • SpringCloud与SpringBoot的版本兼容关系如下:
    在这里插入图片描述

服务拆分及远程调用

服务拆分

服务拆分注意事项:

  • 不同微服务,不要重复开发相同业务
  • 微服务数据独立,不要访问其他微服务的数据库
  • 微服务可以将自己的业务暴露为接口,供其他微服务调用

创建工程

  • 创建数据库:

    CREATE DATABASE cloud-order;
    USE `cloud-order`;
    
    
    DROP TABLE IF EXISTS `tb_order`;
    
    CREATE TABLE `tb_order` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单id',
      `user_id` bigint(20) NOT NULL COMMENT '用户id',
      `name` varchar(100) DEFAULT NULL COMMENT '商品名称',
      `price` bigint(20) NOT NULL COMMENT '商品价格',
      `num` int(10) DEFAULT '0' COMMENT '商品数量',
      PRIMARY KEY (`id`) USING BTREE,
      UNIQUE KEY `username` (`name`) USING BTREE
    ) ENGINE=InnoDB AUTO_INCREMENT=109 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
    
    
    insert  into `tb_order`(`id`,`user_id`,`name`,`price`,`num`) values (101,1,'Apple 苹果 iPhone 12 ',699900,1),(102,2,'雅迪 yadea 新国标电动车',209900,1),(103,3,'骆驼(CAMEL)休闲运动鞋女',43900,1),(104,4,'小米10 双模5G 骁龙865',359900,1),(105,5,'OPPO Reno3 Pro 双模5G 视频双防抖',299900,1),(106,6,'美的(Midea) 新能效 冷静星II ',544900,1),(107,2,'西昊/SIHOO 人体工学电脑椅子',79900,1),(108,3,'梵班(FAMDBANN)休闲男鞋',31900,1);
    
    
    
    
    
    
    
    
    CREATE DATABASE `cloud-user` ;
    
    USE `cloud-user`;
    
    
    
    DROP TABLE IF EXISTS `tb_user`;
    
    CREATE TABLE `tb_user` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `username` varchar(100) DEFAULT NULL COMMENT '收件人',
      `address` varchar(255) DEFAULT NULL COMMENT '地址',
      PRIMARY KEY (`id`) USING BTREE,
      UNIQUE KEY `username` (`username`) USING BTREE
    ) ENGINE=InnoDB AUTO_INCREMENT=109 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
    
    
    
    insert  into `tb_user`(`id`,`username`,`address`) values (1,'柳岩','湖南省衡阳市'),(2,'文二狗','陕西省西安市'),(3,'华沉鱼','湖北省十堰市'),(4,'张必沉','天津市'),(5,'郑爽爽','辽宁省沈阳市大东区'),(6,'范兵兵','山东省青岛市');
    
  • 导入项目

远程调用

  • 注册RestTemplate:在order-service的OrderApplication中注册RestTemplate 配置类

    /**
     * 创建RestTemplate并注入Spring容器
     * @return
     */
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
    
  • 注入RestTemplate

    @Autowired
    private RestTemplate restTemplate;
    
  • 发送请求

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.利用RestTemplate发起HTTP请求,查询用户
        // 2.1.url路径
        String url = "http://localhost:8081/user/"+order.getUserId();
        // 2.2.发送HTTP请求,实现远程调用
        User user = restTemplate.getForObject(url, User.class);
        
        // 3. 封装user到Order
        order.setUser(user);
        // 4.返回
        return order;
    }
    

提供者与消费者

  • 服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)
  • 服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)

Eureka注册中心

服务调用出现的问题

  • 服务消费者该如何获取提供者的地址信息?
    • 服务提供者启动时向Eureka注册自己的信息
    • Eureka保持这些信息
    • 消费者根据服务名称向Eureka拉取提供者信息
  • 如果有多个服务提供者,消费者该如何选择?
    • 服务消费者利用负载均衡算法,从服务列表中挑选一个
  • 消费者如何得知服务提供者的健康状态?
    • 服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
    • Eureka会更新记录服务列表信息,心跳不正常会被剔除
    • 消费者就可以拉取到最新的信息

Eureka的作用

在这里插入图片描述
在Eureka架构中,微服务角色有两类:

  • EurekaServer:服务端,注册中心
    • 记录服务信息
    • 心跳监控
  • EurekaClient:客户端
    • Provider:服务提供者
      • 注册自己的信息到EurekaServer
      • 每隔30秒向EurekaServer发送心跳
    • consumer:服务消费者
      • 根据服务名称从EurekaServer拉取服务列表
      • 基于服务列表做负载均衡,选中一个微服务后发起远程调用

搭建EurekaServer

搭建EurekaServer服务步骤如下:

  • 创建项目,引入spring-cloud-starter-netflix-eureka-server的依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    
  • 编写启动类,添加 @EnableEurekaServer 注解

  • 编辑 application.yml 文件,添加下面配置

    server:
      port: 10086 #服务端口
    spring:
      application:
        name: eurekaserver    #服务名称
    eureka:
      client:
        service-url:  #Eureka的地址信息
          defaultZone: http://127.0.0.1:10086/eureka
    

服务注册

将user-service服务注册到EurekaServer

  • 在user-server项目引入 spring-cloud-starter-netflix-eureka-client 的依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
  • 在application.yml文件,编写下面配置

    server:
      port: 10086 #服务端口
    spring:
      application:
        name: userservice    #服务名称
    eureka:
      client:
        service-url:  #Eureka的地址信息
          defaultZone: http://127.0.0.1:10086/eureka
    

服务发现

服务拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡

  • 修改OrderService的代码,修改访问的路径,用服务名代替ip、端口:
    在这里插入图片描述

  • 在order-service项目的启动类OrderApplication中的RestTemplate添加 负载均衡 注解: @LoadBalanced

Ribbon负载均衡

负载均衡策略

Ribbon的负载均衡规则是一个叫做IRule的接口来定义的,每一个子接口都是一种规则:
在这里插入图片描述


负载均衡策略:

内置负载均衡规则类规则描述
RoundRobinRule简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则
AvailabilityFilteringRule对以下两种服务器进行忽略:
1、在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加
2、并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AVailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户的<clientName>,<clientConfigNameSpace>,ActiveConnectionsLimit属性进行配置
WeightedResponseTimeRule为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择一个服务器,这个权重值会影响服务器的选择。
ZoneAvoidanceRule以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。然后再对Zone内的多个服务做轮询
BestAvailableRule忽略哪些短路的服务器,并选择并发数较低的服务器
RandomRule随机选择一个可用的服务器
RetryRule重试机制的选择逻辑

修改负载均衡策略

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

  • 代码方式(全局):在order-service中的OrderApplication类(配置类)中,定义一个新的IRule:

    @Bean
    public IRule randomRule(){
        return new RandomRule();
    }
    
  • 配置文件方式:在order-service的application.yml

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

饥饿加载

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

ribbon:
  eager-load:
    enabled: true #开启饥饿加载
    clients: userservice #指定对userservice这个服务饥饿加载,这里可以添加多个服务(采用数组的方式)

Nacos注册中心

下载安装

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

Nacos入门

  • 在cloud-demo父工程中添加 spring-cloud-alibaba 的管理依赖

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-dependencies</artifactId>
        <version>2.2.5.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
    
  • 注释掉order-service和user-service中原有的Eureka依赖

  • 添加nacos的客户端依赖

    <!--nacos客户端-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    
  • 修改user-service&order-service中的application.yml,注释Eureka地址,添加nacos地址

    spring:
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848 #nacos 服务端地址
    
  • 启动测试
    在这里插入图片描述

Nacos服务分级存储模型

在这里插入图片描述

  • 服务调用尽可能选择本地集群的服务,跨集群调用延迟较高
  • 本地集群不可访问时,再去访问其它集群
    在这里插入图片描述

服务集群属性

  • 修改application.yml,添加如下内容

    spring:
      cloud:
        nacos:
          discovery:
            cluster-name: GZ  #配置集群名称,也就是机房位置,例如:GZ,贵州
    
  • 重启服务查看Nacos
    在这里插入图片描述

NacosRule负载均衡

  • 在order-service中设置负载均衡的IRule为NacosRule,这个规则优先会寻找与自己同集群的服务

    userservice:
     ribbon:
    	NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
    

根据权重负载均衡

  • 在Nacos设置实例的权重值,首先选中实例后面的编辑按钮
    在这里插入图片描述
  • 将群众设置为0.1(0-1之间),测试可以发现8081被访问到的频率大大降低
    在这里插入图片描述

环境隔离(namespace)

Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离
在这里插入图片描述

  • 在Nacos控制台可以创建namespace,用来隔离不同环境
    在这里插入图片描述

  • 然后填写一个新的命令空间信息
    在这里插入图片描述

  • 保存后会在控制台看到这个命令空间的id:
    在这里插入图片描述

  • 修改order-service的application.yml,添加namespace

    spring:
      cloud:
        nacos:
          discovery:
            namespace: ae6c2eb8-c143-4854-ad08-7e2c2c744290 #命名空间,填Id
    
  • 不同namespace下的服务不可见

  • 配置非临时实例

    spring:
      cloud:
        nacos:
          discovery:
            ephemeral: false  #是否是临时实例
    
    

HTTP客户端Feign

Feign代替RestTemplate

Feign是一个声明式的HTTP客户端,官网地址
其作用就是帮助我们优雅的实现HTTP请求的发送,解决上面提到的问题


RestTemplate方式调用存在的问题

String url = "http://userservice/user/" + order.getUserId():
User user = restTemplate.getForObject(url,User.class);
  • 代码可读性差,编程体验不统一
  • 参数复杂URL难以维护

定义和使用Feign客户端

  • 引入依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
  • 在order-service的启动类加 @EnableFeignClients 注解开启Feign的功能:

  • 编写Feign客户端:

    @FeignClient("userservice")    //指定服务名称
    public interface UserClient {
        @GetMapping("/user/{id}")
        User findByID( @PathVariable("id") Long id );
    }
    

    主要是基于SpringMVC的注解来声明远程调用的信息,比如:

    • 服务名称:userservice
    • 请求方式:GET
    • 请求路径:/user/{id}
    • 请求参数:Long id
    • 返回值类型:User
  • 使用Feign客户端

    @Autowired
    private UserClient userClient;      //注入userClient
    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.用Feign远程调用
        User user = userClient.findByID(order.getUserId());
        // 3. 封装user到Order
        order.setUser(user);
        // 4.返回
        return order;
    }
    

自定义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的重试

配置Feign日志有两种方式:
方式一:配置文件方式

  • 全局生效

    feign:
      client:
        config: 
          default:
            loggerLevel: FULL
    
  • 局部生效

    feign:
      client:
        config: 
          userservice:		#服务名称
            loggerLevel: FULL
    

方式二:java代码方式,需要先声明一个Bean:

public class DefaultFeignConfiguration {
    @Bean
    public Logger.Level logLevel(){
        return Logger.Level.BASIC;
    }
}
  • 全局配置,则把他放到@EnableFeignClients这个注解中:

    @EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
    
  • 局部配置,则把它放到@FeignClient这个注解中:

    @FeignClient(value = "userservice",configuration = DefaultFeignConfiguration.class)
    

Feign的性能优化

Feign底层的客户端实现:

  • UTLConnection:默认实现,不支持连接池
  • Apache HTTPClient:支持连接池
  • OKHTTP:支持连接池

因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection


Feign添加HTTPClient的支持:

  • 引入依赖

    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-httpclient</artifactId>
    </dependency>
    
  • 配置连接池

    feign:
      httpclient:
        enabled: true # 开启FeignHttpClient的支持
        max-connections: 200 # 最大连接数
        max-connections-per-route: 50 # 每个路径的最大连接数
    

Feign的最佳实践

方式一(继承):给消费者的FeignClient和提供者的Controller定义统一的父接口作为标准

  • 服务紧耦合
  • 父接口参数列表映射不会被继承

方式二(抽取):将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用


抽取FeignClient

  • 首先创建一个module,命名为feign-api,然后引入feign的starter依赖

  • 将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
    在这里插入图片描述

  • 在order-service中引入feign-api的依赖

    <!--引入feign的统一api-->
    <dependency>
        <groupId>com.xiaowu.demo</groupId>
        <artifactId>feign-api</artifactId>
        <version>1.0</version>
    </dependency>
    
  • 修改order-service中所有上述三个组件有关的import部分,改成导入feign-api中的包

  • 重启测试


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

  • 方式一:指定FeignClient所在包

    @EnableFeignClients(basePackages = "com.xiaowu.feign.clients")
    
  • 方式二:指定FeignClient字节码

    @EnableFeignClients(clients = {UserClient.class})
    

统一网关Gateway

网关作用介绍

网关的功能:

  • 身份认证和权限校验
  • 服务路由、负载均衡
  • 请求限流
    在这里插入图片描述

网关的技术实现
在SpringCloud中网关的实现包括两种:

  • gateway
  • zuul

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

搭建网关服务

  • 创建一个新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖

    <!--nacos服务注册发现依赖-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--网关gateway依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    
  • 编写主方法

    @SpringBootApplication
    public class GatewayApplication {
        public static void main( String[] args ) {
            SpringApplication.run(GatewayApplication.class,args);
            
        }
    }
    
  • 编写路由配置及nacos地址

    server:
      port: 10010
    spring:
      application:
        name: gateway
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848 #nacos地址
        gateway:
          routes:
            - id: user-service # 路由标识,必须唯一
              uri: lb://userservice #路由的目标地址
              predicates: # 路由断言,判断请求是否符合规则
                - Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合
                  
            - id: order-service 
              uri: lb://orderservice
              predicates:
                - Path=/order/**
    
  • 启动测试
    在这里插入图片描述


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

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

路由断言工厂(Route Predicate Factory)

  • 我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件
  • 例如Path=/user/**是按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的
  • 像这样的断言工厂在SpringCloudGateway还有十几个

Spring提供了11种基本的Predicate工厂:

名称说明示例
After在某个时间点后的请求- After=2023-01-20T17:12:47.789-07:00[America/Denver]
Before是某个时间点之前的请求- Before=2023-01-20T17:12:47.789-07:00[Asia/Shanghai]
Between是某两个时间点之前的请求- Between=2023-01-20T17:12:47.789-07:00[Asia/Shanghai],2025-01-20T17:12:47.789-07:00[Asia/Shanghai]
Cookie请求必须包含某些Cookie- Cooke=chocolate,ch.p
Header请求必须包含某些Header- Header=X-Request,\d+
Host请求必须是访问某个host(域名)- Host = **.somehost,**.anotherhost.ort
Method请求方式必须是指定方式- Method=GET,POST
Path请求路径必须符合指定规则- Path=/red/{segment},/bule/**
Query请求参数必须包含指定参数- Query=name,jack或者Query=name
RemoteAddr请求者的ip必须是指定范围- RemoteAdd=192.168.1.1/24
Weight权重处理

路由过滤器 GatewayFilter

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


Spring提供了31种不同的路由过滤器工厂。例如:

名称说明
AddRequestHeader给当前请求添加一个请求头
RemoveRequestHeader移除请求中的一个请求头
AddResponseHeader给响应结果中添加一个响应头
RemoveResponseHeader从响应结果中移除一个响应头
RequestRateLimiter限制请求流量
……

给所有进入userservice的请求添加一个请求头
给所有进入userservice的请求添加一个请求头:Truth=xiaowu
实现方式:在gateway中修改application.yml文件,给userservice的路由添加过滤器:

server:
  port: 10010
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #nacos地址
    gateway:
      routes:
        - id: user-service # 路由标识,必须唯一
          uri: lb://userservice #路由的目标地址
          predicates: # 路由断言,判断请求是否符合规则
            - Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合
          filters: #过滤器
            - AddRequestHeader= Truth, xiaowu
              
        - id: order-service
          uri: lb://orderservice
          predicates:
            - Path=/order/**

默认过滤器: 如果要对所有的路由都生效,则可以将过期工厂写到default下,如

server:
  port: 10010
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #nacos地址
    gateway:
      routes:
        - id: user-service # 路由标识,必须唯一
          uri: lb://userservice #路由的目标地址
          predicates: # 路由断言,判断请求是否符合规则
            - Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合
          

        - id: order-service
          uri: lb://orderservice
          predicates:
            - Path=/order/**
      default-filters: # 默认过滤器,会对所有的路由请求都生效
          - AddRequestHeader= Truth, xiaowu

全局过滤器 GlobalFiltering

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


定义方式是实现GlobalFiltering接口

public interface GlobalFilter{
	/**
	 * 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
	 * @param exchange 请求上文,里面可以获取Request、Response等信息
	 * @param chain 用来把请求委托给下一个过滤器
	 * @return {@code Mana<Void>} 返回表示当前过滤器业务结束
	 * /
	Mono<Void> filter(ServerWebExchange exchange,GatewayFilterChain chain);
}

定义全局过滤器,拦截并判断用户身份
需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:

  • 参数中是否有authorization

  • authorization参数值是否为admin

  • 如果同时满足则放行,否则拦截

    @Order(-1)      //优先级
    @Component
    public class AutherizeFilter implements GlobalFilter {
        @Override
        public Mono<Void> filter( ServerWebExchange exchange, GatewayFilterChain chain ) {
            // 1.获取请求参数
            ServerHttpRequest request = exchange.getRequest();
            MultiValueMap<String, String> queryParams = request.getQueryParams();
    
            // 2.获取参数中的authorization参数
            String auth = queryParams.getFirst("authorization");
            
            // 3.判断参数是否等于 admin
            if("admin".equals(auth)){
                // 4.是,放行
                return chain.filter(exchange);
            }
            
            // 5.否,拦截
            // 5.1.设置状态码
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            
            // 5.2.拦截请求
            return exchange.getResponse().setComplete();
        }
    }
    

过滤器执行顺序

请求进入网关会喷到三类过滤器:当前路由的过滤器、DefaultFiltering、GlobalFiltering
请求路由后,会将当前路由过滤器和DefaultFiltering、GlobalFiltering,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器


  • 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。
  • GlobalFiltering通过实现Ordered接口,或者添加@Order注解来指定Order值,由我们自己指定
  • 路由过滤器和DefaultFiltering的Order由Spring指定,默认是按照声明顺序从1递增
  • 当过滤器的Order值一样时,会按照DefaultFilter>路由过滤器>GlobalFilter的顺序执行

跨域问题处理

跨域:域名不一致就是跨域,主要包括:

  • 域名不同:www.taobao.com和www.taobao.org和www.jd.com和miaosha.jd.com
  • 域名相同:端口不同,localhost:8080和localhost8081

跨域问题:浏览器禁止请求的发起者与服务端发生跨域Ajax请求,请求被浏览器拦截的问题
解决方案:CORS


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

server:
  port: 10010
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #nacos地址
    gateway:
      routes:
        - id: user-service # 路由标识,必须唯一
          uri: lb://userservice #路由的目标地址
          predicates: # 路由断言,判断请求是否符合规则
            - Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合


        - id: order-service
          uri: lb://orderservice
          predicates:
            - Path=/order/**
      default-filters: # 默认过滤器,会对所有的路由请求都生效
        - AddRequestHeader= Truth, xiaowu

      globalcors: # 全局的跨域处理
        add-to-simple-url-handler-mapping: true # 解决options请求拦截问题
        corsConfigurations:
          '[/**]':
            allowedOrigins:  # 允许哪些网站跨域请求
              - "http://localhost:8090"
              - "http:127.0.0.1"
            allowedMethods: # 允许跨域的Ajax请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" # 允许请求中携带头信息
            allowCredentials: true # 是否允许携带Cookie
            maxAge: 360000 # 这次跨域检查的有效期

MQ

初始MQ

同步通讯和异步通讯
在这里插入图片描述


同步调用的问题
微服务间基于Feign的调用就属于同步方式,存在一些问题。

  • 耦合度高: 每次加入新的需求,都要修改原来的代码
  • 性能下降: 调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和
  • 浪费资源: 调用链中的每个服务在等待响应过程中,不能释放请求占用资源,高并发场景下会极度浪费系统资源
  • 级联失败: 如果服务提供者出现问题,所有调用方都会跟着出问题,如同多米诺骨牌一样,迅速导致整个微服务群故障

异步调用方案
异步调用常见实现就是事件驱动模式


事件驱动优势

  • 优势一:服务解耦
  • 优势二:性能提升,吞吐量提高
  • 优势三:服务没有强依赖,不担心级联失败问题
  • 优势四:流量削峰

异步通信的缺点

  • 依赖于Broker的可靠性、安全性、吞吐能力
  • 架构复杂了,业务没有明显的流程线,不好追踪管理

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

RabbitMQActiveMQRocketMQKafka
公司/社区RabbitApache阿里Apache
开发语言ErlangJavaJavaScala&Java
协议支持AMQP,XMPP,SMTP,STOMPOpenWire,STOMP,REST,XMPP,AMQP自定义协议自定义协议
单机吞吐量一般非常高
消息延迟微妙级毫秒级毫秒级毫秒以内
消息可靠性一般一般

RabbitMQ快速入门

RabbitMQ概述和安装

RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网


安装

  • 将下载的安装包上传到Linux

  • 安装docker命令: yum -y install docker

  • 启动到dockker:systemctl start docker

  • 加载镜像RabbitMQ: docker load -i mq.tar

  • 执行下面命令来运行MQ容器:

    • RABBITMQ_DEFAULT_USE:用户名
    • RABBITMQ_DEFAULT_PASS:密码
    • name:名字
    • hostname:主机名
    • p:端口映射
      • 1672:管理端口
      • 5672:消息通信端口
      • rabbitmq:镜像名称
    docker run -e RABBITMQ_DEFAULT_USER=xiaowu -e RABBITMQ_DEFAULT_PASS=123456 --name mq --hostname mq1 -p 15672:15672 -p 5672:5672 -d rabbitmq:3-management
    
  • 查看时候运行成功:docker ps
    在这里插入图片描述

  • 浏览器访问:http://主机地址:15672/
    在这里插入图片描述

  • 登录
    在这里插入图片描述


RabbitMQ中的几个概念:

  • channel:操作MQ的工具
  • exchange:路由消息队列中
  • queue:缓存消息
  • virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组

消息模型介绍

MQ的官方文档中给出了5个MQ的Demo示例,对应了几种不同的用法:

  • 基本消息队列(BasicQueue)
  • 工作消息队列(WorkQueue)
  • 发布订阅(Publish、Subscribe),又根据交换机类型不同分为三种:
    • Fanout Exchange:广播
    • Direct Exchange:路由
    • Topic Exchange:主题

HelloWorld案例

官方的HelloWorld是基于最基础的消息队列模型来实现的,只包括三个角色:

  • publisher:消息发布者,将消息发送到队列queue
  • queue:消息队列,负责接受并缓存消息
  • consumer:订阅队列,处理队列中的消息
    在这里插入图片描述

实现步骤:

  • 导入工程
  • 运行publisher服务中的测试类PublisherTest中的测试方法testSendMessage()
  • 查看RabbitMQ控制台消息
  • 启动consumer服务,查看是否能接收消息

基本消息队列的消息发送流程:

  • 建立connection
  • 创建channel
  • 利用channel声明队列
  • 利用channel向队列发送消息

基本消息队列的消息接收流程:

  • 建立connection
  • 创建channel
  • 利用channel声明队列
  • 定义consumer的消费行为handleDelivery()
  • 利用channel将消费者与队列绑定

SpringAMQP

什么是SpringAMQP
在这里插入图片描述

Basic Queue简单队列模型

  • 在父工程中引入spring-amqp的依赖
    • 因为publisher和consumer服务都需要amqp依赖,因此这里把依赖直接放到父工程mq-demo中
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
        <version>3.1.1</version>
    </dependency>
    

  • 在publisher服务中利用RabbitTemplate发送消息到simple.queue这个队列
    • 在publisher服务中编写application.yml,添加mq连接信息

      spring:
        rabbitmq:
          host: 192.168.2.102 #主机地址
          port: 5672 #端口
          virtual-host: / #虚拟主机
          username: xiaowu  #用户名
          password: 123456  #密码
      
    • 在publisher服务中新建一个测试类,编写测试方法

      @RunWith(SpringRunner.class)			//声明测试类需要注入容器
      @SpringBootTest		//单元测试
      public class SpringAmqpTest {
          @Autowired
          private RabbitTemplate rabbitTemplate;
      
          @Test
          public void testSendMessageSimpleQueue( ) {
              String queueName = "simple.queue";
              String message = "Hello,spring amqp";
              rabbitTemplate.convertAndSend(queueName,message);
          }
      }
      

  • 在consumer服务中编写消费逻辑,绑定simple.queue这个队列
    • 在consumer服务中编写application.yml,添加mq连接信息:
      spring:
        rabbitmq:
          host: 192.168.2.102 #主机地址
          port: 5672 #端口
          virtual-host: / #虚拟主机
          username: xiaowu  #用户名
          password: 123456  #密码
      
    • 在consumer服务中新建一个类,编写消费逻辑
      @Component      //实现Bean注入
      public class SpringRabbitListener {
      
          @RabbitListener(queues = "simple.queue")        //监听消息,queues:消息的名称,可以是多个
          public void listenSimpleQueue(String msg){
              System.out.println("消费者接收到simple.queue消息:"+msg);
      
          }
      }
      

Work Queue工作队列模型

在这里插入图片描述
模拟WorkQueue,实现一个队列绑定多个消费者
基本思路如下:

  • 在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue
  • 在consumer服务中定义两个消息监听者,都监听simple.queue队列
  • 消费者1秒处理50条消息,消费者2每秒处理10条消息

通过测试发现,消费者1处理完后需要等待消费者2处理


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

spring:
  rabbitmq:
    host: 192.168.2.102 #主机地址
    port: 5672 #端口
    virtual-host: / #虚拟主机
    username: xiaowu  #用户名
    password: 123456  #密码
    listener:
      simple:
        prefetch: 1 #每次只能取得到一条消息,处理完成ACR之后才能取到下一个消息

发布、订阅模型-Fanout

发布订阅模式与之前案例的区别就是允许将同一消息发送给多个消费者。实现方式是加入了Exchange(交换机)
在这里插入图片描述

Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue
实现思路如下:

  • 在consumer服务中,利用代码声明队列,交换机,并将两则绑定

    @Configuration
    public class FanoutConfig {
        // 声明xiaowu.fanout 交换机
        @Bean
        public FanoutExchange fanoutExchange(){
            return new FanoutExchange("xiaowu.fanout");
        }
        
        // 声明队列fanout.queue1
        @Bean
        public Queue fanoutQueue1(){
            return new Queue("fanout.queue1");
        }
        
        // 绑定队列1到交换机
        @Bean
        public Binding fanoutBinding1(Queue fanoutQueue1,FanoutExchange fanoutExchange){
            return BindingBuilder
                    .bind(fanoutQueue1)
                    .to(fanoutExchange);
        }
        
        // 声明队列fanout.queue2
        @Bean
        public Queue fanoutQueue2(){
            return new Queue("fanout.queue2");
        }
    
        // 绑定队列2到交换机
        @Bean
        public Binding fanoutBinding2(Queue fanoutQueue2,FanoutExchange fanoutExchange){
            return BindingBuilder
                    .bind(fanoutQueue2)
                    .to(fanoutExchange);
        }
    }
    
    
  • 在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2

    @RabbitListener(queues = "fanout.queue1")        //监听消息,queues:消息的名称,可以是多个
    public void listenFanoutQueue1(String msg){
        System.out.println("消费者接收到fanout.queue1消息:"+msg);
    }
    @RabbitListener(queues = "fanout.queue2")        //监听消息,queues:消息的名称,可以是多个
    public void listenFanoutQueue2(String msg){
        System.out.println("消费者接收到fanout.queue2消息:"+msg);
    }
    
  • 在publisher中编写测试方法,向xiaowu.fanout发送消息

    @Test
    public void testSendFanoutExchange(){
        // 交换机名称
        String exchangeName = "xiaowu.fanout";
        //消息
        String message = "hello,every one!";
        rabbitTemplate.convertAndSend(exchangeName,"",message);
    }
    

发布、订阅模型-Direct

Direct Exchange会将接收到的消息根据规则路由到指定的Queue,因此成为路由模式(routes)
在这里插入图片描述

  • 每一个Queue都与Exchange设置一个BindingKey
  • 发布者发送消息时,指定消息的RoutingKey
  • Exchange将消息路由到BindingKey与消息RoutingKey一致的队列

实现思路如下:

  • 利用@RabbitListener声明Exchange、Queue、RoutingKey

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1"),     //队列名称
            exchange = @Exchange(name = "xiaowu.direct",type = ExchangeTypes.DIRECT),   //name:交换机名称    type:交换机类型
            key = {"red","blue"}        //BindingKey
    ))
    
  • 在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1"),     //队列名称
            exchange = @Exchange(name = "xiaowu.direct",type = ExchangeTypes.DIRECT),   //name:交换机名称    type:交换机类型
            key = {"red","blue"}        //BindingKey
    ))
    public void listentDirectQueue1(String msg){
        System.out.println("消费者接收到direct.queue1消息:"+msg);
    }
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2"),     //队列名称
            exchange = @Exchange(name = "xiaowu.direct",type = ExchangeTypes.DIRECT),   //name:交换机名称    type:交换机类型
            key = {"red","yellow"}        //BindingKey
    ))
    public void listentDirectQueue2(String msg){
        System.out.println("消费者接收到direct.queue2消息:"+msg);
    
    
  • 在publisher中编写测试方法,向xiaowu.direct发送消息

    @Test
    public void testSendDirectExchange(){
        // 交换机名称
        String exchangeName = "xiaowu.direct";
        //消息
        String message = "hello,blue";
        rabbitTemplate.convertAndSend(exchangeName,"blue",message);
    }
    

发布、订阅模型-Topic

TopicExchange与DirectExchange类似,区别在于RoutingKey可以是多个单词的列表,并且以 . 分割
在这里插入图片描述

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

  • #:代表0个或多个单词
  • *:代表一个单词

实现思路如下:

  • 利用@RabbitListener声明Exchange、Queue、RoutingKey

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "Topic.queue1"),     //队列名称
            exchange = @Exchange(name = "xiaowu.Topic",type = ExchangeTypes.TOPIC),   //name:交换机名称    type:交换机类型
            key = "china.#"       //BindingKey
    ))
    
  • 在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "Topic.queue1"),     //队列名称
            exchange = @Exchange(name = "xiaowu.Topic",type = ExchangeTypes.TOPIC),   //name:交换机名称    type:交换机类型
            key = "china.#"       //BindingKey
    ))
    public void listentTopicQueue1(String msg){
        System.out.println("消费者接收到Topic.queue1消息:"+msg);
    }
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "Topic.queue2"),     //队列名称
            exchange = @Exchange(name = "xiaowu.Topic",type = ExchangeTypes.TOPIC),   //name:交换机名称    type:交换机类型
            key = "#.news"       //BindingKey
    ))
    public void listentTopicQueue2(String msg){
        System.out.println("消费者接收到Topic.queue2消息:"+msg);
    }
    
  • 在publisher中编写测试方法,向xiaowu.topic发送消息

    @Test
    public void testSendTopicExchange(){
        // 交换机名称
        String exchangeName = "xiaowu.Topic";
        //消息
        String message = "小吴在学Java,敲得都是Bug";
        rabbitTemplate.convertAndSend(exchangeName,"china.news",message);
    }
    

消息转换器

在SpringAMQP的发送方法中,接收消息的类型是Object,也就是说我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送

  • 在consumer中利用@Bean声明一个队列:

    @Bean
    public Queue objectQueue(){
        return new Queue("object.queue");
    }
    
  • 在publisher中发送消息测试

    @Test
    public void testSendObjectQueue(){
        Map<String,Object> msg = new HashMap<>();
        msg.put("name","小吴");
        msg.put("age",21);
        rabbitTemplate.convertAndSend("object.queue",msg);
    }
    

在这里插入图片描述


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

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

  • 在publish服务引入依赖

    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
        <version>2.15.2</version>
    </dependency>
    
  • 在publisher服务(配置类)声明MessageConverter

    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
    

在这里插入图片描述


接收JSON消息

  • 在consumer服务引入Jackson依赖

    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
        <version>2.15.2</version>
    </dependency>
    
  • 在consumer服务(配置类)定义MessageConverter

    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
    
  • 然后定义一个消费者,监听object.queue队列并消费消息

    @RabbitListener(queues = "object.queue")
    public void listenObjectQueue( Map<String,Object> msg ){
        System.out.println("接收到的object.queue:"+msg);
    }
    

Docker

初识Docker

什么是Docker

Docker是一个快速交付应用,运行应用的技术:

  • 可以将程序及其依赖、运行环境一起打包为一个镜像,可以迁移到任意Linux操作系统
  • 运行时利用沙箱机制形成隔离容器,各个应用互不干扰
  • 启动、移除都可以通过一行命令完成,方便快捷

项目部署问题
大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题:

  • 依赖关系复杂,容易出现兼容性问题
  • 开发、测试、生产环境差异

Docker如何解决依赖的兼容问题

  • 将应用的Libs(函数库)、Deps(依赖)
    • 配置与应用一起打包
  • 将每个应用发到一个隔离容器去运行,避免互相干扰

Docker如何解决不同系统环境的问题

  • Docker将用户程序与所需要调用的系统(比如CentOS)函数库一起打包
  • Docker运行到不同操作系统时,直接基于打包的库函数,借助于操作系统的Linux内核来运行

镜像和容器

  • 镜像(Image): Docker将应用程序及其所需要的依赖、函数库、环境、配置等文件打包在一起,称为镜像。
  • 容器(Container): 镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器做隔离,对外不可见。
    在这里插入图片描述

Docker和DockerHub

  • DockerHub:DockerHub是一个Docker镜像托管平台。这样的平台称为Docker Registry。
  • 国内也有类似于DockerHub的公开服务,比如网易云镜像服务、阿里云镜像库等。

Docker架构
Docker是一个CS架构的程序,由两部分组成:

  • 服务端(Server):Docker守护进程,负责处理Docker指令,管理镜像、容器等
  • 客户端(Client):通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。
    在这里插入图片描述

安装Docker

企业部署一般都是采用Linux操作系统,而其中CentOS发行版占比最多,因此我们在CentOS下安装Docker

  • 卸载:如果之前安装过旧版本的Docker,可以使用下面命令卸载:

    yum remove -y docker \
    				   docker-client \
    				   Docker-client-latest \
    				   docker-common \
    				   docker-latest \
    				   docker-latest-logrotate \
    				   docker-logrotate \
    				   docker-selinux \
    				   docker-engine-selinux \
    				   docker-engine \
    				   docker-ce
    
  • 安装yum工具

    yum install -y yum-utils \
    			device-mapper-persistent-data \
    			lvm2 --skip-broken
    
  • 更新本地镜像源:

    # 设置Docker镜像源
    yum-config-manager \
        --add-repo \
        https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
    sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo
    yum makecache fast
    
  • 安装Docker:yum install -y docker-ce

  • 启动Docker:启动Docker前要关闭防火墙或者放行端口

    • 关闭防火墙
      systemctl stop firewalld #关闭防火墙
      systemctl disable firewalld	#禁止关机重启
      
    • 启动Docker:
      systemctl start docker #启动
      systemctl stop docker #停止
      systemctl restart docker #重启
      
    • 查看版本:docker -v

配置镜像:阿里云文档

Docker基本操作

镜像操作

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

在这里插入图片描述

从DockerHub中拉取一个Nginx镜像并查看

  • 首先去镜像仓库搜索Nginx镜像
    在这里插入图片描述
  • 查看镜像:docker images
    在这里插入图片描述

利用docker save将Nginx镜像导出磁盘,然后再通过Load加载回来

  • 利用 docker xx --help 命令查看 docker savedocker load 语法
    在这里插入图片描述

容器操作

在这里插入图片描述


创建运行一个Nginx容器

  • docker hub查看Nginx的容器运行命令

    docker run --name mn  -p 80:80 -d nginx
    
    • doucker run:创建并运行一个容器
    • - -name:给容器起一个名字,比如mn
    • -p:将宿主机端口与容器端口映射,冒号左侧是宿主机端口,右侧是容器端口
    • -d:后台运行容器
    • Nginx:镜像名称

    在这里插入图片描述


进入Nginx容器,修改HTML文件内容,添加“小吴在敲Bug”

  • 进入容器

    docker exec -it mn bash
    
    • docker exec:进入容器内部,执行一个命令
    • -it:给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互
    • bash:进入容器后执行的命令,八十是一个Linux终端交换命令
  • 进入Nginx的HTML所在目录 /usr/shar/nginx/html

    cd /usr/shar/nginx/html
    
  • 修改index.html的内容

    sed -i 's#Welcome to nginx#小吴在敲Bug#g' index.html
    sed -i 's#<head>#<head><meta charset="utf-8">#g' index.html
    

数据卷

容器与数据耦合的问题
在这里插入图片描述


数据卷(volume): 是一个虚拟目录,指向宿主机文件系统的某个目录
数据卷操作的基本语法如下:

docker volume [COMMAND]

docker volume命令是数据操作,根据命令跟随的command来确定下一步操作:

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

创建一个数据卷,并查看数据卷在宿主机的目录位置

  • 创建数据卷

    docker volume create html
    
  • 查看所有数据

    docker volume ls
    
  • 查看数据卷详细信息卷

    docker volume inspect html
    

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

docker run \	#创建并运行容器
	--name mn \	# 给容器起名字
	-v html:/root/html \ 	#把html数据卷挂载到容器内的/root/html/这个目录中
	-p 80:80 \	#把宿主机的80端口映射到容器内的80端口
	nginx	#镜像名称

创建一个Nginx容器,修改容器内的html目录内的index.html内容

  • 创建容器并挂载数据卷到容器内的HTML目录

    # 如果容器运行时volume不存在,会自动创建出来
    docker run --name mn  -p 80:80 -v html:/usr/share/nginx/html -d nginx
    
  • 进入HTML数据卷所在位置,并修改HTML内容

    # 查看HTML数据卷的位置
    docker volume inspect html
    # 进入该目录
    cd /var/lib/docker/volumes/html/_data
    # 修改文件
    vi index.html
    

创建并运行一个MySQL容器,将宿主机目录直接挂载到容器
提示:目录挂载与数据卷挂载的语法是类似的:

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

实现思路如下:

  • 将下载好的mysql.tar文件上传到虚拟机,通过load命令加载为镜像

  • 创建目录/tmp/mysql/data

  • 创建目录/tmp/mysql/conf,将hmy.cnf文件上传到/tmp/mysql/conf

  • 去DockerHub查阅资料,创建运行MySQL容器,要求

    • 挂载/tmp/mysql/data到容器内数据存储目录
    • 挂载/tmp/mysql/conf/hmy.cnf到MySQL容器的配置文件
    • 设置MySQL密码
    docker run \
        --name mysql \
        -e MYSQL_ROOT_PASSWORD=root \
        -p 3306:3306 \
        -v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \
        -v /tmp/mysql/data:/var/lib/mysql \
        -d \
        mysql:5.7.25
    

Dockerfile自定义镜像

镜像结构

  • 镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。
  • 基础镜像(BaseImage):应用依赖的系统函数库、环境、配置、文件等
  • 层(Layer):在BaseImage基础上添加安装包、依赖、配置等,每次操作都形成新的一层
  • 入口(Entrypoint):镜像运行入口,一般是程序启动的脚步和参数
  • 其它:在BaseImage基础上添加依赖,安装程序,完成整个应用的安装和配置

Dockerfile语法

Dockerfile就是一个文本文件,其中包含一个个的指令(Instruction),用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层(Layer)
官方文档

指令说明实例
FORM指定基础镜像FROM centos:6
ENV设置环境变量,可在后面指令使用ENV key value
COPY拷贝本地文件到镜像的指定目录COPY ./mysql-5.7.rpm /tmp
RUN执行Linux的shell命令,一般是安装过程的命令RUN yum install gcc
EXPOSE指定容器运行时监听的端口,是给镜像使用者看的EXPOSE 8080
ENTRYPOINT镜像中应用的启动命令,容器运行时调用ENTRYPOINT java -jar xx.jar

基于Ubuntu镜像构建一个新镜像,运行一个Java项目

  • 创建一个空文件夹docker-demo
  • docker-demo.jar文件上传到docker-demo这个目录
  • jdk8.tar.gz文件上传到docker-demo这个目录
  • Dockerfile上传到docker-demo这个目录
  • 进入docker-demo
  • 运行命令:docker build -t javaweb:1.0 .

基于Java:8-alpine镜像,将一个Java项目构建为镜像

  • 新建一个空的目录,然后再目录中新建一个文件,命名为Dockerfile

  • docker-demo.jar文件上传到这个目录

  • 编写Dockerfile文件:

    • 基于java:8-alpine作为基础镜像
    • 将app.jar拷贝到镜像中
    • 暴露端口
    • 编写入口ENTRYPOINT
    # 指定基础镜像
    FROM java:8-alpine
    
    COPY ./docker-demo.jar /tmp/app.jar
    
    # 暴露端口
    EXPOSE 8090
    # 入口,java项目的启动命令
    ENTRYPOINT java -jar /tmp/app.jar
    
  • 使用docker build命令构建镜像

  • 使用docker run创建容器并运行

DockerCompose

初始DockerCompose

  • Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需一个个创建和运行容器
  • Compose文件是一个文本文件,通过指令定义集群汇总的每个容器如何运行
  • DockerCompose官网

安装DockerCompose

  • 下载

    # 安装
    curl -L https://get.daocloud.io/docker/compose/releases/download/1.29.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
    
  • 修改文件权限:chmod +x /usr/local/bin/docker-compose

  • 命令补全设置

    echo "199.232.68.133 raw.githubusercontent.com" >> /etc/hosts
    curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose -o /etc/bash_completion.d/docker-compose
    

项目部署

将之前学习的cloud-demo微服务集群利用DockerCompose部署

  • 修改自己的cloud-demo项目,将数据库、nacos地址都命名为docker-compose中的服务名
    在这里插入图片描述

  • 使用maven打包工具,将项目中的每个微服务都打包为app.jar

  • 将打包好的app.jar拷贝到cloud-demo中的每一个对应的子目录

  • 在每个对应的子目录创建 Dockerfile 文件并写入

    FROM java:8-alpine
    COPY ./app.jar /tmp/app.jar
    ENTRYPOINT java -jar /tmp/app.jar
    
  • 数据库文件

  • 在项目根目录创建 docker-compose.yml 文件并编写

    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"
    
  • 将cloud-demo上传到虚拟机
    在这里插入图片描述

  • 进入cloud-demo目录,利用 docker-compose up -d 来部署
    在这里插入图片描述

Docker镜像厂库

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

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

搭建私有厂库
简化版镜像仓库
Docker官方的Docker Registry是一个基础版本的Docker镜像仓库,具备仓库管理的完整功能,但是没有图形化界面。命令如下:

docker run -d \
    --restart=always \
    --name registry	\
    -p 5000:5000 \
    -v registry-data:/var/lib/registry \
    registry

命令中挂载了一个数据卷registry-data到容器内的/var/lib/registry 目录,这是私有镜像库存放数据的目录。
访问http://YourIp:5000/v2/_catalog 可以查看当前私有镜像服务中包含的镜像


图形化界面版本

  • 配置docker信任地址,因为私服采用的是http协议,默认不被Docker信任,所以需要做一个配置:

    # 打开要修改的文件
    vi /etc/docker/daemon.json
    # 添加内容:配置之间要用逗号分隔
    "insecure-registries":["http://192.168.2.102:8090"]
    # 重加载
    systemctl daemon-reload
    # 重启docker
    systemctl restart docker
    

    在这里插入图片描述

  • 使用DockerCompose部署带有图象界面的DockerRegistry

    • 新建空文件夹registry-ui:mkdir registr-ui

    • 创建docker-compose.yml文件: vi registr-ui/docker-compose.yml ,内容如下

      version: '3.0'
      services: #官方Docker Registr
        registry:
          image: registry
          volumes:
            - ./registry-data:/var/lib/registry
        ui: #图形化界面
          image: joxit/docker-registry-ui:static
          ports: #暴露端口8090
            - 8090:80
          environment: #服务部署标题和registry内部访问地址
            - REGISTRY_TITLE=小吴私有仓库
            - REGISTRY_URL=http://registry:5000
          depends_on:
            - registry
      
    • 进入registry-ui运行docker-compose,日志查看

      docker-compose up -d
      docker-compose logs -f
      

      在这里插入图片描述

  • 访问8090端口查看下
    在这里插入图片描述

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

    • 重新tag本地镜像,名称前缀为私有仓库的地址:192.168.2.102:8090/

      docker tag nginx:latest 192.168.2.102:8090/nginx:1.0
      
    • 推送镜像

      docker push 192.168.2.102:8090/nginx:1.0 
      

      在这里插入图片描述

    • 拉取镜像

      docker pull 192.168.2.102:8090/nginx:1.0
      
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小吴在敲Bug

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值