续 消息队列
消息队列的特征
上次课介绍了消息队列的基本作用和功效
下面来介绍一下消息队列运行时的特征
- 利用异步的特性,提高服务器的运行效率,减少线程阻塞的时间
- 削峰填谷:在并发峰值的瞬间将信息保存到消息队列中,依次处理,不会因短时间需要处理大量请求而出现意外,在并发较少时在依次处理队列中的内容,直至处理完毕
- 消息队列的弊端:因为是异步执行,faq模块完成响应时,search模块可能还没有运行,这样的话就可能出现延迟的现象,如果不能接受这种延迟,就不要使用消息队列
我们在工作中常见的消息队列软件:
- ActiveMQ
- RabbitMQ
- RocketMQ(阿里)
- Kafka
kafka简介
Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。该项目的目标是为处理实时数据提供一个统一、高吞吐、低延迟的平台。Kafka最初是由LinkedIn开发,并随后于2011年初开源。
kafka软件结构
Producer: 消息的发送方,既消息的来源,是生产者
faq就是消息的发送方
Consumer:消息的接收方,既消息的去处,是消费者
search模块就是消息的接收方
Topic:就是话题或主题的意思,消息的发送方和接收方需要统一一个话题名称,才能不会错误的将消息发送给其它人,或错误的接收其它人的信息
Record:消息记录,就是生产者和消费者传递的消息,保存在topic中
faq模块和search模块中传递的信息是一个Question对象
kafka的安装
将下载的kafka压缩包在根目录解压
路径尽量短,否则运行时报错,路径不要有中文和空格
在当前目录下创建一个空目录,名称随意,但一定要是空的,
本次创建的目录名称为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命令,安装方式参考
创建项目测试kafka
创建knows-kafka项目
我们启动了kafka
但是现在暂时没有办法使用它
我们直接使用SpringBoot项目编写代码向它发送消息和接收消息,来进行测试
什么都不需要勾选,直接创建项目
父子相认
<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)”的代码块进行统一管理与扩展
什么是"切面"呢
程序中的切面指的就是程序中方法的互相调用
名词解释
-
切面(aspect):是一个可以加入额外代码运行的特定位置,一般指方法之间的调用,可以在不修改原代码的情况下,添加新的代码,对现有代码进行升级维护和管理
-
织入(weaving):选定一个切面,利用动态代理技术,为原有的方法的持有者生成动态对象,然后将它和切面关联,在运行原有方法时,就会按照织入之后的流程运行了
-
通知(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 面向对象编程