学习消息中间件Kafka从配置到基本应用
- 一、服务器安装配置Kafka
- 1、配置介绍与修改
- 2、启动
- 3、配置开机自启
- 4、如果不使用自带的zookeeper
- 二、Kafka的使用场景
- 1、异步处理
- 2、应用解耦
- 3、流量削锋
- 4、日志处理
- 5、消息通讯
- 三、点对点消息传递模式
- 1、介绍
- 四、发布-订阅消息传递模式
- 1、介绍
- 2、依赖
- 3、生产者
- 4、消费者
- 5、测试
- 6、消费者的 auto-offset-reset 含义详解
- 五、保证消息幂等性的办法
- 1、生成者不重复发送消息到MQ
- 2、 消费者不重复消费
- 3、代码举例
- 六、KafkaTemplate同异步发送
- 1、KafkaTemplate默认是异步发送
- 2、同步发送
- 3、异步发送消息回调
一、服务器安装配置Kafka
因为我的springboot是2.3.3所以我用的是2.6版本的kafka
#解压
tar -zvxf kafka_2.12-2.6.0.tgz
#修改名称
mv kafka_2.12-2.6.0 kafka
#修改配置
vim /home/installed/kafka/config/server.properties
1、配置介绍与修改
broker.id=0 #当前机器在集群中的唯一标识
advertised.listeners=PLAINTEXT://ip:9092 #修改,注:ip指的是本机ip地址
log.dirs=/tmp/kafka-logs #log存放目录
环境变量
#配置ZOOKEEPER环境变量
export ZOOKEEPER_HOME=/home/installed/kafka
export PATH=$PATH:$ZOOKEEPER_HOME/bin
#配置KAFKA环境变量
export KAFKA_HOME=/home/installed/kafka
export PATH=$PATH:$KAFKA_HOME/bin
# 使配置生效source /etc/profile
2、启动
#到kafka目录下
cd /home/installed/kafka
#使用安装包中的脚本启动单节点Zookeeper
bin/zookeeper-server-start.sh -daemon config/zookeeper.properties &
#使用kafka-server-start.sh启动kafka服务
bin/kafka-server-start.sh config/server.properties &
# & 为后台启动
3、配置开机自启
cd /lib/systemd/system/
vim zookeeper.service
vim kafka.service
#zookeeper、kafka服务加入开机自启
systemctl enable zookeeper
systemctl enable kafka
#使用systemctl启动/关闭/重启 zookeeper、kafka服务systemctl start/stop/restart zookeeper/kafka
systemctl start zookeeper
systemctl start kafka
#查看状态
systemctl status zookeeper
systemctl status kafka
zookeeper.service 添加内容:
[Unit]
Description=Zookeeper service
After=network.target
[Service]
Type=simple
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/java/jdk1.8.0_301/bin"
User=root
Group=root
ExecStart=/home/installed/kafka/bin/zookeeper-server-start.sh /home/installed/kafka/config/zookeeper.properties
ExecStop=/home/installed/kafka/bin/zookeeper-server-stop.sh
Restart=on-failure
[Install]
WantedBy=multi-user.target
kafka.service 添加内容:
[Unit]
Description=Apache Kafka server (broker)
After=network.target zookeeper.service
[Service]
Type=simple
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/java/jdk1.8.0_301/bin"
User=root
Group=root
ExecStart=/home/installed/kafka/bin/kafka-server-start.sh /home/installed/kafka/config/server.properties
ExecStop=/home/installed/kafka/bin/kafka-server-stop.sh
Restart=on-failure
[Install]
WantedBy=multi-user.target
4、如果不使用自带的zookeeper
zookeeper安装链接地址
二、Kafka的使用场景
1、异步处理
传统串行:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端。
传统并行:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间
引入消息队列,将不是必须的业务逻辑,异步处理。改造后的架构如下:
2、应用解耦
订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功
库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作
假如:在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦
3、流量削锋
流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛。
应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。
例如:
可以控制活动的人数
可以缓解短时间内高流量压垮应用
用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面。
秒杀业务根据消息队列中的请求信息,再做后续处理
4、日志处理
日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。架构简化如下
5、消息通讯
消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等
点对点通讯:
客户端A和客户端B使用同一队列,进行消息通讯。聊天室通讯:
客户端A,客户端B,客户端N订阅同一主题,进行消息发布和接收。实现类似聊天室效果。
以上实际是消息队列的两种消息模式,点对点或发布订阅模式。模型为示意图,供参考。
三、点对点消息传递模式
1、介绍
在点对点消息系统中,消息持久化到一个队列中。此时,将有一个或多个消费者消费队列中的数据。但是一条消息只能被消费一次。当一个消费者消费了队列中的某条数据之后,该条数据则从消息队列中删除。该模式即使有多个消费者同时消费数据,也能保证数据处理的顺序。
四、发布-订阅消息传递模式
1、介绍
在发布-订阅消息系统中,消息被持久化到一个topic中。与点对点消息系统不同的是,消费者可以订阅一个或多个topic,消费者可以消费该topic中所有的数据,同一条数据可以被多个消费者消费,数据被消费后不会立马删除。在发布-订阅消息系统中,消息的生产者称为发布者,消费者称为订阅者。
2、依赖
<!--kafka依赖-->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.7.6</version>
</dependency>
3、生产者
application.yml
server:
port: 8000
spring:
kafka:
bootstrap-servers: 122.111.111.11:9092
#=============== provider =======================
producer:
retries: 0
batch-size: 16384
buffer-memory: 33554432
# 指定消息key和消息体的编解码方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
消息发送类
/**
* 消息发送类
*
* @author lichangyuan
* @create 2021-09-04 16:40
*/
@RestController
public class KafkaSender {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
//发送消息方法
@RequestMapping("send_message")
public void send(@RequestParam String message) {
kafkaTemplate.send("topic_1", message);
}
}
4、消费者
application.yml
server:
port: 8001
spring:
kafka:
bootstrap-servers: 122.111.111.11:9092
#=============== provider =======================
consumer:
# 指定默认消费者group id
group-id: test-consumer-group1
#earliest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时(从头开始消费)
#latest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时(消费新产生的该分区下的数据)
#none:topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常(消费未消费的信息)
auto-offset-reset: latest
#自动提交,消费完不会再消费
enable-auto-commit: true
auto-commit-interval: 100
# 指定消息key和消息体的编解码方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
消息接收类
/**
* 消息接收类
*
* @author lichangyuan
* @create 2021-09-04 16:45
*/
@Component
public class KafkaReceiver {
//里面我订阅了topic_1和topic_2,如果只订阅一个@KafkaListener(topics = {"topic_1"})
@KafkaListener(topics = {"topic_1","topic_2"})
public void listen(ConsumerRecord<?, ?> record) {
Optional<?> kafkaMessage = Optional.ofNullable(record.value());
if (kafkaMessage.isPresent()) {
Object message = kafkaMessage.get();
System.out.println("----------------- record =" + record);
System.out.println("------------------ message =" + message);
}
}
}
5、测试
生产者发送
消费者1
消费者2
目录结构
6、消费者的 auto-offset-reset 含义详解
如果存在已经提交的offest时,不管设置为earliest 或者latest 都会从已经提交的offest处开始消费 如果不存在已经提交的offest时,earliest 表示从头开始消费,latest 表示从最新的数据消费,也就是新产生的数据. none topic各分区都存在已提交的offset时,从提交的offest处开始消费;只要有一个分区不存在已提交的offset,则抛出异常
五、保证消息幂等性的办法
1、生成者不重复发送消息到MQ
mq内部可以为每条消息生成一个全局唯一、与业务无关的消息id,当mq接收到消息时,会先根据该id判断消息是否重复发送,mq再决定是否接收该消息。
2、 消费者不重复消费
消费者怎么保证不重复消费的关键在于消费者端做控制,因为MQ不能保证不重复发送消息,所以应该在消费者端控制:即使MQ重复发送了消息,消费者拿到了消息之后,要判断是否已经消费过,如果已经消费,直接丢弃。所以根据实际业务情况,有下面几种方式:
1、如果从MQ拿到数据是要存到数据库,那么可以根据数据创建唯一约束,这样的话,同样的数据从MQ发送过来之后,当插入数据库的时候,会报违反唯一约束,不会插入成功的。(或者可以先查一次,是否在数据库中已经保存了,如果能查到,那就直接丢弃就好了)。
2、让生产者发送消息时,每条消息加一个全局的唯一id,然后消费时,将该id保存到redis里面。消费时先去redis里面查一下有么有,没有再消费。(其实原理跟第一点差不多)。
3、如果拿到的数据是直接放到redis的set中的话,那就不用考虑了,因为set集合就是自动有去重的。
3、代码举例
/**
* 强校验幂等伪代码演示这都是简单的伪代码真实情况复杂一点但是大的逻辑是这样
* @param orderId
*/
public void process(String orderId){
try {
//查询这个订单是否存在这个活动加GYV的流水
Object gmvFlow = getFlowBy0rderId( "addGmv" +orderId);
if (Objects.isNull( gmvFlow)){
//不存在流水去加GMV和加流水注意这两个在一个事务的回滚就一起回滚了
addGmvAndFlow(orderId);
}else {
//存在流水证明加过了返回就好了return;
}
}catch (Exception e){
//发送异常触发消息队列框架重试机制
}
}
六、KafkaTemplate同异步发送
1、KafkaTemplate默认是异步发送
kafkaTemplate.send(topic,message);
2、同步发送
kafkaTemplate.send(topic,message).get();
原理:
同步发送消息时,需要在每次send()方法调用get()方法,因为每次send()方法会返回一个Future类型的值,Future的get()方法会一直阻塞,知道该线程的任务获取到返回值,即当消息发送成功。
3、异步发送消息回调
//发送消息方法(异步发送消息回调)
@RequestMapping("send_message2")
public void send2(@RequestParam String message) {
ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send("topic_1", message);
future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
//发送消息成功回调
@Override
public void onSuccess(SendResult<String, String> result) {
System.out.println(result.getProducerRecord());
//getRecordMetadata里面存在发送的topic和partition等信息
System.out.println(result.getRecordMetadata());
}
//发送消息失败回调
@Override
public void onFailure(Throwable ex) {
ex.printStackTrace();
}
});
}