简述

最近看了这部分的网课,但网课只是大概了解了一下这部分的基础知识,后期还会专门找视频学习
有些人喜欢看书学习,有些人喜欢看视频学习,我觉得初期了解一样东西,看视频更好,后期熟悉了这部分内容
可能看书能学到更多的东西,话不多说,直接来进入正题。

Docker

Docker一些基础命令我就不介绍了,我会把docker基础内容pdf放到github上,
这里我记录一下docker部署一个前后端分离项目,使用基本的Dockerfile部署项目,没有使用Docker-compose的方式
下次有时间尝试一下使用Docker-compose方式部署项目,到时候再记录一下

回归正题,使用docker部署前后端分离的项目步骤如下:

  • 先部署mysql容器

    • 拉取mysql镜像:docker pull mysql

    • 配置宿主机和容器的映射文件
      这样我们可以通过修改宿主机文件从而修改mysql容器中的数据和配置

      //1,创建三个宿主机映射文件,在/dockerFile/myproject/arsystem/arsystem-mysql/下
      mkdir data logs conf
        
      //2,在该conf目录下创建一个my.cnf文件,这会覆盖掉mysql容器中的配置文件
        cd conf
        touch my.cnf
    • 创建mysql容器

      docker run  \
      --name arsystem-mysql \
      -p 3306:3306 \
      -v /dockerFile/myproject/arsystem/arsystem-mysql/data:/var/lib/mysql \
      -v /dockerFile/myproject/arsystem/arsystem-mysql/conf:/etc/mysql/conf.d \
      -v /dockerFile/myproject/arsystem/arsystem-mysql/logs:/var/log/mysql \
      -e MYSQL_ROOT_PASSWORD=123765 \
      -d mysql
      
      //上述,mysql最新版,所以没有加tag版本号,设置了mysql密码,进行了文件映射,完美~
      //上述宿主机文件路径一定要是绝对路径,不然会当成数据卷
    • 创建mysql容器后,使用sqlyog连接该数据库,账号:root,密码:123765,IP地址就是宿主机IP地址
      你可能出现连接不上远程mysql的情况:

      连接可能爆:Authentication plugin 'caching_sha2_password' cannot be loaded错误
      报错原因:
        MySQL8.0以后,身份验证插件默认使用caching_sha2_password,旧版则是mysql_native_password
        这可能造成一些MySQL客户端连接不上的问题,从而报错:plugin caching_sha2_password could not be loaded
      解决办法:
        1,登录mysql容器中的mysql:
          docker exec -it arsystem-mysql bash
          mysql -uroot -p 输入123765(密码)
        2,执行以下命令:
          #用于更换MySQL root账户的密码和加密方式
          ALTER USER 'root' IDENTIFIED WITH mysql_native_password BY '123765';
          #用于刷新权限
          FLUSH PRIVILEGES;
    • 使用sqlyog连接远程mysql后,根据xxx.sql文件创建数据库和表

  • 接着部署springboot容器

    • 首先修改项目中数据库的地址
      修改项目中mysql的IP地址为mysql容器的地址,即宿主机地址,然后再将项目打成jar包

    • 配置springboot项目Dockerfile文件

      //创建Dockerfile文件,并将项目jar包放在同一目录下,Dockerfile文件内容如下:
      FROM java:8
      ADD arsystem-0.0.1-SNAPSHOT.jar app.jar
      RUN bash -c 'touch /app.jar'
      ENTRYPOINT ["java","-jar","/app.jar"]
      
      //根据java8镜像获取底层依赖,将项目jar包加入镜像文件系统的根目录下并改名称app.jar
      //第三句是修改文件的创建修改时间,最后一句是运行我们的项目
    • 根据Dockerfile文件制作镜像

      docker build -t arsystem-backend .
      //-t 表示设置项目的名字和版本号,版本号不设置则为latest
      //. 表示Dockerfile的位置,.表示Dockerfile就在当前目录下
    • 启动项目容器

      制作好项目镜像后,查看:docker images
      然后根据该镜像启动一个容器:
      docker run \
      --name arsystem-backend \
      -p 8081:8081 \
      -d arsystem-backend
      
      //当然,我觉得在Dockerfile中应该要暴露8081端口的,这里我漏了
      //另外我的Dockerfile并不好,可能需要配置一些其他宿主机文件映射啥的
  • 最后配置nginx容器

    • 拉取nginx镜像,创建宿主机映射文件

      1,拉取nginx镜像:docker pull nginx
      2,创建宿主机映射文件,这里有两个地方需要映射,
        第一个是nginx容器的配置文件目录,/etc/nginx/conf.d目录
        第二个是前端vue文件在nginx容器中存放的目录,/usr/share/nginx/html目录
        所以我们需要把vue项目生成的dist目录拿到宿主机上,
        我把dist目录在了/dockerFile/myproject/arsystem/arsystem-frontend下,用来映射html目录
        然后在dist同级目录创建好nginx-config目录,用来映射容器中conf.d目录
    • 宿主机nginx-config目录用来映射nginx容器的conf.d目录,该目录下所有*.conf文件都会被加载进
      nginx配置文件nginx.conf中,所以我们需要在nginx-config文件中创建一个nginx配置文件

      //nginx-config目录下创建keyi.conf,该文件内容如下:
      server {
      	listen 80;
      	server_name 47.98.138.53;
      	location / {
              root   /usr/share/nginx/html;
              index  index.html index.htm;
          }
      }
      //请求一进来,被转到html目录下的index.html中
    • 创建nginx容器

      //宿主机映射目录创建好,nginx配置文件创建好后,下面就创建nginx容器了
      docker run \
      > --name arsystem-nginx \
      > -p 80:80 \
      > -v /dockerFile/myproject/arsystem/nginx-config/:/etc/nginx/conf.d \
      > -v /dockerFile/myproject/arsystem/arsystem-frontend/dist/:/usr/share/nginx/html/ \
      > -d nginx
      
      //将宿主机映射目录挂载到nginx容器目录中,这样,我们就可以通过修改keyi.conf来修改容器中的nginx配置文件
      //我们的vue项目也被部署在nginx中,当我们访问http://宿主机ip/,就被转发到html目录下index.html中
  • 以上就是docker部署前后端分离项目的步骤,其中还有很多注意的地方,

    例如docker容器文件挂载问题
      如果宿主机目录有文件,容器目录中有文件会导致容器目录中文件被覆盖,如果宿主机目录没有文件
      而容器目录中有文件,则容器内文件会拷贝到宿主机目录中
    例如docker创建容器时,宿主机目录务必是绝对路径,不然会被识别成数据卷
    例如docker的文件挂载最好是目录挂载目录,如果是文件挂载文件,可能导致宿主机文件修改后,容器内文件没有被修改

    下面是一些大佬docker文章

    最后是网课中关于docker的基础笔记PDF:docker笔记,如果访问不了尝试使用外网访问。

RabbitMQ

RabbitMQ,一款消息队列工具,让我们可以实现异步通信,更多具体内容见最下方的Rabbit基础文档
我大概了解了一下Rabbit基本概念,SpringAMQP的基本使用,队列,和3种交换机(分发规则不同)的使用

本小节不深入RabbitMQ,只记录下RabbitMQ的5个基本消息模型,使用SpringAMQP实现一下这5种模型
以后有机会将专门找这类网课学习,本次实验通过docker搭建rabbitMQ服务。

配置rabbitMQ:

//拉取rabbitMQ镜像
docker pull rabbitmq:3-management
//开启rabbitMQ容器
docker run -d -p 5672:5672 -p 15672:15672 --name my-rabbit rabbitmq:3-management

配置依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

配置application.yaml:

spring:
  rabbitmq:
    host: 47.98.138.53
    port: 5672
    virtual-host: /
    username: wanyi
    password: 123765
    #rabbitMQ初始用户名和密码为:guest

5种基本消息模型:

  • BasicQueue,基本消息模型

    • 发送方

      @Autowired
      private RabbitTemplate rabbitTemplate;
      
      @Test
      public void simpleQueueProvider(){
        String queueName = "simple.queue";
        String msg = "这里是简单消息队列测试消息";
        rabbitTemplate.convertAndSend(queueName,msg);	//参数应该少给了,rabbit需要有该队列
      }
    • 接收方

      @Component
      public class MyRabbitMQConsumerComponent {
      	/*simpleQueue测试*/
        @RabbitListener(queues = "simple.queue")
        public void simpleQueueTest(String msg){
        	System.out.println("simple.queue----接收消息为:"+msg);
        }
      }
  • WorkQueue,工作消息模型

    • 发送方

      @Autowired
      private RabbitTemplate rabbitTemplate;
      
      //发送50条消息
      @Test
      public void workQueueProvider(){
        String queueName = "work.queue";
        String msg = "工作队列第";
        for (int i = 1; i <= 50; i++) {
          rabbitTemplate.convertAndSend(queueName,msg+i+"条消息");
          try {
            Thread.sleep(20);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    • 接收方

      /*workQueue测试,一个queue,两个消费者*/
      @RabbitListener(queues = "work.queue")
      public void workQueue1Test(String msg) throws InterruptedException {
        System.out.println("work.queue----消费者1接收消息为:"+msg+ LocalTime.now());
        Thread.sleep(20);
      }
      @RabbitListener(queues = "work.queue")
      public void workQueue2Test(String msg) throws InterruptedException {
        System.err.println("work.queue----消费者2接收消息为:"+msg+ LocalTime.now());
        Thread.sleep(200);
      }
      /*
      	我们可以看到,queue1消费者比queue2消费者处理数据更快,本该处理更多请求,但由于rabbitMQ中消费者
      	可以预取消息,即消费者处理之前,先把消息平均分配好给各个消费者,所以导致queue1和queue2
      	处理的消息数量一样,并没有利用好queue1的优势,我们可以通过配置,实现每次预取从默认无上限
      	改成每次预取1条消息
      */
      spring:
        rabbitmq:
          host: 47.98.138.53
          port: 5672
          virtual-host: /
          username: wanyi
          password: 123765
          listener:
            simple:
              prefetch: 1  #控制消费者预取的消费数量,改成1,表示每个消费者先从消息队列中拿一个消息。
  • FanoutExchange,订阅模型之Fanout
    订阅模式中使用到Exchange,交换机,其作用:交换机本身不存储消息,而是接收消息并路由给队列

    根据路由的规则不同大致分为FanoutExchange,DirectExchange,TopicExchange三种,它们区别如下:

    fanoutExchange会将消息发给每一个绑定的队列
    directExchange会将接收到的消息根据规则(routingKey)路由到指定的Queue,因此称为路由模式(routes)
    topicExchange,和directExchange类似,不同的是,topicExchange的routingKey可以设置成通配符的形式,简化操作
    例如如果队列routingKey为:china.#   则匹配所有key是china.xxx的消息,类似正则匹配,#代表0或多个单词,*代表一个单词
    • 发送方

      @Test
      public void fanoutExchangeProvider(){
        String exchangeName = "wanyi.fanout";
        String msg = "hello everyone!";
        rabbitTemplate.convertAndSend(exchangeName,"",msg);
      }
      /*
      	第二个参数是routingKey,如果不指定则是向每个关联的队列发送消息,
      	如果指定则是向带有该key的队列发送消息
      */
    • 接收方
      接收方需要配置一下队列与交换机的关联,配置方式有两种,一种是通过注解的方式,一种是通过
      注册bean的方式,FanoutExchange案例这里通过注册bean的方式实现,下面DirectExchange,Topic通过注解实现

      //FanoutConfig
      @Configuration
      public class FanoutConfig {
          //交换机
          @Bean
          public FanoutExchange fanoutExchange(){
              return new FanoutExchange("wanyi.fanout");
          }
      
          //两个队列
          @Bean
          public Queue fanoutQueue1(){
              return new Queue("fanout.queue1");
          }
          @Bean
          public Queue fanoutQueue2(){
              return new Queue("fanout.queue2");
          }
      
          //队列绑定到交换机上
          @Bean
          public Binding fanoutBinding1(FanoutExchange fanoutExchange, Queue fanoutQueue1){
              return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
          }
          @Bean
          public Binding fanoutBinding2(FanoutExchange fanoutExchange, Queue fanoutQueue2){
              return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
          }
      }

      接收消息:

      /*fanoutExchange,两个queue,每个queue一个消费者*/
      @RabbitListener(queues = "fanout.queue1")
      public void fanoutQueue1Test(String msg){
        System.out.println("fanout.queue1----接收消息为:"+msg);
      }
      @RabbitListener(queues = "fanout.queue2")
      public void fanoutQueue2Test(String msg){
        System.out.println("fanout.queue2----接收消息为:"+msg);
      }
  • DirectExchange,订阅模型之Direct

    • 发送方

      @Test
      public void directExchangeProvider(){
        String exchangeName = "wanyi.direct";
        String msg = "hello red!!!";
        rabbitTemplate.convertAndSend(exchangeName,"red",msg);
      }
      //与DirectExchange没什么差别,无非指定某些队列发送消息
    • 接收方

      /*directExchange,两个queue,每个queue一个消费者*/
      @RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "direct.queue1"),
        exchange = @Exchange(name = "wanyi.direct",type = ExchangeTypes.DIRECT),
        key = {"red","blue"}
      ))
      public void directQueue1Test(String msg){
        System.out.println("direct.queue1----接收消息为:"+msg);
      }
      
      @RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "direct.queue2"),
        exchange = @Exchange(name = "wanyi.direct",type = ExchangeTypes.DIRECT),
        key = {"red","yellow"}
      ))
      public void directQueue2Test(String msg){
        System.err.println("direct.queue2----接收消息为:"+msg);
      }
  • TopicExchange,订阅模型之Topic

    • 发送方

      @Test
      public void topicExchangeProvider(){
        String exchangeName = "wanyi.topic";
        String msg = "china fake new !!!";
        rabbitTemplate.convertAndSend(exchangeName,"china.news",msg);
      }
    • 接收方

      /*directExchange,两个queue,每个queue一个消费者*/
      @RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "topic.queue1"),
        exchange = @Exchange(name = "wanyi.topic",type = ExchangeTypes.TOPIC),
        key = {"china.#"}
      ))
      public void topicQueue1Test(String msg){
        System.out.println("topic.queue1----接收消息为:"+msg);
      }
      
      @RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "topic.queue2"),
        exchange = @Exchange(name = "wanyi.topic",type = ExchangeTypes.TOPIC),
        key = {"#.news"}
      ))
      public void topicQueue2Test(String msg){
        System.err.println("topic.queue2----接收消息为:"+msg);
      }
      //接收方使用key进行匹配消息

上述便是5种基本消息模型的案例,最后说一下消息转换器,我们可以通过消息队列工具发送任意类型消息
对象,map等等,底层使用JDK的序列化,这种方式数据体积大,可读性差,解决办法就和SpringMVC一样
SpringMVC使用了Jackson做数据格式转换,所以我们也需要配置一个Jackson的消息转换器

  • 添加依赖

    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
        <version>2.9.10</version>
    </dependency>
  • 将消息转换器注册进容器中

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

最后rabbitMQ的基本资料已经放在github上了,点击下载:rabbitMQ笔记