[toc]

视频地址

微服务技术栈图

认识微服务

01 服务框架演变

  • 单体架构
    • 概念:将业务的所有功能集中在一个项目中进行开发,打成一个包部署
    • 优点:架构简单,部署成本低
    • 缺点:耦合度高
  • 分布式架构
    • 概念:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,成为一个服务
    • 优点:降低服务耦合,有利于服务拓展升级
    • 考虑问题:
      • 服务拆分粒度如何?
      • 服务集群地址如何维护?
      • 服务之间如何实现远程调用?
      • 服务健康状态如何感知?
  • 微服务:是一种经过良好架构设计的分布式架构方案
    • 微服务架构特征:
      • 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
      • 面向服务:微服务对外暴露业务接口
      • 自治:团队独立,技术独立,数据独立,部署独立
      • 隔离性强:服务调用做好隔离,容错,降级,避免出现级联问题

02 微服务技术对比

  • 微服务这种方案需要技术框架来落地,国内最知名为SpringCloud和阿里巴巴的Duddo
    微服务技术对比

03 SpringCloud

  • SpringCloud是目前全球使用最广泛的微服务框架
  • SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验
  • SpringCloud和SpringCloud搭配使用需要注意版本兼容

服务拆分与远程调用

01 服务拆分

  • 服务拆分注意事项
      1. 不同微服务,不要重复开发相同业务
      1. 微服务数据独立,不要访问其它微服务的数据库
      1. 微服务可以将自己的业务暴露为接口,供其它微服务调用

02 服务间调用

  • 微服务调用方式

    • 基于RestTemplate发起的http请求实现远程调用
    • http请求做远程调用是与语言无关的调用,只需直到对方的ip,端口,接口路径,请求参数即可
  • 步骤:

      1. 注册RestTemplate
     @Bean
      public RestTemplate restTemplate(){
          return new RestTemplate();
      }
    
      1. 服务远程调用RestTempla
      @GetMapping("{orderId}")
      public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
          // 1. 查询订单
          Order order=orderService.queryOrderById(orderId);
          // 2. 利用RestTemplate发起http请求,查询用户
          //(1)url路径
          String url="http://localhost:8081/user/"+order.getUserId();
          //(2)发送http请求,实现远程调用
          User user=restTemplate.getForObject(url,User.class);
          // 3. 封装user到Order
          order.setUser(user);
          // 4. 返回
          return order;
      }
    
  • 服务调用关系

    • 服务提供者:暴露接口给其它微服务调用
    • 服务消费者:调用其他微服务提供的接口
    • 提供者和消费者角色是相对的
    • 一个服务可以同时是服务提供者和消费者

Eureka 注册中心

01 远程调用的问题

  • 服务消费者该如何获取服务提供者的地址信息?
  • 如果有多个服务者,消费者如何选择?
  • 消费者如何得知服务提供者的健康状态?

02 eureka原理

  • 服务消费者该如何获取服务提供者的地址信息?

    • 服务提供者启动时向eureka注册自己的信息
    • eureka保存这些信息
    • 消费者根据服务名称向eureka拉取提供者信息
  • 如果有多个服务者,消费者如何选择?

    • 服务消费者利用负载均衡算法选择一个执行
  • 消费者如何得知服务提供者的健康状态?

    • 服务提供者每30s向eurekaServer发送心跳请求,报告健康状态
    • eureeure
  • 在Eureka架构中,微服务角色有两类

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

03 搭建EurekaServer

  • 搭建步骤
      1. 创建项目,引入依赖
    <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
      </dependency>
    
      1. 编写启动类,添加@EnableEurekaServer注解
      @EnableEurekaServer
      @SpringBootApplication
      public class EurekaApplication {
      public static void main(String[] args) {
          SpringApplication.run(EurekaApplication.class,args);
          }
      }
    
      1. 添加application.yml文件,编写配置
      server:
          port: 10086
      spring:
          application:
              name: eurekaserver #eureka服务名称
      eureka:
          client:
              service-url:
                  defaultZone: http://127.0.0.1:10086/eureka
    

04 服务注册

  • 步骤如下:
      1. 在user-service项目引入spring-cloud-starter-netflix-eureka-client的依赖
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
      </dependency>
    
      1. application.yml文件,编写下面配置
      server:
          port: 10086
      spring:
          application:
              name: eurekaclient #eureka服务名称
      eureka:
          client:
              service-url:
                  defaultZone: http://127.0.0.1:10086/eureka
    

05 服务发现

  • 服务拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡
      1. 修改代码,修改url路径,用服务名代替ip和端口号
        String url="http://userservice/user/"+order.getUserId();
      1. 在项目启动类中添加负载均衡注解
        @LoadBalanced
      public RestTemplate restTemplate(){
          return new RestTemplate();
      }
    

Ribbon 负载均衡原理

01 负载均衡原理

负载均衡流程
负载均衡流程2

02 负载均衡策略

负载均衡策略

  • 通过定义rule可以修改负载均衡规则
      1. 代码方式:在order-service中的OrderApplication类中,定义一个新的Rule,这种方法会对整个项目起作用,配置灵活,但修改时需要重新打包
    @Bean
    public IRule randomRule(){
      return new RandomRule();
    }
    
      1. 配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则,直观方便,不需要重新打包,但无法做全局配置
    userservice:
      ribbon:
        NFLoadBalancerRuleClassName:com.netflix.loadbalancer.RandomRule
    

03 懒加载

  • Ribbon默认是采用懒加载,即第一访问时才回去创建LoadBalanceClient,请求时间较长。
  • 而饥饿加载会在项目启动时创建,降低第一次访问的时间消耗,通过下面配置饥饿加载
ribbon:
  eager-load:
    enabled: true
    clients: userservice # 指定对userservice这个服务饥饿加载

Nacos 注册中心

这里暂时不需要,以后再来填坑
视频地址

http客户端Feign

01 Feign替代RestTemplate

  • RestTempla存在的问题

    String url="http://userservice/user/"+order.getUserId();
    User user=restTemplate.getForObject(url,User.class);
    
    • 代码可读性差,编程体验不统一
    • 参数复杂URL难以维护
  • Feign 是一个声明式htpp客户端,其作用可以让我们更优雅的实现http发送请求

  • 定义和使用Feign客户端:

      1. 引入依赖
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
      </dependency>
    
      1. 在order-service的启动类中添加注解开启Feign开关@EnableFeignClients
      1. 编写Feign客户端,主要基于SpringMVC的注解来声明说成调用信息
    @FeignClient("userservice")
    public interface UserClient {
      @GetMapping("/user/{id}")
      User findById(@PathVariable("id") Long id);
    }
    
      1. 使用FeignClien中定义的方法代替RestTemplate
    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;
      }
    

02 自定义配置

  • 配置feign的日志
      1. 配置文件方式
    feign:
      client:
        config:
          default/userservice: #默认为全局配置,或使用服务名仅针对某个服务 
            loggerLevel: FULl # 日志级别
    
      1. 使用java代码
    public class DefaultFeignConfiguration {
      @Bean
      public Logger.Level logLevel(){return Logger.Level.BASIC;}
    }
    

03 Feign使用优化

  • 优化手段
      1. 使用连接池代替默认的URLConnection
      • 引入HTTPClient依赖
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-httpclient</artifactId>
    </dependency>
    
    feign:
      httpclient:
        enable: true # 支持HttpClient开关
        max-connections: 200 #最大连接数
        max-connections-per-route: 50 #单个路径最大连接数
    
      1. 日志级别,最好用basic或none

04 最佳实践

    1. 继承:给消费者的FeignClient和提供者的controller定义统一的父接口作为标准
    • 服务紧耦合(API已经相同了)
    • 父接口参数列表中的映射不会被继承
    1. 抽取:将FeignClient抽取为独立模块,并且把接口有关的POJO,默认的Feign配置都放到这个模块中,提供给所有消费者使用
  • 抽取实践

    • 步骤:
        1. 首先创建一个module,命名为feign-api,然后引入feign的starter依赖
        1. 将order-service中编写的UserClient,User,DefaultFeignConfiguration都复制到feign-api项目中
        1. 在order-service中引入feign-api依赖
        1. 修改order-service中的所有与上述三个组件相关部分的import部分,改成导入feign-api的包
        1. 重启测试
    • 当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClien无法使用,有两种解决办法
        1. 指定FeignClient所在包@EnableFeignClients(basePackages="cn.itcast.feign.clients")
        1. 指定FeignCLient字节码@EnableFeignClients(clients={UserClient})

统一网关Gateway

01 为什么需要网关

  • 网关功能

    • 身份认证和权限校验
    • 服务路由,负载均衡
    • 请求限流
  • 在springcloud中网关的实现有两种

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

02 gateway快速入门

  • 搭建网关服务的步骤
      1. 创建新的moudle,引入SpringCloudGateway的依赖和eureka的服务发现依赖
    <!--gateway-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!--eureka-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
      1. 编写路由配置及eureka地址
      • 路由配置包括:
          1. 路由id:路由的唯一标识
          1. 路由目标uri:路由的目标地址,http代表目标地址,lb代表根据服务名负载均衡
          1. 路由断言predicates:判断路由的规则
          1. 路由过滤器filters:对请求或响应做处理
    server:
    port: 10010
    spring:
      application:
        name: gateway
      cloud:
        gateway:
          routes:
            - id: user-service # 路由标识
              uri: lb://userservice #路由的目标地址
              predicates: #路由断言,判断请求是否符合规则
                - Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合
            - id: order-service
              uri: lb://orderservice
              predicates:
                - Path=/order/**
    

网关服务图解

03 断言工厂

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

具体使用可以参考官网

04 过滤器工厂

  • 路由过滤器GatewayFilter
    • GatewaFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理
    • Spring提供了31种不同的路由过滤器工厂,同样详见官网
    spring:
      application:
        name: gateway
      cloud:
        gateway:
          routes:
            - id: user-service # 路由标识
              uri: lb://userservice #路由的目标地址
              predicates: #路由断言,判断请求是否符合规则
               - Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合
            - id: order-service
              uri: lb://orderservice
              predicates:
               - Path=/order/**
              filters:
                - AddRequestHeader = "" # 添加请求头
          #default-filters: 这个是全路由过滤器,对所有的路由都生效,与routes平级
            #- AddRequestHeader = "" # 添加请求头
    

05 全局过滤器

  • 全局过滤器GlobalFilter的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter作用一样。
  • 区别是GatewaFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现,定义方法是实现GlobalFilter接口
  • 实现全局过滤器的步骤
      1. 实现GlobalFilter接口
      1. 添加@Order注解或实现Ordered接口
      1. 编写处理逻辑
@Order(-1)//设置优先级
@Component//bean
public class AuthorizeFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1. 获取请求参数
        MultiValueMap<String,String> params = exchange.getRequest().getQueryParams();;
        // 2. 获取参数中的authorization参数
        String auth = params.getFirst("authorization");
        // 3. 判断参数值是否为admin
        if("admin".equals(auth)){
            // 4. 是,放行
            return chain.filter(exchange);
        }
        // 5. 否,拦截
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();
    }
}
  • 过滤器执行顺序
    • 请求进入网关后会碰到三类过滤器:当前路由的过滤器,DefaultFilter,GlobalFilter。请求路由后,会将当前路由过滤器和DefaultFilter,GlobalFilter合并到一个过滤器链(集合)中,排序后依次执行每个过滤器
    • 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。
    • GlobalFilter通过实现Ordered接口或者添加@Order注解来指定order值,有编码者自己指定
    • 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增
    • 当过滤器的order值一样时,会按照 defaultFilter>路由过滤器>GlobalFilter的顺序执行

06 跨域问题处理

  • 跨域:域名不一致就是跨域,主要包括
    • 域名不同
    • 域名相同,端口不同:
  • 跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
  • 解决方案:CORS
  • gateway已经实现CORS,使用者需要配置
spring:
  cloud:
    gateway:
      globalcors: # 全局的跨域处理
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
        corsConfigurations:
          '[/**]':
            allowedOrigins: # 允许哪些网站的跨域请求
              - "http://localhost:8090"
              - "http://www.leyou.com"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" # 允许在请求中携带的头信息
            allowCredentials: true # 是否允许携带cookie
            maxAge: 360000 # 这次跨域检测的有效期

Docker

01 初始Docker

  • 什么是Docker?

    • 项目部署问题:大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题
      • 依赖关系复杂,容易产生兼容性问题
      • 开发,测试,生产环境有差异
    • Docker如何解决依赖的兼容问题
      • 将应用的Libs(函数库),Deps(依赖),配置和应用一起打包
      • 将每个应用放到一个隔离容器去运行,避免相互干扰
    • Docker如何解决不同系统环境的问题
      • Docker将用户程序与所需要调用的系统(比如Ubuntu)函数库一起打包
      • Docker运行到不同操作系统时,直接基于打包的库函数,借助于操作系统的Linux内核来运行
      • 即Docker镜像中包含完整的运行环境,包含系统函数库,仅依赖系统的Linux内核,因此可以在任意Linux操作系统上运行
  • Docker和虚拟机的差异

    • docker是一个系统进程;虚拟机是在操作系统中的操作系统
    • docker体积小,启动速度快,性能好;虚拟机体积大,启动速度慢,性能一般
  • Docker架构

    • 镜像和容器
      • 镜像(Image):Docker将应用程序及其所需依赖,函数库,环境,配置等文件打包在一起,称为镜像
      • 容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是docker会给容器做隔离,对外不可见
    • Docker和DockerHub
      • DockerHub:是一个Docker镜像的托管平台,这样的平台称为Docker Registry
      • 国内有类似DockerHub的公开服务,如网易云镜像服务,阿里云镜像服务
    • Docker是一个CS架构,由两部分组成
      • 服务端(server):Docker守护进程,负责处理Docker指令,管理镜像,容器等
      • 客户端(client):通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令
  • 安装Docker
    跟着视频按即可

02 Docker基本操作

镜像操作

Docker基本操作
主要是查看文档

容器操作

Docker相关命令

  • 创建一个Nginx容器

    • 去docker hub查看Nginx的容器的命令
      docker run --name containerName -p 80:80 -d nginx
      • 命令解读:
        • docker run:创建并运行一个容器
        • –name:给容器起一个名字,比如叫做mn
        • -p:将宿主机端口与容器端口映射,冒号左边是宿主机端口,右侧是容器端口
        • -d:后台运行容器
        • nginx:镜像名称
  • 查看容器日志的命令:

    • docker logs
    • 添加-f参数可以持续查看日志
  • 查看容器状态:docker ps

  • 删除容器docker rm

  • 案例操作示例:

    • 进入Nginx容器,修改HTML文件内容,添加“HELLO WORLD”:进入容器使用命令docker exec -it containerName bash
      • 命令解读
        • docker exec:进入容器内部
        • -it:给当前进入的容器创建一个标准输入,输出终端,允许我们与容器交互
        • containerName:要进去的容器的名称
        • bash:进入容器后执行的命令
    • 创建并运行一个redis容器,并且支持数据持久化
        1. 到DockerHub搜索redis镜像
        1. 查看Redis镜像文档中的帮助信息
        1. 利用docker run命令运行一个Redis容器

数据卷(容器数据管理)

  • 容器和数据耦合问题

      1. 不便于修改:当我们要修改Nginx的html内容时,需要进入容器内部修改,很不方便
      1. 数据不可复用:在容器内的修改对外是不可见的。所有修改对新创建的容器是不可复用的
      1. 升级维护困难,数据在容器内,如果要升级必然删除旧容器,所有数据会丢失
  • 数据卷

    • 感觉和C++的指针很像,Container调用指向宿主文件系统的某个目录的指针
      Docker数据卷
  • 操作数据卷

    • 数据卷操作基本语法:docker volume [COMMAND]
      • create 创建一个volume
      • inspect 显示一个或多个volume的信息
      • ls 列出所有volume
      • prune 删除未使用的volume
      • rm 删除一个或多个指定的volume
    • 操作案例:创建一个数据卷,并查看数据卷在宿主机的目录位置
  • 挂载数据卷

    • 创建容器时,可以通过-v参数来挂载一个数据卷到某个容器目录
    • 举例docker run --name mn -v html:/root/html -p 8080:80 nginx
      • docker run 创建并运行容器
      • –name mn:给容器起名叫mn
      • -v html:/root/html: 将html数据卷挂载到容器内/root/html这个目录中
      • -p 8080:80: 将宿主机的8080端口映射到容器内的80端口
      • ngnix:镜像名称
    • 案例:创建一个ngnix容器,修改html目录内index.html的内容(通过数据卷挂载)
  • 如果容器运行时volume不存在,会自动被创建出来

案例:创建并运行一个MySQL容器,将宿主机目录直接挂载到容器中

见视频,这里使用 docker pull mysql直接拉取最新docker镜像

总结

    1. docker run的命令通过-v参数挂载文件或目录到容器中
      1. -v volume名称:容器内目录
      1. -v 宿主机文件: 容器内文件
      1. -v 宿主机目录:容器内目录
    1. 数据卷挂载与目录直接挂载的
      1. 数据卷挂载耦合度低,由docker来管理目录,但是目录较深,不好寻找
      1. 目录挂载耦合度较高,需要自己记录管理目录,但便于寻找

03 Dockerfile自定义镜像

镜像结构

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

  • 镜像是分层结构:

    • BaseImage层:包含基本的系统函数库,环境变量,文件系统
    • Entrypoint:入口,是镜像中应用启动的命令
    • 其它:在BaseImage基础上添加依赖,安装程序,完成整个应用的安装和配置

Dockfile语法

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

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

构建Java项目

视频地址

  • 案例:基于centos创建一个新景象,运行一个java项目

      1. 新建一个空文件夹docker-demo
      1. 拷贝jar文件到该目录
      1. 拷贝jdk8.tar.gz到该目录
      1. 拷贝Dockerfile文件到该目录
      1. 进入docker-demo
      1. 运行命令docker build -t javaweb:1.0
  • 基于java:8-alpine镜像,将一个Java项目构建为镜像

    • 该镜像可以直接构建java镜像环境
      1. 新建空目录,其中新建文件Dockerfile
      1. 将jar包拷贝到这个目录
      1. 编写Dockerfile文件
        1. 基于java:8-alpine作为基础镜像
        1. 将app.jar拷贝到镜像
        1. 暴露端口
        1. 编写入口ENTRYPOINT
      1. 使用docker build命令构建镜像
      1. 使用docker run创建容器并运行

总结

    1. Dockerfile本质是一个文件,通过指令描述镜像的构建过程
    1. Dockerfile的第一行必须是FROM,从一个基础镜像来构建
    1. 基础镜像可以是基本操作系统,如centos,也可以是其他人制作好的镜像,如java:8-alpine

04 DockerCompose

认识DockerCompose

  • 什么是DockerCompose
    • DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器
    • Compose文件是一个文本文件,可以通过定义集群中的每个容器如何运行
  • DockerCompose的安装

部署微服务集群

05 Docker镜像仓库

搭建私有镜像仓库

  • 镜像仓库有公有的和私有的两种形式
    • 公共仓库:例如Docker官方Docker Hub,网易云镜像服务,阿里云镜像服务等
    • 除使用公开仓库外,用户可以在本地搭建Docker Registry。企业自己的镜像最好是采用私有Docker Registry来实现
  • 创建过程,看视频

向镜像仓库推送镜像

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

  1. 重新tag本地镜像,名称前缀为私有仓库的地址:ip:8080/docker tag ngnix:latest ip:8080/nginx:1.0
  2. 推送镜像docker push ip:8080/nginx:1.0
  3. 拉取镜像docker pull ip:8080/nginx:1.0

总结

  1. 推送本地镜像仓库前都必须重命名(docker tag)镜像,以镜像仓库地址为前缀
  2. 镜像仓库推送前需要把仓库地址配置到docker服务的daemon.json文件中,被docker信任
  3. 推送使用docker push命令
  4. 拉取使用docker pull命令

MQ