续 微服务(三)

148 阅读15分钟

续 消息队列

消息队列的特征

上次课介绍了消息队列的基本作用和功效

下面来介绍一下消息队列运行时的特征

  • 利用异步的特性,提高服务器的运行效率,减少线程阻塞的时间
  • 削峰填谷:在并发峰值的瞬间将信息保存到消息队列中,依次处理,不会因短时间需要处理大量请求而出现意外,在并发较少时在依次处理队列中的内容,直至处理完毕
  • 消息队列的弊端:因为是异步执行,faq模块完成响应时,search模块可能还没有运行,这样的话就可能出现延迟的现象,如果不能接受这种延迟,就不要使用消息队列

我们在工作中常见的消息队列软件:

  • ActiveMQ
  • RabbitMQ
  • RocketMQ(阿里)
  • Kafka

kafka简介

Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。该项目的目标是为处理实时数据提供一个统一、高吞吐、低延迟的平台。Kafka最初是由LinkedIn开发,并随后于2011年初开源。

kafka软件结构

image-20211221175200488.png

Producer: 消息的发送方,既消息的来源,是生产者

​ faq就是消息的发送方

Consumer:消息的接收方,既消息的去处,是消费者

​ search模块就是消息的接收方

Topic:就是话题或主题的意思,消息的发送方和接收方需要统一一个话题名称,才能不会错误的将消息发送给其它人,或错误的接收其它人的信息

Record:消息记录,就是生产者和消费者传递的消息,保存在topic中

faq模块和search模块中传递的信息是一个Question对象

kafka的安装

将下载的kafka压缩包在根目录解压

路径尽量短,否则运行时报错,路径不要有中文和空格

image-20211222093006282.png

在当前目录下创建一个空目录,名称随意,但一定要是空的,

本次创建的目录名称为data,它来保存kafka运行过程中的临时文件和日志文件

要知道,想启动kafka要先启动zookeeper

而zookeeper和kafka启动前都需要一些配置

F:\kafka\config下有文件zookeeper.properties

dataDir=F:/data

F:\kafka\config下有文件server.properties

log.dirs=F:/data

启动Kafka

先启动zookeeper

打开命令行界面

Win+R输入cmd

C:\Users\TEDU>F:

F:>cd kafka\bin\windows

F:\kafka\bin\windows>zookeeper-server-start.bat ....\config\zookeeper.properties

再启动kafka

打开命令行 Win+R输入cmd

C:\Users\TEDU>F:

F:>cd kafka\bin\windows

F:\kafka\bin\windows>kafka-server-start.bat ....\config\server.properties

Mac系统启动Kafka服务命令(参考):

# 进入Kafka文件夹
cd Documents/kafka_2.13-2.4.1/bin/
# 动Zookeeper服务
./zookeeper-server-start.sh -daemon ../config/zookeeper.properties 
# 启动Kafka服务
./kafka-server-start.sh -daemon ../config/server.properties 

Mac系统关闭Kafka服务命令(参考):

# 关闭Kafka服务
./kafka-server-stop.sh 
# 启动Zookeeper服务
./zookeeper-server-stop.sh

在启动kafka时有一个常见错误

wmic不是内部或外部命令

这样的提示,需要安装wmic命令,安装方式参考

zhidao.baidu.com/question/29…

创建项目测试kafka

创建knows-kafka项目

我们启动了kafka

但是现在暂时没有办法使用它

我们直接使用SpringBoot项目编写代码向它发送消息和接收消息,来进行测试

image-20211222102852674.png

什么都不需要勾选,直接创建项目

父子相认

<module>knows-kafka</module>

子项目依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>cn.tedu</groupId>
        <artifactId>knows</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.tedu</groupId>
    <artifactId>knows-kafka</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>knows-kafka</name>
    <description>Demo project for Spring Boot</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- Google JSON API -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>
        <!-- Kafka API -->
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </dependency>
    </dependencies>
</project>

knows-kafka项目的application.properties文件

# 配置kafka的位置
spring.kafka.bootstrap-servers=localhost:9092

# spring-kafka要求我们配置一个分组名称
# 以便于即使不同的项目有相同的话题名称,也能区分它们
# 这个分组名称必须定义,否则报错
spring.kafka.consumer.group-id=knows

# 日志门槛
logging.level.cn.tedu.knows.kafka=debug

SpringBoot启动类

@SpringBootApplication
// 启动当前项目对kafka的支持
@EnableKafka
// 启动SpringBoot框架内部的定时任务功能
// 和kafka没有必然依赖关系,只是需要测试时使用它
@EnableScheduling
public class KnowsKafkaApplication {

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

}

编写消息的发送

要发送消息一定要有一个承载消息的对象

我们在项目中创建一个vo包,包中定义Message类

代码如下

@Data
@Accessors(chain = true)
public class Message implements Serializable {

    private Integer id;
    private String content;
    private Long time;
}

下面来编写发送消息的代码

我们的设计是编写一个类,类中编写一个周期运行的方法

每隔10秒向kafka发送一条消息,消息就是一个Message对象的信息

创建一个demo包,包中创建一个Producer类,代码如下

@Component
@Slf4j
public class Producer {

    // 从Spring容器中获得能够操作kafka的对象
    // 这个对象是Spring-kafka框架提供的,我们无需编写
    // KafkaTemplate<[话题名称的类型],[传递消息的类型]>
    @Resource
    private KafkaTemplate<String,String> kafkaTemplate;

    // 编写一个每隔10秒运行一次的方法
    // 每次运行会向kafka发送一条消息
    int i=1;
    @Scheduled(fixedRate = 10000)
    public void sendMessage(){
        // 实例化Message对象用户消息的发送
        Message message=new Message()
                .setId(i++)
                .setContent("这是发送给Kafka的消息")
                .setTime(System.currentTimeMillis());
        // 实例化转换json格式字符串的工具类
        Gson gson=new Gson();
        String json=gson.toJson(message);
        log.debug("即将发送消息:{}",json);
        // 执行消息的发送
        kafkaTemplate.send("myTopic",json);
        log.debug("消息已发送!");

    }

}

编写消息的接收

继续在demo包中创建一个类Consumer

类中编写接收消息的方法

代码如下

@Component
@Slf4j
public class Consumer {

    // 我们添加的Spring-kafka依赖中包含了一个kafka监听器
    // 当指定话题有消息出现时,这个监听器就会自动调用对应话题接收的方法
    // 还会将话题中的消息赋值给对应方法的参数
    @KafkaListener(topics = "myTopic")
    public void receive(ConsumerRecord<String,String> record){
        // 根据监听器的作用,主要话题"myTopic"中出现新的消息,这个方法就会被调用执行
        // 参数record就是发送到"myTopic"话题中的消息
        // ConsumerRecord<[话题名称的类型],[消息的类型]>
        // 获得record中的消息内容
        String json=record.value();
        // 将json字符串转回为java对象
        Gson gson=new Gson();
        Message message=gson.fromJson(json, Message.class);
        // 日志输出得到的java对象
        log.debug("接收到的消息:{}",message);
    }

}

编写完之后

直接启动项目

观察控制台,每10秒发送一次信息

实现新增问题同步到ES

我们需要利用上面章节学习的kafka来完善达内知道项目新增问题并新增到ES的功能

  • faq模块新增question时,将question对象发送给kafka
  • search模块监听kafka中保存question对象的话题,当有消息时,将question读取并保存到ES

统一话题名称

faq模块和search模块是两个不同的项目

话题名称的对应是能够在kafka中相互通信的唯一保证

我们一般会在comments模块中,定义一个常量,供这两个项目使用

两个项目调用相同的一个常量值,他们的话题名称一定是一致的

转到knows-commons模块

创建一个vo包,包中创建一个Topic类

类中编写常量代码如下

public class Topic {

    public static final String QUESTION="add_question_es";
}

其实这个类中的常量名称和常量的值都不重要,关键是有了它,两个项目(生产者和消费者)能指向同一个常量

faq模块发送消息

转到knows-faq模块

先添加相关依赖

<!-- Google JSON API -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
</dependency>
<!-- Kafka API -->
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

application.properties

# kafka相关配置
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=knows

SpringBoot启动类

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("cn.tedu.knows.faq.mapper")
// ↓↓↓↓↓↓↓↓↓↓
@EnableKafka
public class KnowsFaqApplication {
      // 其它代码略
}

下面开始编写faq模块将question对象发送给kafka的代码

先创建一个kafka包

在包中创建一个QuestionProducer的类,完成这个工作

代码如下

@Component
@Slf4j
public class QuestionProducer {

    @Resource
    private KafkaTemplate<String,String> kafkaTemplate;
    //将问题信息发送到kafka中
    public void sendQuestion(Question question){
        // 将question对象转换为json字符串发送到kafka
        Gson gson=new Gson();
        String json=gson.toJson(question);
        log.debug("发送问题内容:{}",json);
        kafkaTemplate.send(Topic.QUESTION,json);
    }

}

我们找到QuestionServiceImpl类,添加上面类的依赖注入,然后找到用户新增问题方法的最后

添加调用上面方法的代码

这样就能实现新增的问题发送给kafka了

@Resource
private QuestionProducer questionProducer;

//此处省略很多代码...

@Override
@Transactional 
public void saveQuestion(String username, QuestionVo questionVo) {
    //此处省略将问题保存到数据库的过程 ...
    
    //将刚刚新增完成的问题,发送到kafka
    //以便search模块接收并新增到Es
    questionProducer.sendQuestion(question);
}

消息的接收

转到knows-search模块

和faq模块一样,也要添加相关依赖和配置

先添加相关依赖

<!-- Google JSON API -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
</dependency>
<!-- Kafka API -->
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

application.properties

# kafka相关配置
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=knows

SpringBoot启动类

@SpringBootApplication
@EnableDiscoveryClient
// ↓↓↓↓↓↓↓↓↓↓
@EnableKafka
public class KnowsSearchApplication {
      // 其它代码略
}

相关配置完毕

我们需要将kafka监听器调用指定方法看做是一个特殊的控制层方法

这个控制层方法需要调用业务逻辑层方法完成新增

然而我们当前search模块没有新增QuestionVO到ES的业务逻辑层方法

所以我们要先在search模块的业务逻辑层方法中新增它

IQuestionService接口添加新增QuestionVO到ES的方法

// 新增QuestionVO到ES的业务逻辑层方法
void saveQuestion(QuestionVO questionVO);

QuestionServiceImpl实现类

@Override
public void saveQuestion(QuestionVO questionVO) {
    questionRepository.save(questionVO);
}

在controller包中新建类QuestionConsumer

这个类中编写kafka监听器,使用监听器的功能,在kafka指定话题出现信息时,执行新增操作

@Component
@Slf4j
public class QuestionConsumer {
    @Resource
    private IQuestionService questionService;
    // kafka监听器声明指定监听的话题
    @KafkaListener(topics = Topic.QUESTION)
    public void questionReceive(ConsumerRecord<String,String> record){
        // 从kafka中获得传递到当前项目的消息
        String json=record.value();
        Gson gson=new Gson();
        QuestionVO questionVO=gson.fromJson(json,QuestionVO.class);
        // 调用业务逻辑层执行新增
        questionService.saveQuestion(questionVO);
        log.debug("成功的新增了问题到ES:{}",questionVO);
    }

}

到此为止功能代码就编写完毕了

下面要进行测试

Nacos\Redis\ES\Zookeeper\Kafka

gateway\auth\sys\faq\search

client

上面服务都要起

Kafka 概念总结

  • Kafka 是消息队列服务器,其核心目的实现业务系统之间进行异步消息通讯;
  • 利用Kafka可以将业务层过程中的分支流程优化为异步处理,提升软件的性能;
  • Kafka数据发送端称为:消息生产者、或者消息发布者;
  • Kafka数据接收端称为:消息消费者、或者消息订阅者;
  • 队列服务器可以解决数据的“生产者和消费者”问题;
  • 队列服务器也有人称为发布于订阅模型;
  • **“生产者和消费者”**问题:是指数据生产和数据消费速度不一致会造成数据堆积的矛盾,这种矛盾的解决常用方式就是采用队列进行缓存,然后再进行异步处理。也称为“削峰填谷”,也就是把数据高峰缓存到队列中,在一步一步处理平滑处理。

AOP面向切面编程

什么是AOP

AOP:

面向切面的程序设计(Aspect Oriented Programming)又译作剖面导向程序设计

和OOP一样,也是计算机开发的一种程序设计思想

一句话概括面向切面编程

就是在不修改程序现有代码的前提下,可以设置某个方法运行之前或运行之后新增额外代码的操作

目标是将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与扩展

什么是"切面"呢

程序中的切面指的就是程序中方法的互相调用

image-20211222152901695.png

名词解释

  • 切面(aspect):是一个可以加入额外代码运行的特定位置,一般指方法之间的调用,可以在不修改原代码的情况下,添加新的代码,对现有代码进行升级维护和管理

  • 织入(weaving):选定一个切面,利用动态代理技术,为原有的方法的持有者生成动态对象,然后将它和切面关联,在运行原有方法时,就会按照织入之后的流程运行了

捕获.PNG

  • 通知(advice)

    通知要织入的代码的运行时机

    • 前置通知(before advice)
    • 后置通知(after advice)
    • 环绕通知(around advice)
    • 异常通知(after throwing advice)

使用Spring实现AOP

实际上,我们在开发达内知道项目的过程中,很多框架中都包含了AOP思想

也都实现了不修改代码就能新增各种功能的情况

例如

  • 过滤器
  • 拦截器
  • Spring-Security(底层通过过滤器实现)
  • .....

Spring框架两大功能

1.Ioc\DI

2.Aop

明确Aop是一种编程思想,不是只有Spring能实现

Spring是现在流行的使用Aop编写程序的解决方案

我们课程中使用Aop注解实现Aop程序

所以要先添加依赖

转到faq模块

pom.xml文件添加依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>

创建aspect包

DemoAspect类

代码如下

// 编写Aop配置的类必须保存在Spring容器中才能使配置生效
@Component
// 下面的注解表示当前类是一个编写设计切面的类
// 专门编写Aop代码,不编写一般业务
@Aspect
public class DemoAspect {

    // 1.定义切面
    // 定义切面就是指定一个方法
    // 我们可以在这个方法运行前或运行后等位置织入额外代码
    @Pointcut("execution(public * cn.tedu.knows.faq.controller" +
            ".TagController.tags(..))")
    // 这个注解定义了一个方法的切面,后面必须跟一个方法的声明
    // 这个方法就是为了单纯的定义这个切面的名称,不需要写任何代码
    public void pointCut(){}

    // 2.织入内容
    // 向指定切面的方法前\后添加代码
    // 我们这里先测试前置advice,在tags方法运行之前输出指定内容
    @Before("pointCut()")
    public void before(){
        // 这个方法的内容会在切面方法运行之前运行
        System.out.println("前置advice执行");
    }

}

启动Nacos和faq

浏览器输入http://localhost:8002/v2/tags

如果能在faq的控制台看到前置的输出表示一切正常

其他advice类型和织入方法的参数

// 编写Aop配置的类必须保存在Spring容器中才能使配置生效
@Component
// 下面的注解表示当前类是一个编写设计切面的类
// 专门编写Aop代码,不编写一般业务
@Aspect
public class DemoAspect {

    // 1.定义切面
    // 定义切面就是指定一个方法
    // 我们可以在这个方法运行前或运行后等位置织入额外代码
    @Pointcut("execution(public * cn.tedu.knows.faq.controller" +
            ".TagController.tags(..))")
    // 这个注解定义了一个方法的切面,后面必须跟一个方法的声明
    // 这个方法就是为了单纯的定义这个切面的名称,不需要写任何代码
    public void pointCut(){}

    // 2.织入内容
    // 向指定切面的方法前\后添加代码
    // 我们这里先测试前置advice,在tags方法运行之前输出指定内容
    @Before("pointCut()")
    public void before(JoinPoint joinPoint){
        // 这个方法的内容会在切面方法运行之前运行
        System.out.println("前置advice执行");
        // JoinPoint可以作为advice方法的参数来声明
        // joinPoint对象会包含当前切面的各种信息
        // 经常使用到的是获取当前切面对应方法的方法信息
        //joinPoint.getSignature() 就能获得当前切面对应方法的信息
        System.out.println(joinPoint.getSignature().getName());
    }

    // 后置advice
    @After("pointCut()")
    public void after(){
        System.out.println("后置advice执行");
    }

    //异常advice(只有目标方法发生异常时才会执行)
    @AfterThrowing("pointCut()")
    public void throwing(){
        System.out.println("方法发生异常");
    }

    //环绕advice
    @Around("pointCut()")
    // 环绕通知要想正常运行,必须设置方法的参数
    // 而且参数类型为ProceedingJoinPoint
    // ProceedingJoinPoint是JoinPoint的子接口,拥有更多的方法
    // 这个参数的功能是可以在环绕advice中调用我们切面的方法
    // 这个方法还要有返回值,因为我们调用的切面的方法可能有返回值
    // 环绕advice如果不返回这个值,调用者就接收不到这个值了
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕advice,运行之前");
        // ProceedingJoinPoint类型的参数具有调用切面方法的功能
        Object obj=joinPoint.proceed();
        System.out.println("环绕advice,运行之后");
        return obj;
    }
}

切面定义语法

上面章节中我们使用了下面的表达式来定义切面

@Pointcut("execution(public * cn.tedu.knows.faq." +
        "controller.TagController.tags(..))")

下面来介绍这个切面定义表达式的详细语法规则

语法模板为:

execution(
modifier-pattern?
ret-type-pattern
declaring-type-pattern?
name-pattern(param-pattern)
throws-pattern?)

带?的是可选属性,不带?是必须写的

  • modifier-pattern:访问修饰符(可选)
  • ret-type-pattern:返回值类型(必写)
  • declaring-type-pattern:全路径类名(可选)
  • name-pattern:方法名(必写)
  • param-pattern:参数列表(必写)
  • throws-pattern:抛出的异常类型(可选)

分析下面几个切面表达式的含义

execution(* *(..)):
匹配全部方法:Spring框架能扫描到的范围内的全部方法
execution(public * com.test.TestController.*(..)):
匹配com.test.TestController类中所有被public修饰的方法
execution(* cn.tedu.knows.faq.*.*(..)):
匹配cn.tedu.knows.faq包中的所有类的所有方法

AOP实现业务逻辑层性能显示

我们下面要通过我们学习的Spring提供的AOP功能

实现将faq模块中所有业务逻辑层方法运行的用时记录并显示出来

我们可以将当前faq模块service包下的所有方法作为切面方法来定义

然后使用环绕advice进行Aop处理,记录时间,输出时间差

aspect包下创建TimeAspect

代码如下

@Component
@Aspect
public class TimeAspect {
    // 匹配service包中的所有接口的所有方法
    @Pointcut("execution(public * cn.tedu.knows.faq.service.*.*(..))")
    public void timer(){}

    // 环绕advice
    @Around("timer()")
    public Object timeRecord(ProceedingJoinPoint joinPoint) throws Throwable {
        // 开始时间
        long start = System.currentTimeMillis();
        // 调用切面方法
        Object obj=joinPoint.proceed();
        // 结束时间
        long end = System.currentTimeMillis();
        // 计算时间差
        long time=end-start;
        // 获得切面对应的方法名
        String methodName=joinPoint.getSignature().getName();
        System.out.println(methodName+"用时"+time+"ms");
        return obj;

    }

}

随笔

kafka和zookeeper

zoo keeper

动物园 管理者

动物管理员

zoo keeper是一个能够集中当前系统中所有软件配置文件的软件

到了后期很多软件

干脆将自己有配置文件的功能删除了,只找zookeeper

kafka就是这样的软件

所有配置需要保存到zookeeper中

英文

Cluster:集群

send:发送

receive:接收

Aspect :切面

execution: 可执行的

execute:执行

{"id":4,"content":"这是发送给Kafka的消息","time":1642736447062}

Aspect Oriented Programming 面向切面编程

Object Oriented Programming 面向对象编程