banner
NEWS LETTER

微服务

Scroll down

微服务

认识微服务

微服务是一种软件设计模式,它将应用程序拆分成小型、相互独立的服务,每个服务都可以通过标准化的接口进行通信和交互。

这些服务通常部署在容器中,每个服务都可以独立地进行扩展、升级和维护,从而提高了系统的灵活性、可伸缩性和可靠性。

与传统的单体式架构不同,微服务的架构可以更轻松地适应快速变化的业务需求,并且可以更容易地实现持续集成和持续部署(CI/CD)的流程。

微服务的特点:

  • 单一职责:微服务拆分颗粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
  • 面向服务:微服务对外暴露业务接口
  • 自治:团队独立、技术独立、数据独立、部署独立
  • 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题】

SpringCloud与SpringBoot的版本兼容关系

image-20230421171911074

远程调用

微服务的远程连接实例通常采用 RESTful API 或 RPC(Remote Procedure Call)来实现。

RESTful API 是一种基于 HTTP 协议的轻量级的、可扩展的架构风格,它使用明确的 URL 和 HTTP 方法(例如 GET、POST、PUT、DELETE)来表示对资源的操作。每个微服务都提供一组 RESTful API,其他微服务可以通过调用这些 API 来访问和使用该服务提供的功能。

使用步骤:

  1. 注册RestTemplate

    在order-service的OrderApplication中注册RestTemplate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @MapperScan("cn.itcast.order.mapper")
    @SpringBootApplication
    public class OrderApplication {

    public static void main(String[] args) {
    SpringApplication.run(OrderApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate(){
    return new RestTemplate();
    }
    }
  2. 服务远程调用RestTemplate

    修改order-service中的OrderService的queryOrderById方法

    也就是通过http连接获取到user获取到的数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    @Service
    public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @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 服务器,并让其他服务消费该服务

服务提供者和服务消费者统称为eureka客户端

Eureka注册中心执行步骤:

  1. 在服务提供者启动时会自动将注册服务信息发送给eureka

  2. eureka接收到注册信息例如:

    user-service:

    ​ localhost:8081

    ​ localhost:8082

    ​ localhost:8083

    order-service:

    ​ localhost:8080

  3. 消费者使用时会找rureka拿取注册信息

    例如想要请求user-service的信息,eureka就会将注册信息通过负载均衡向服务提供者发送请求

注:服务消费者不会拿到死掉的地址,因为服务提供者会每隔30s发送一次心跳确认状态

image-20230508113756050

搭建eureka服务

  1. 创建项目,引入spring-cloud-starter-netflix-eureka-server

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    <version>2.2.7.RELEASE</version>
    </dependency>

    注意:springboot、springcloud和eureka版本要对应

  2. 编写启动类,添加@EnableEurekaServer注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @EnableEurekaServer
    @SpringBootApplication
    public class EurekaServerApplication {

    public static void main(String[] args) {
    SpringApplication.run(EurekaServerApplication.class, args);
    }

    }
  3. 添加application.yml文件,编写下面的配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    server:
    port: 10086 #服务端口
    spring:
    application:
    name: eurekaserver #eureka的服务名称
    eureka:
    client:
    service-url: #eureka的地址信息
    defaultZone: http://127.0.0.1:10086/eureka

    文件结构:eureka以子文件形式出现

    image-20230508182704703

服务注册

服务注册,就是将提供某个服务的模块信息(通常是这个服务的ip和端口)注册到1个公共的组件上去

  1. 在order-service项目引入spring-cloud-starter-netflix-eureka-client的依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    <version>2.2.7.RELEASE</version>
    </dependency>
  2. 在application.yml文件,编写下面的配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    server:
    port: 8080 #服务端口
    spring:
    application:
    name: eurekaserver #eureka的服务名称
    eureka:
    client:
    service-url: #eureka的地址信息
    defaultZone: http://127.0.0.1:10086/eureka

服务发现

服务发现,就是新注册的这个服务模块能够及时的被其他调用者发现。不管是服务新增和服务删减都能实现自动发现。

  1. 修改OrderService的代码,修改访问的url路径,用服务名代替ip、端口:

    1
    String url="http://userservice/user/"+order.getUserId();
  2. 在order-service项目的启动类OrderApplication中的RestTemplate添加负载均衡注解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @MapperScan("cn.itcast.order.mapper")
    @SpringBootApplication
    public class OrderApplication {

    public static void main(String[] args) {
    SpringApplication.run(OrderApplication.class, args);
    }

    @Bean
    @LoadBalanced //实现负载均衡
    public RestTemplate restTemplate(){
    return new RestTemplate();
    }
    }

Ribbon负载均衡

image-20230806140023892

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

  1. 添加依赖:您需要将Spring Cloud Ribbon依赖添加到您的项目中。 您可以在pom.xml文件中添加以下代码:

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    </dependency>
  2. 代码方式:在order-service中的OrderApplication类中,定义一个新的IRule:

    1
    2
    3
    4
    5
    @Bean
    public IRule randomRule({
    //实行随机分配负载
    return new RandomRule();
    }
  3. 配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则

    1
    2
    3
    4
    userservice:
    ribbon:
    #负载均衡规则
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
  4. image-20230621085421470

懒加载和饥饿加载

懒加载:在第一次发起请求的时候才会创建LoadBalancerClient对象,就会导致第一次访问时长比较久

饥饿加载:饥饿加载是指在启动项目时就创建LoadBalancerClient对象,并在后续请求中只使用一个LoadBalanceClient对象,从而减少了一开始就请求大量资源所带来的延迟和耗电量。

程序默认为懒加载:

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

Nacos

下载安装运行

下载1.4.1版本

Release 1.4.1 (Jan 15, 2021) · alibaba/nacos · GitHub

![image-20230509181757525](/img/微服务/Java SE.md)

解压安装

运行bin/startup.cmd

使用necos注册中心

  1. 在父组件的pom文件中

    1
    2
    3
    4
    5
    6
    7
    8
    <!--nacos组件-->
    <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>
  2. 在子组件中去掉eureka依赖,并加上

    1
    2
    3
    4
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
  3. 配置yaml文件,去掉eureka的并添加:

    1
    2
    3
    4
    spring:
    cloud:
    nacos:
    server-addr: localhost:8848

注:在运行程序时,记得启动nacos

Nacos服务分级存储模型

在企业中,一般我们会把服务的多个实例分放在多个机房以达到容灾

服务调用尽可能选择本地集群的服务,跨集群调用延迟较高

本地集群不可访问时,再去访问其它集群

image-20230510103222771

Nacos服务分级存储模型

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

服务集群属性设置:

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

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

加权负载均衡

  • Nacos控制台可以设置实例的权重值,0~1之间同集群内的多个实例,

  • 权重越高被访问的频率越高权重

  • 设置为0则完全不会被访问

image-20230510121556683

环境隔离

Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离

Nacos环境隔离

  • namespace用来做环境隔离

  • 每个namespace都有唯一的id

  • 不同namespace下的服务不可见


  1. 在Nacos控制台可以创建namespace,用来隔离不同环境

    image-20230510163803464

  2. 设置新命名空间

    image-20230510163840077

  3. 保存后会在控制台看到一个命名空间id

    image-20230510163949690

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

    1
    2
    3
    4
    5
    6
    7
    spring:
    cloud:
    nacos:
    server-addr: localhost:8848
    discovery:
    cluster-name: SH #上海
    namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 #命名空间,填ID

临时实例和非临时实例

临时实例和非临时实例区别:

​ 临时实例:每隔三十秒向注册中心发送心跳

​ 非临时实例:不自动发送心跳,由注册中心询问,更新速度更快

image-20230510171337216

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

1
2
3
4
5
spring:
cloud:
ntcos:
discovery:
ephemeral: false #设置为非临时实例

Nacos与Eureka的区别

  1. Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
  2. 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
  3. Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
  4. Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式

Nacos统一配置管理

Nacos不只是有强大的注册中心功能,他同时还兼备配置管理功能。所谓统一配置就是将这一个微服务中都能用到的配置放在一个文件中,并且能实现热更新

  1. image-20230510173601186

  2. image-20230510173619767

  3. 服务获取配置

配置获取的步骤如下:

项目启动时会优先读取一个叫bootstarp.yml的文件,然后再将这个文件和本地的application.yml相结合,在通过这俩配置文件的结合体来创建spring容器最后加载bean

具体步骤:

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

    1
    2
    3
    4
    5
    <! --nacos配置管理依赖-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
  2. 在userservice中的resource目录添加一个bootstarp.yaml文件,这个文件是引导文件,优先级高于application.yaml:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    spring:
    application:
    name: userservice #服务名称,和nacos注册中心名称一致
    profiles:
    active: dev #开发环境,这里是dev
    cloud:
    nacos:
    server-addr: localhost:8848 #Nacos地址
    config:
    file-extension: yaml#文件后缀名

    可以在user-servie中将pattern.dateformat这个属性注入到UserController中做测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @RestController
    @RequestMapping("/user")
    public class UserController {

    //注入nacos中的配置属性
    @Value("${pattern.dateformat}")
    private String dateformat;

    //编写controller,通过日期格式化器来格式化当前时间并返回
    @GetMapping("now")
    public String now(){
    return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
    }

    另外,Nacos配置更改后,微服务可以实现热更新,有两种方法:

    1. 通过@value注解注入,结合@RefreshScope来刷新
    2. 通过@ConfigurationProperties注入,自动刷新

优先级:

服务名-环境.yaml>服务名.yaml>本地配置

Nacos集群搭建步骤

  1. 搭建MySQL集群并初始化数据库表

  2. 下载解压nacos

  3. 修改集群配置(节点信息)、数据库配置

    1. 进入nacos的conf目录,修改配置文件cluster.conf.example重命名为cluster.conf

    2. 添加内容,集群中每一个节点的信息

      127.0.0.1:8845

      127.0.0.1:8846

      127.0.0.1:8847

    3. 修改application.properties文件,添加数据库配置

      1
      2
      3
      4
      5
      6
      7
      8
      #数据源,告诉nacos用的什么集群
      spring.datasource.platform=mysq1
      #有几台机器
      db.num=1
      db.ur1.O=jdbc:mysq7://127.0.0.1:3306/nacos?
      characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useunicode=true&usessL=fa1se&serverTimezone=UTC
      db.user.0=root I
      db.password.0=123
  4. 分别启动多个nacos节点

  5. nginx反向代理

    修改conf/nginx.conf文件,配置如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //放置集群地址,方便nginx设置反向代理
    upstream nacos-cluster {
    server 127.0.0.1:8845;
    server 127.0.0.1:8846;
    server 127.0.0.1:8847;
    }
    server {
    listen 80;
    server_name localhost;
    location /nacos {
    proxy_pass http://nacos-cluster;
    }
    }

Feign

基于Feign的远程调用

步骤:

  1. 引入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  2. 创建UserClient

    1
    2
    3
    4
    5
    @FeignClient("userservice")
    public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
    }
  3. 在springboot启动类中添加注解

    1
    @EnableFeignClients
  4. 修改之前的orderService.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Service
    public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private 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的性能优化

在Fegin的默认中是不支持连接池的URLConnection,因此每次请求都会进行三次握手和四次挥手大大提高了响应速度,因此我们对Fegin的性能优化首先就要解决让Fegin支持连接池

Fegin中底层的客户端实现:

  • URLcontroller:默认实现,不支持连接池

  • Apache HttpClient:支持连接池

  • OKHttp:支持连接池

这里主要介绍使用Apache HttpClient的使用方法

  1. 引入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    </dependency>
  2. 配置连接池

    1
    2
    3
    4
    5
    6
    7
    8
    9
    feign:
    client:
    config:
    default: #default全局的配置
    loggeiLevel: BASIC #日志级别,BASIC就是基本的请求和晌应信息
    httpclient:
    enabled : true #开启feign对HttpClient的支持
    max-connections: 200 #最大的连接数
    max-connections-per-route: 50 #每个路径的最大连接数

Feign最佳实践

  1. 将clients和pojo的user单独分离出来成为fegin-api
  2. 将fegin-api当做包引用
  3. 可能会遇到的问题:自动注入不成功

​ 解决方法:

  1. 在@EnableFeignClients注释中添加basePackages,指定FeignClient所在的包

    1
    @EnableFeignClients(basePackages = "cn.itcast.feign.clients")
  2. 在@EnableFeignClients注解中添加clients,指定具体Feignclient的字节码

    1
    EnableFeignclients(clients = {Userclient.class})

统一网关Gateway

为什么需要网关

网关的功能:

  1. 身份验证和权限检测
  2. 服务路由、负载均衡
  3. 请求限流

快速入门

搭建网关服务的步骤:

  1. 创建新的module,引入SpringCloudGetway的依赖和nacos的服务发现依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <! --网关依赖-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <! --nacos服务发现依赖-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>
  2. 编写路由配置及nacos地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    server:
    port: 10010 #网关端口
    spring:
    application:
    name: gateway #服务名称
    cloud:
    nacos:
    server-addr: localhost:8848 # nacos地址
    gateway:
    routes: #网关路由配置
    - id: user-service #路由id,自定义,只要唯一即可
    # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
    uri: lb://userservice # 路由的目标地址lb就是负载均衡,后面跟服务名称
    predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
    - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
    - id : order-service
    uri: lb://orderservice
    predicates:
    - Path=/order/**

    网关执行流程:

    image-20230517165753824

断言工厂

网关需要符合所有的断言工厂才能正常分配

image-20230517175034135

image-20230517174934209

过滤器工厂

过滤器的作用:

  • 对路由的请求或响应做加工处理,比如添加请求头
  • 配置在路由下的过滤器只对当前路由的请求生效

例如:给所有进入userservice的请求添加一个请求头: Truth=itcast is freaking awesome!

实现方式:

  1. 在gateway中修改application.yml文件,给userservice的路由添加过滤器:
1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:#网关路由配置
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
filters:#过滤器
- AddRequestHeader=Truth,Itcast is freaking awesome! #添加请求头
  1. 在方法中接收

    1
    2
    3
    4
    5
    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id ,
    @RequestHeader(value = "Truth",required = false) String truth) {
    return userService.queryById(id);
    }

默认过滤器

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
application:
name: gateway #服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: #网关路由配置
- id: user-service #路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
filters: #过滤器
- AddRequestHeader=Truth,Itcast is freaking awesome! #添加请求头
- id : order-service
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters: #默认过滤器,会对所有的路由请求生效
- AddRequestHeader=Truth,Itcast is freaking awesome! #在这里写添加请求头

全局过滤器

全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。

区别在于GatewayFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现。定义方式是实现GlobalFilter接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*order设置优先级,数字越低优先级越高*/
@Order(-1)
/*注册成spring的bean*/
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> params = request.getQueryParams();
// 2.获取参数中authorization参数
String auth=params.getFirst("authorization");
// 3.判断参数值是否等于admin
if("admin".equals(auth)) {
// 4.是,放行
return chain.filter(exchange);
}else {
// 5.否,拦截并设置状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
}

跨域问题处理

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
application:
name: gateway #服务名称
cloud:
gateway:
globalcors: #全局的跨域处理
add-to-simple-url-handler-mapping: true #解决options请求被拦截问题
corsConfigurations:
'[/**]': #所有请求进行跨域处理
allowed0rigins: #允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: #允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" #允许在请求中携带的头信息
allowCredentials: true #是否允许携带cookie
maxAge: 360000 #这次跨域检测的有效期,在这个范围内浏览器将不再询问跨域

Docker分布式部署

项目部署的问题

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

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

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

首先我们要知道,linux系统分为很多种,例如:Ubuntu、centos等,他们都是基于linux内核,而linux内核通过固定的指令就能操作硬件。而每个系统的差异就是因为打包linux指令的函数式不同,函数式不同就会造成不同的系统无法执行同一个命令。

Docker如何解决:

  • Docker将用户程序与所需要调用的系统(例如centos)函数库一起打包成可移植的镜像

  • Docker运行到不同的操作系统时,直接基于打包的库函数,借助操作系统的linux内核来运行

安装、卸载Docker

centos安装Docker

1
yum install -y yum-utils device-mapper-persistent-data lvm2 --skip-broken

设置Docker下载源

1
2
3
4
5
6
7
8
# 设置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

1
2
3
4
5
6
7
8
9
10
11
yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-selinux \
docker-engine-selinux \
docker-engine \
docker-ce

配置镜像加速

docker官方镜像仓库网速较差,我们需要设置国内镜像服务:

参考阿里云的镜像加速文档:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors

docker获取镜像

Docker Hub Container Image Library | App Containerization

1
docker pull nginx

docker其他命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#启动 Docker 服务
sudo systemctl start docker

#设置为自动启动:
sudo systemctl enable docker

#查看帮助
docker save --help

#查看镜像
docker images

#将镜像打包
docker save -o nginx.tar nginx: latest

#删除指定镜像
docker rmi nginx : latest

#读取镜像打包文件
docker load -i nginx.tar

docker容器命令

  1. docker run:创建并启动一个新容器。

例如,使用下面的命令可以启动一个基于nginx镜像的名为 mycontainer 的容器

1
docker run --name mycontainer -p 80:80 -d nginx
  • docker run :创建并运行一个容器

  • –name:给容器起一个名字,比如叫做mn

  • -p∶将宿主机端口与容器端口映射,冒号左侧是宿主机端口,右侧是容器端口-d:后台运行容器

  • nginx:镜像名称,例如nginx

  1. docker start:启动已经存在的一个容器。

例如,使用下面的命令可以启动名为 mycontainer 的容器:

1
docker start mycontainer
  1. docker stop:停止正在运行的容器。

例如,使用下面的命令可以停止名为 mycontainer 的容器:

1
docker stop mycontainer
  1. docker rm:删除指定的容器。

例如,使用下面的命令可以删除名为 mycontainer 的容器:

1
docker rm mycontainer
  1. docker ps:列出所有正在运行的容器。

例如,使用下面的命令可以列出所有正在运行的容器:

1
docker ps
  1. docker exec:在正在运行的容器中执行命令。

例如,使用下面的命令可以在名为 mycontainer 的容器中运行一个 ls 命令:

1
docker exec mycontainer bash

进入Nginx容器,修改HTML文件内容,添加“传智教育欢迎您”

步骤一︰进入容器。

进入我们刚刚创建的nginx容器的命令为:

1
docker exec -it mn bash

命令解读:

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

步骤二︰进入nginx的HTML所在目录

/usrlshare/nginx/ htmlcd /usr/share/nginx/html

步骤三:修改index.html的内容

1
2
sed -i ' s#Welcome to nginx#传智教育欢迎您#g'index.html
sed -i 's#<head>#<head><meta charset="utf-8">#g' index.html

数据卷

数据卷基本操作

创建数据卷并查看数据卷在本机的位置:

  1. 创建数据卷
    docker volume create html

  2. 查看所有数据
    docker volume ls

  3. 查看数据卷详细信息卷

    docker volume inspect html

数据卷挂载到容器

1
docker run --name mn -v html:/root/html -p 8080:80 nginx

docker run :就是创建并运行容器

–name mn:给容器起个名字叫mn

-v html:/root/htm : 把html数据卷挂载到容器内的/root/html这个目录中

-p 8080:80:把宿主机的8080端口映射到容器内的80端口

nginx:镜像名称

自定义镜像

了解镜像结构

镜像是分层结构,每一层称为一个Layer

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

自定义镜像

  1. 配置文件如下配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # 指定基础镜像
    FROM ubuntu:16.04
    # 配置环境变量,JDK的安装目录
    ENV JAVA_DIR=/usr/local

    # 拷贝jdk和java项目的包
    COPY ./jdk8.tar.gz $JAVA_DIR/
    COPY ./docker-demo.jar /tmp/app.jar

    # 安装JDK
    RUN cd $JAVA_DIR \
    && tar -xf ./jdk8.tar.gz \
    && mv ./jdk1.8.0_144 ./java8

    # 配置环境变量
    ENV JAVA_HOME=$JAVA_DIR/java8
    ENV PATH=$PATH:$JAVA_HOME/bin

    # 暴露端口
    EXPOSE 8090
    # 入口,java项目的启动命令
    ENTRYPOINT java -jar /tmp/app.jar
  2. 构建镜像

1
docker build -t javaweb:1.0 .

javaweb指的是镜像名称、1.0表示版本,后面的.表示构建镜像的步骤文件在本文件夹内

  1. 在镜像的基础上创建容器并运行
1
docker run --name web -p 8090:8090 -d javaweb:1.0

鉴于每次部署java太麻烦,docker提供了一个名为java:8-alpine镜像,将一个java项目构建为镜像

实现思路如下:

  1. 新建一个空的目录,然后在目录中新建一个文件,命名为Dockerfile
  2. 拷贝课前资料提供的docker-demo.jar到这个目录中
  3. 编写Dockerfile文件:
    1. 基于java:8-alpine作为基础镜像b)将app.jar拷贝到镜像中
    2. 暴露端口
    3. 编写入口ENTRYPOINT使用docker build命令构建镜像
    4. 使用docker run创建容器并运行

DockerCompose

什么是DockerCompose

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

DockerCompose有什么作用

可以帮助我们快速部署分布式应用,无需一个个微服务去构建镜像和部署

案例:将之前写好的cloud-dome使用docker部署成微服务项目

使用步骤:

  1. 将需要的包放入其中包括gateway、mysql、order-service、user-service、nacos镜像

  2. 编辑配置文件docker-compose

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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"
  3. 修改项目,将数据库、nacos地址都命名为docker-compose中的服务名

例如nacos:server-addr:local:80更改成nacos:server-addr:nacos:8848

mysql同理

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

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

  3. 将cloud-dome上传到虚拟机,利用dock-compose up -d来部署

同步通信和异步通信

image-20230531112842838

同步通信就像打电话一样,需要立即的响应和回应,而且在两个通信方之间只能进行单向或双向通信。如果有其他消息发送,就必须等待当前的通信完成后才能处理下一个请求。因此,同步通信会占用较多的资源,并且不太适合大量数据传输或高并发处理。

image-20230531113346100

而异步通信更像是发短信一样,我把消息发送出去,就会立即显示发送成功。但是具体啥时候接收到响应就不管了。就像上面的例子用户支付完之后通知支付服务,支付服务会直接把支付成功发送给用户而不是等所有流程走完在发送。大大减少了传输时间。

RabbitMQ消息队列

单机部署和集群部署

单机部署:

  1. 在线拉取

    1
    docker pull rabbitmq:3-management
  2. 拉取后运行

    1
    docker load -i mq.tar
  3. 运行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    docker run \
    -e RABBITMQ_DEFAULT_USER=root \
    -e RABBITMQ_DEFAULT_PASS=510609 \
    --name mq \
    --hostname mq1 \
    -p 15672:15672 \
    -p 5672:5672 \
    -d \
    rabbitmq:3-management
  4. 打开浏览器,地址栏搜索

    1
    http://192.168.136.131:15672/
  5. 输入自己设置的用户名和密码

  6. RabbitMQ中的几个概念:.

    channel:操作MQ的工具.

    exchange:路由消息到队列中·

    queue:缓存消息

    virtual host:虚拟主机,是对queue、exchange等
    资源的逻辑分组

  7. 快速入门

    基本消息队列的消息发送流程:
    1.建立connection
    2.创建channel
    3.利用channel声明队列
    4.利用channel向队列发送消息

    基本消息队列的消息接收流程:
    1.建立connection
    2.创建channel
    3.利用channel声明队列
    4.定义consumer的消费行为handleDelivery()5.利用channel将消费者与队列绑定

SpringAMQP

什么是AMQP

AMQP是用于在应用程序或之间传递业务消息的开放标准该协议与语言和平台无关,更符合微服务中独立性的要求

Spring AMQP是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现。

消息的发送和接收(入门)

img

image-20230531143841728

消息发送:

  1. 配置rabbitAMOP

    1
    2
    3
    4
    5
    6
    7
    spring:
    rabbitmq:
    host: 192.168.136.131
    port: 5672
    username: root
    password: 510609
    virtual-host: /
  2. 新建测试类

    image-20230531144907807

  3. 编写发送

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class SpringAmqpTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendMessage2SimpleQueue(){
    String queueName="simple.queue";
    String message="hello,spring amqp!";
    rabbitTemplate.convertAndSend(queueName,message);
    }
    }

消息接收:

  1. 配置rabbitAMOP

    1
    2
    3
    4
    5
    6
    7
    spring:
    rabbitmq:
    host: 192.168.136.131
    port: 5672
    username: root
    password: 510609
    virtual-host: /
  2. 新建类

    image-20230531150742841

  3. 类中编写

    1
    2
    3
    4
    5
    6
    7
    @Component
    public class SpringRabbitListener {
    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg){
    System.out.println("消费者接收到的simple.queue:【"+msg+"】");
    }
    }

控制台返回:消费者接收到的simple.queue:【hello,spring amqp!】

work Queue工作队列

img

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

publisher在发送消息之后会将消息存放在queue中等待connection处理

默认轮询模式下,Work Queue会将生产者生产的消息一次性平均分配给consumer1和consumer2,当分配完消息后,它的自动确认机制会一次性全部确认。而不在乎自己能不能处理,届时就会出现消息堆积问题

而最好的方法就是在配置文件中添加消费策略

例子:

  1. 测试:发送消息50次

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Test
    public void testSendMessage2WorkQueue() throws InterruptedException {
    String queueName="simple.queue";
    String message="hello,message_ _";
    for (int i = 1; i <=50 ; i++) {
    rabbitTemplate.convertAndSend(queueName,message+i);
    Thread.sleep(20);
    }
    }
  2. 添加两个消费者

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Component
    public class SpringRabbitListener {
    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg) throws InterruptedException {
    System.out.println("消费者1 接收到的simple.queue:【"+msg+"】"+ LocalTime.now());
    Thread.sleep(20);
    }

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue2(String msg) throws InterruptedException {
    System.out.println("消费者2.....接收到的simple.queue:【"+msg+"】"+LocalTime.now());
    Thread.sleep(200);
    }
    }
  3. 解决消费策略问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    spring:
    rabbitmq:
    host: 192.168.136.131
    port: 5672
    username: root
    password: 510609
    virtual-host: /
    #表示每次只处理一条消息
    listener:
    simple:
    prefetch: 1

发布和订阅

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

常见exchange类型包括:

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

img

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

FanoutExchange

image-20230531165438796

所谓FanoutExchange就是把消息传递给交换机,而交换机会将消息分别发送给消息队列1和消息队列2

实现思路如下:

  1. 在consumer服务中,利用代码声明队列、交换机,并将两者绑定

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

  3. 在publisher中编写测试方法,向itcast.fanout发送消息

  4. 编写FanoutConfig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
public class FanoutConfig {
//itcast.fanout交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("itcast.fanout");
}

//fanout.queue1队列
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}

//fanout.queue2队列
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}

//绑定队列交换机
@Bean
public Binding fanoutBinding1(Queue fanoutQueue1,FanoutExchange fanoutExchange){
return BindingBuilder
.bind(fanoutQueue1)
.to(fanoutExchange);
}
}
  1. 编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) throws InterruptedException {
System.out.println("消费者接收到fanout.queue1的消息是:【"+msg+"】");
}

@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) throws InterruptedException {
System.out.println("消费者接收到fanout.queue2的消息是:【"+msg+"】");
}
}
  1. 在publisher中编写测试方法,向itcast.fanout交换机发送消息
1
2
3
4
5
6
@Test
public void testSendFanoutExchange() throws InterruptedException {
String exchangeName="itcast.fanout";
String message="hello,every one!";
rabbitTemplate.convertAndSend(exchangeName,"",message);
}

得到输出:

消费者接收到fanout.queue2的消息是:【hello,every one!】

消费者接收到fanout.queue1的消息是:【hello,every one!】

DirectExchange

DirectExchange交换机是指提供者在发送消息时携带着一个key,就像暗号一样。而队列中会定义一个key,当发送的消息经过交换机时,交换机会核对到底是哪个消息队列和key匹配。如果几个消息队列中都存在这个key就都可以得到消息。

实现思路如下:

  1. 在consumer服务中,编写两个消费者方法,利用@RabbitListener声明Exchange、Queue、RoutingKey

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Component
    public class SpringRabbitListener {
    @RabbitListener(bindings = @QueueBinding(
    value = @Queue(name="direct.queue1") ,
    exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
    key = { "red","blue"}
    ))
    public void listenDirectQueue1(String msg){
    System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】");
    }


    @RabbitListener(bindings = @QueueBinding(
    value = @Queue(name="direct.queue2") ,
    exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
    key = { "red","yellow"}
    ))
    public void listenDirectQueue2(String msg){
    System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");
    }
    }
  1. 在publisher中编写测试方法,向itcast. direct发送消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Test
    public void testSendDirectExchange( ){
    //交换机名称
    String exchangeName = "itcast.direct";
    //消息
    String message = "hello, blue ! ";
    //发送消息
    rabbitTemplate.convertAndSend(exchangeName, "blue", message);
    }

TopicExchange

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

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

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

image-20230531180547012

实现思路如下:

  1. 在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2并利用@RabbitListener声明Exchange、Queue、RoutingKey

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Component
    public class SpringRabbitListener {
    @RabbitListener(bindings = @QueueBinding (
    value = @Queue(name = "topic.queue1"),
    exchange = @Exchange(name = "itcast.topic",
    type = ExchangeTypes.TOPIC),
    key = "china.#"
    ))
    public void listenTopicqueue1(String msg) {
    System.out.println("消费者接收到topic.queue1的消息:【" + msg + "]");
    }

    @RabbitListener(bindings = @QueueBinding (
    value = @Queue(name = "topic.queue2"),
    exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
    key = "#.news"
    ))
    public void listenTopicqueue2(String msg) {
    System.out.println("消费者接收到topic.queue1的消息:【" + msg + "]");
    }
    }
  2. 在publisher中编写测试方法,向itcast.topic发送消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Test
    public void testSendTopicExchange() {
    //交换机名称
    String exchangeName = "itcast.topic";
    //消息
    String message ="传智教育在深交所上市了!是教育行业IPO第一股!";
    //发送消息
    rabbitTemplate.convertAndSend(exchangeName,"china.news",message);
    }

消息转换器

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

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

  • 发送消息:

  1. 我们在父工程引入依赖
1
2
3
4
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
  1. 我们在publisher启动类中声明MessageConverter:
1
2
3
4
@Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
  • 接收消息:
  1. 我们在consumer启动类中定义MessageConverter:
1
2
3
4
@Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
  1. 然后定义一个消费者,监听object.queue队列并消费消息:
1
2
3
4
@RabbitListener(queues = "object.queue")
public void listenObjectQueue(Map<String, Object> msg){
System.out.println("消费者接收到了object.queue:" + msg);
}
Other Articles
cover
微服务高级篇
  • 23/10/10
  • 17:10
  • 3.1k
  • 11
cover
Nginx服务器
  • 23/09/18
  • 18:15
  • 960
  • 3