一、消息队列
消息队列,英文名:Message Queue,经常缩写为MQ。从字面上来理解,消息队列是一种用来存储消息的队列。消息队列就是将需要传输的数据存放在队列中
消息队列中间件:消息队列中间件就是用来存储消息的软件(组件)
市面上的消息队列有很多,例如:Kafka、RabbitMQ、ActiveMQ、RocketMQ、ZeroMQ等
二、消息队列的应用场景
2.1 异步处理
2.2 系统解耦
2.3 流量削峰
2.4 日志处理(大数据领域常见)
大型电商网站(淘宝、京东、...)、App(抖音、美团、滴滴等)等需要分析用户行为,要根据用户的访问行为来发现用户的喜好以及活跃情况,需要在页面上收集大量的用户访问信息
三、消息队列的模式
3.1 点对点模式
点对点模式特点:
每个消息只有一个接收者(Consumer)(即一旦被消费,消息就不再在消息队列中)
发送者和接收者间没有依赖性,发送者发送消息之后,不管有没有接收者在运行,都不会影响到发送者下次发送消息;
接收者在成功接收消息之后需向队列应答成功,以便消息队列删除当前接收的消息;
3.2 发布订阅模式
发布/订阅模式特点:
每个消息可以有多个订阅者;
发布者和订阅者之间有时间上的依赖性。针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息。
为了消费消息,订阅者需要提前订阅该角色主题,并保持在线运行;
四、kafka
4.1 kafka是什么
Apache Kafka是一个分布式流平台。一个分布式的流平台应该包含3点关键的能力:
- 发布和订阅流数据流,类似于消息队列或者是企业消息传递系统
- 以容错的持久化方式存储数据流
- 处理数据流
- Producers:可以有很多的应用程序,将消息数据放入到Kafka集群中。
- Consumers:可以有很多的应用程序,将消息数据从Kafka集群中拉取出来。
- Connectors:Kafka的连接器可以将数据库中的数据导入到Kafka,也可以将Kafka的数据导出到数据库中。
- Stream Processors:流处理器可以Kafka中拉取数据,也可以将数据写入到Kafka中。
4.2 kafka基本概念
4.2.1topic主题
Topic 被称为主题,在 kafka 中,使用一个类别属性来划分消息的所属类,划分消息的这个类称为 topic。topic 相当于消息的分配标签,是一个逻辑概念。主题好比是数据库的表,或者文件系统中的文件夹。
4.2.2 partition 分区
partition 译为分区,topic 中的消息被分割为一个或多个的 partition,它是一个物理概念,对应到系统上的就是一个或若干个目录,一个分区就是一个 提交日志。消息以追加的形式写入分区,先后以顺序的方式读取。
注意:由于一个主题包含无数个分区,因此无法保证在整个 topic 中有序,但是单个 Partition 分区可以保证有序。消息被迫加写入每个分区的尾部。Kafka 通过分区来实现数据冗余和伸缩性
分区可以分布在不同的服务器上,也就是说,一个主题可以跨越多个服务器,以此来提供比单个服务器更强大的性能。
每一个分区都是一个顺序的、不可变的消息队列, 并且可以持续的添加。分区中的消息都被分了一个序列号,称之为偏移量(offset),在每个分区中此偏移量都是唯一的。
分区策略 | 说明 |
轮询策略 | 按顺序轮流将每条数据分配到每个分区中 |
随机策略 | 每次都随机地将消息分配到每个分区 |
按键保存策略 | 生产者发送数据的时候,可以指定一个key,计算这个key的hashCode值,按照hashCode的值对不同消息进行存储 |
4.2.3 segment段
Segment 被译为段,将 Partition 进一步细分为若干个 segment,每个 segment 文件的大小相等。
4.2.4 broker
Kafka 集群包含一个或多个服务器,每个 Kafka 中服务器被称为 broker。broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。broker 为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘上的消息。
broker 是集群的组成部分,每个集群中都会有一个 broker 同时充当了 集群控制器(Leader)的角色,它是由集群中的活跃成员选举出来的。每个集群中的成员都有可能充当 Leader,Leader 负责管理工作,包括将分区分配给 broker 和监控 broker。集群中,一个分区从属于一个 Leader,但是一个分区可以分配给多个 broker(非Leader),这时候会发生分区复制。这种复制的机制为分区提供了消息冗余,如果一个 broker 失效,那么其他活跃用户会重新选举一个 Leader 接管。
4.2.5 producer生产者
生产者,即消息的发布者,其会将某 topic 的消息发布到相应的 partition 中。生产者在默认情况下把消息均衡地分布到主题的所有分区上,而并不关心特定消息会被写到哪个分区。不过,在某些情况下,生产者会把消息直接写到指定的分区。
4.2.6 consumer消费者
消费者,即消息的使用者,一个消费者可以消费多个 topic 的消息,对于某一个 topic 的消息,其只会消费同一个 partition 中的消息
4.3 Java编程操作kafka
4.3.1 创建kafka-demo项目,导入依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</dependency>
4.3.2 生产者发送消息
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;
/**
* 生产者
*/
public class ProducerQuickStart {
public static void main(String[] args) {
//1.kafka的配置信息
Properties properties = new Properties();
//kafka的连接地址
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.200.130:9092");
//发送失败,失败的重试次数
properties.put(ProducerConfig.RETRIES_CONFIG,5);
//消息key的序列化器
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
//消息value的序列化器
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
//2.生产者对象
KafkaProducer<String,String> producer = new KafkaProducer<String, String>(properties);
//封装发送的消息,(topic,key,value)
ProducerRecord<String,String> record = new ProducerRecord<String, String>("itheima-topic","100001","hello kafka");
//3.发送消息
producer.send(record);
//4.关闭消息通道,必须关闭,否则消息发送不成功
producer.close();
}
}
4.3.3 消费者接收消息
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
/**
* 消费者
*/
public class ConsumerQuickStart {
public static void main(String[] args) {
//1.添加kafka的配置信息
Properties properties = new Properties();
//kafka的连接地址
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.200.130:9092");
//消费者组
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "group2");
//消息的反序列化器
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
//2.消费者对象
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
//3.订阅主题
consumer.subscribe(Collections.singletonList("itheima-topic"));
//当前线程一直处于监听状态
while (true) {
//4.获取消息
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord.key());
System.out.println(consumerRecord.value());
}
}
}
}
- 生产者发送消息,多个消费者订阅同一个主题,只能有一个消费者收到消息(一对一),将消费者设置成同一个组即可;
-
生产者发送消息,多个消费者订阅同一个主题,所有消费者都能收到消息(一对多),需要设置为不同的组。
4.4 Kafka生产者详解
4.4.1 发送类型
4.4.1.1 同步发送
使用send()方法发送,它会返回一个Future对象,调用get()方法进行等待,就可以知道消息是否发送成功
try {
RecordMetadata recordMetadata = producer.send(record).get();
System.out.println(recordMetadata.offset());//获取偏移
} catch (Exception e) {
e.printStackTrace();
}
4.4.1.2 异步发送
调用send()方法,并指定一个回调函数,服务器在返回响应时调用函数
try {
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e != null) {
e.printStackTrace();
}
System.out.println(recordMetadata.offset());
}
});
} catch (Exception e) {
e.printStackTrace();
}
4.4.2 参数详解
4.4.2.1 ack消息的确认
确认机制 | 说明 |
acks=0 | 生产者在成功写入消息之前不会等待任何来自服务器的响应,消息有丢失的风险,但是速度最快 |
acks=1(默认值) | 只要集群首领节点收到消息,生产者就会收到一个来自服务器的成功响应 |
acks=all | 只有当所有参与赋值的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应 |
4.4.2.2 retries重试机制
//设置重试次数
prop.put(ProducerConfig.RETRIES_CONFIG,10);
生产者从服务器收到的错误有可能是临时性错误,在这种情况下,retries参数的值决定了生产者可以重发消息的次数,如果达到这个次数,生产者会放弃重试返回错误,默认情况下,生产者会在每次重试之间等待100ms
4.4.2.3 消息压缩
默认情况下, 消息发送时不会被压缩。
prop.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"gzip");
压缩算法 | 说明 |
snappy | 占用较少的 CPU, 却能提供较好的性能和相当可观的压缩比, 如果看重性能和网络带宽,建议采用 |
lz4 | 占用较少的 CPU, 压缩和解压缩速度较快,压缩比也很客观 |
gzip | 占用较多的 CPU,但会提供更高的压缩比,网络带宽有限,可以使用这种算法 |
使用压缩可以降低网络传输开销和存储开销,而这往往是向 Kafka 发送消息的瓶颈所在。、
4.5 kafka消费者详解
4.5.1 消费者组
消费者组(Consumer Group) :指的就是由一个或多个消费者组成的群体一个发布在Topic上消息被分发给此消费者组中的一个消费者
- 所有的消费者都在一个组中,那么这就变成了queue模型
- 所有的消费者都在不同的组中,那么就完全变成了发布-订阅模型
4.5.2 消息有序性
topic分区中消息只能由消费者组中的唯一一个消费者处理,所以消息肯定是按照先后顺序进行处理的。但是它也仅仅是保证Topic的一个分区顺序处理,不能保证跨分区的消息先后处理顺序。 所以,如果你想要顺序的处理Topic的所有消息,那就只提供一个分区。
4.5.3 提交和偏移量
kafka不会像其他JMS队列那样需要得到消费者的确认,消费者可以使用kafka来追踪消息在分区的位置(偏移量)
消费者会往一个叫做_consumer_offset的特殊主题发送消息,消息里包含了每个分区的偏移量。如果消费者发生崩溃或有新的消费者加入群组,就会触发再均衡
Kafka提供了两种来管理和提交偏移量的方式:自动提交和手动提交。
自动提交:这是默认设置。自动提交通过在消费者配置文件中设置
enable.auto.commit
为true
启用。如果启用,可以进一步通过auto.commit.interval.ms
设置自动提交的频率。自动提交方式简单易用,但可能会引入一些问题。比如,在消费者进程死机前,尚未处理的消息可能已经被自动提交。这将导致这些消息丢失,因为Kafka会认为这些消息已经被成功处理。
手动提交:手动提交可以更精细地控制偏移的处理。你可以选择何时提交偏移,是在消息被接收后立即提交,还是在处理完消息后再进行提交。手动提交可以通过调用commitSync(同步)和commitAsync(异步)方法实现。commitSync在提交偏移后会等待服务器的响应,如果服务器没有响应,commitSync会再尝试一次。commitAsync则会在提交偏移后立即返回,并在提交完成时调用一个回调函数。
手动提交方式给你更多的控制权,但需要你更明确地处理各种可能出现的错误和异常情况。
4.5.3.1 同步提交
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.println(record.value());
System.out.println(record.key());
try {
consumer.commitSync();//同步提交当前最新的偏移量
} catch (CommitFailedException e) {
System.out.println("记录提交失败的异常:" + e);
}
}
}
4.5.3.2 异步提交
while (true){
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.println(record.value());
System.out.println(record.key());
}
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
if(e!=null){
System.out.println("记录错误的提交偏移量:"+ map+",异常信息"+e);
}
}
});}
4.5.3.3 同步和异步组合提交
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.println(record.value());
System.out.println(record.key());
}
consumer.commitAsync();
}
} catch (Exception e) {
+e.printStackTrace();
System.out.println("记录错误信息:" + e);
} finally {
try {
consumer.commitSync();
} finally {
consumer.close();
}
}
4.5.3.4 对比
同步提交(commitSync):
优点:可靠。它会在提交后等待服务器响应,如果提交失败,它会重新尝试,直到提交成功或达到重试次数。这样可以确保偏移的正确提交。
缺点:延时高。因为每次提交都要等待服务器响应,所以在高延时网络或高负载服务器的情况下,会导致消费者等待时间过长。
异步提交(commitAsync):
优点:延时低。不需要等待服务器响应,可以立即返回,提交偏移和消费消息可以并行进行,从而提高了效率。
缺点:不可靠。如果提交失败,不会重新尝试,可能会导致偏移提交失败,进而影响到消息的消费。
同步异步组合提交:一般情况下使用异步提交以保证高效率,但在消费者关闭或重平衡前使用同步提交以确保偏移的正确提交。
优点:兼具了同步提交和异步提交的优点,既有高效率,也有较高的可靠性。
缺点:实现比较复杂,需要同时处理两种提交方式的逻辑。
五、SpringBoot集成kafka
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
server:
port:
9991
spring:
application:
name:kafka-demo
kafka:
bootstrap-servers: 192.168.200.130:9092
producer:
retries: 10
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer:
group-id: test-hello-group
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@GetMapping("/hello")
public String hello () {
//第一个参数:topics
// 第二个参数:消息内容
kafkaTemplate.send("kafka-hello", "黑马程序员");
return "ok";
}
@Componentpublic
class HelloListener {
@KafkaListener(topics = {"kafka-hello"})
public void onMessage(String message) {
if (!StringUtils.isEmpty(message)) {
System.out.println(message);
}
}
}
传递消息为对象:可以把要传递的对象进行转json字符串,接收消息后再转为对象即可
User user = new User();
user.setUsername("zhangsan");
user.setAge(18);
kafkaTemplate.send("kafka-hello", JSON.toJSONString(user));
return "ok";