一:核心概念
kafka是消息中间件的一种,是一种分布式流平台,是用于构建实时数据管道和流应用程序。具有横向扩展,容错,wicked fast(变态快)等优点。
###kafka中涉及的名词:
- 消息记录(record): 由一个key,一个value和一个时间戳构成,消息最终存储在主题下的分区中, 记录在生产者中称为生产者记录(ProducerRecord), 在消费者中称为消费者记录(ConsumerRecord),Kafka集群保持所有的消息,直到它们过期, 无论消息是否被消费了,在一个可配置的时间段内,Kafka集群保留所有发布的消息,不管这些消息有没有被消费。比如,如果消息的保存策略被设置为2天,那么在一个消息被发布的两天时间内,它都是可以被消费的。之后它将被丢弃以释放空间。Kafka的性能是和数据量无关的常量级的,所以保留太多的数据并不是问题。
- 生产者(producer): 生产者用于发布(send)消息
- 消费者(consumer): 消费者用于订阅(subscribe)消息
- 消费者组(consumer group): 相同的group.id的消费者将视为同一个消费者组, 每个消费者都需要设置一个组id, 每条消息只能被 consumer group 中的一个 Consumer 消费,但可以被多个 consumer group 消费
- 主题(topic): 消息的一种逻辑分组,用于对消息分门别类,每一类消息称之为一个主题,相同主题的消息放在一个队列中
- 分区(partition): 消息的一种物理分组, 一个主题被拆成多个分区,每一个分区就是一个顺序的、不可变的消息队列,并且可以持续添加,分区中的每个消息都被分配了一个唯一的id,称之为偏移量(offset),在每个分区中偏移量都是唯一的。每个分区对应一个逻辑log,有多个segment组成。
- 偏移量(offset): 分区中的每个消息都一个一个唯一id,称之为偏移量,它代表已经消费的位置。可以自动或者手动提交偏移量(即自动或者手动控制一条消息是否已经被成功消费)
- 代理(broker): 一台kafka服务器称之为一个broker
- 副本(replica):副本只是一个分区(partition)的备份。 副本从不读取或写入数据。 它们用于防止数据丢失。
- 领导者(leader):Leader 是负责给定分区的所有读取和写入的节点。 每个分区都有一个服务器充当Leader, producer 和 consumer 只跟 leader 交互
- 追随者(follower):跟随领导者指令的节点被称为Follower。 如果领导失败,一个追随者将自动成为新的领导者。 跟随者作为正常消费者,拉取消息并更新其自己的数据存储。replica 中的一个角色,从 leader 中复制数据。
- zookeeper:Kafka代理是无状态的,所以他们使用ZooKeeper来维护它们的集群状态。ZooKeeper用于管理和协调Kafka代理
kafka功能
- 发布订阅:生产者(producer)生产消息(数据流), 将消息发送到到kafka指定的主题队列(topic)中,也可以发送到topic中的指定分区(partition)中,消费者(consumer)从kafka的指定队列中获取消息,然后来处理消息。
- 流处理(Stream Process): 将输入topic转换数据流到输出topic
- 连接器(Connector) : 将数据从应用程序(源系统)中导入到kafka,或者从kafka导出数据到应用程序(宿主系统sink system), 例如:将文件中的数据导入到kafka,从kafka中将数据导出到文件中
kafka中的消息模型
- 队列:同名的消费者组员瓜分消息
- 发布订阅:广播消息给多个消费者组(不同名)
生产者(producer)将消息记录(record)发送到kafka中的主题中(topic), 一个主题可以有多个分区(partition), 消息最终存储在分区中,消费者(consumer)最终从主题的分区中获取消息。
二:安装与启动
一: Mac版安装
brew install kafka
安装kafka是需要依赖于zookeeper的,所以安装kafka的时候也会包含zookeeper
kafka的安装目录:/usr/local/Cellar/kafka
kafka的配置文件目录:/usr/local/etc/kafka
kafka服务的配置文件:/usr/local/etc/kafka/server.properties
zookeeper配置文件: /usr/local/etc/kafka/zookeeper.properties
# server.properties中的重要配置
broker.id=0
listeners=PLAINTEXT://:9092
advertised.listeners=PLAINTEXT://127.0.0.1:9092
log.dirs=/usr/local/var/lib/kafka-logs
# zookeeper.properties
dataDir=/usr/local/var/lib/zookeeper
clientPort=2181
maxClientCnxns=0
二: 启动zookeeper
# 新起一个终端启动zookeeper
cd /usr/local/Cellar/kafka/1.0.0
./bin/zookeeper-server-start /usr/local/etc/kafka/zookeeper.properties
三: 启动kafka
# 新起一个终端启动zookeeper,注意启动kafka之前先启动zookeeper
cd /usr/local/Cellar/kafka/1.0.0
./bin/kafka-server-start /usr/local/etc/kafka/server.properties
四:创建topic
# 新起一个终端来创建主题
cd /usr/local/Cellar/kafka/1.0.0
## 创建一个名为“test”的主题,该主题有1个分区
./bin/kafka-topics --create
--zookeeper localhost:2181
--partitions 1
--replication-factor 1
--topic test
五:查看topic
// 创建成功可以通过 list 列举所有的主题
./bin/kafka-topics --list --zookeeper localhost:2181
// 查看某个主题的信息
./bin/kafka-topics --describe --zookeeper localhost:2181 --topic <name>
六:发送消息
# 新起一个终端,作为生产者,用于发送消息,每一行算一条消息,将消息发送到kafka服务器
> ./bin/kafka-console-producer --broker-list localhost:9092 --topic test
This is a message
This is another message
七:消费消息(接收消息)
# 新起一个终端作为消费者,接收消息
cd /usr/local/Cellar/kafka/1.0.0
> ./bin/kafka-console-consumer --bootstrap-server localhost:9092 --topic test --from-beginning
This is a message
This is another message
八:在生产者发送消息
在步骤六中新起的终端属于一条消息(任意字符),输入完回车就算一条消息,可以看到在步骤7中的消费者端就会显示刚才输入的消息
kafka集群:在一台机器上配置kafka集群
1、 首先在kafka配置文件的目录下复制server.properties,复制两份,分别命名为server-1.properties和server-2.properties
cp /usr/local/etc/kafka/server.properties /usr/local/etc/kafka/server-1.properties
cp /usr/local/etc/kafka/server.properties /usr/local/etc/kafka/server-2.properties
2、分别修改server-1.properties和server-2.properties的配置参数
# server-1.properties
broker.id=1
listeners=PLAINTEXT://:9093
log.dirs=/usr/local/var/lib/kafka-logs-1
# server-2.properties
broker.id=2
listeners=PLAINTEXT://:9094
log.dirs=/usr/local/var/lib/kafka-logs-2
3、开启两个终端分别启动kafka并使用自己的配置文件
./bin/kafka-server-start /usr/local/etc/kafka/server-1.properties
./bin/kafka-server-start /usr/local/etc/kafka/server-2.properties
4、创建主题
./bin/kafka-topics.sh --create
--zookeeper localhost:2181
--replication-factor 3
--partitions 1
--topic my-replicated-topic
5、查看主题
./bin/kafka-topics --describe --zookeeper localhost:2181 --topic my-replicated-topic
导入导出
1、 在/usr/local/Cellar/kafka/1.0.0 下创建test.txt 文件,并写入两条内容,每一行属于一条
2、运行connect-standalone命令后可以看到在/usr/local/Cellar/kafka/1.0.0目录下有个文件:test.sink.txt
/usr/local/etc/kafka/connect-standalone.properties
/usr/local/etc/kafka/connect-file-source.properties
/usr/local/etc/kafka/connect-file-sink.properties
3、运行connect-standalone命令可以看到test.txt文件的内容已经导入到 “connect-test”主题中了, 并且每当往test.txt中追加内容时就会实时的导入到“connect-test ” 主题中
./bin/kafka-console-consumer
--zookeeper localhost:2181
--topic connect-test
--from-beginning
注意:关于文件名"test.txt"、“test.sink.txt”、主题名"connect-test"都是在connect-file-source.properties和connect-file-sink.properties配置文件中配置的
kafka其他命令:
修改分区
./bin/kafka-topics --alter
--zookeeper localhost:2181
--topic <topic-name>
--partitions <分区数量>
删除主题:
# 默认是不允许删除主题的,需要在server.properties中配置delete.topic.enable=true, 然后重启kafka
./bin/kafka-topics --delete --zookeeper localhost:2181 --topic <topic_name>
三:kafka-clients
1、引入依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>1.0.0</version>
</dependency>
2、KafkaTest
public class KafkaTest {
@Test
public void testProducer(){
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
// acks: 应答模式,发送确认,请求是否成功发送到kafka,“all”将会阻塞消息,这种设置性能最低,但是是最可靠的。
props.put("acks", "all");
// 如果请求失败,生产者会自动重试,我们指定是0次,如果启用重试,则会有重复消息的可能性。
props.put("retries", 0);
// 缓存的大小
props.put("batch.size", 16384);
// 逗留毫秒数
props.put("linger.ms", 1);
// 控制生产者可用的缓存总量,如果消息发送速度比其传输到服务器的快,将会耗尽这个缓存空间。当缓存空间耗尽,其他发送调用将被阻塞,阻塞时间的阈值通过max.block.ms设定,之后它将抛出一个TimeoutException。
props.put("buffer.memory", 33554432);
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<String, String>(props);
for (int i = 0; i < 100; i++){
String key = i + "";
String value = i + "";
ProducerRecord<String, String> record = new ProducerRecord<String, String>("my-topic", key, value);
// send方法异步的,添加消息到缓冲区等待发送,并立即返回。生产者将单个的消息批量在一起发送来提高效率。
producer.send(record, new Callback() {
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e != null) e.printStackTrace();
// 发送的结果是一个RecordMetadata,它指定了消息发送的分区,分配的offset和消息的时间戳
System.out.println(recordMetadata);
}
});
}
producer.close();
}
/**
* 自动提交偏移量
*/
@Test
public void testConsumer4AutoCommit(){
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
// 订阅,subscribe动态分配分区
consumer.subscribe(Arrays.asList("my-topic"));
while (true) {
// poll : 将消费者加入到消费者组中,
// 消费者需要向服务器定时发送心跳, 如果消费者崩溃或者无法在session.timeout.ms配置的时间内发送心跳,则消费者被视为死亡,并且将其分区被重新分配
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record: records) {
System.out.printf("offset = %d, key = %s, value= %s%n", record.offset(), record.key(), record.value());
}
}
}
}
@Test
public void testConsumerByPartition() throws InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "testO");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("session.timeout.ms", "30000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
consumer.subscribe(Arrays.asList("my-topic"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
// 按分区读取数据
for (TopicPartition partition: records.partitions()) {
List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
for (ConsumerRecord<String, String> record: partitionRecords) {
System.out.printf("offset = %d, key = %s, value= %s%n", record.offset(), record.key(), record.value());
}
}
}
}
先运行消费者testConsumer4AutoCommit,再运行生产者testProducer, 当生产者发送消息的时候,消费者就能收到消息。
/**
* 手动提交偏移量commitSync(同步提交)
*/
@Test
public void testConsumer4CommitSync(){
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "false");
props.put("auto.commit.interval.ms", "1000");
props.put("session.timeout.ms", "30000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
consumer.subscribe(Arrays.asList("foo", "bar"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record: records) {
System.out.printf("offset = %d, key = %s, value= %s%n", record.offset(), record.key(), record.value());
consumer.commitSync();
}
}
}
kafka队列模型
队列模型:多个消费者(所有消费者拥有相同的group.id, 即所有消费者都属于同一个消费者组)订阅同一个主题, 这个主题下的所有消息被消费者组中消费者瓜分。
主题的创建:当生产者往主题中发送消息或者消费者订阅主题消息的时候,如果发现主题不存在,会帮你创建相应的主题并设置主题的分区数量PartitionCount为1,要想实现队列模型必须保证分区数量大于1,一般等于消费者数量,所以不能使用默认的创建方式,需要手动先创建多个分区。如果已经创建了可以修改分区的数量
./bin/kafka-topics --create --zookeeper localhost:2181 --partitions 2 --topic topic-test
./bin/kafka-topics --alter --zookeeper localhost:2181 --partitions 2 --topic topic-test
Kafka中的一个分区只允许一个消费者来消费,所以如果主题只有一个分区,要想实现瓜分消息至少要2个消费者,那这样只有一个消费者能获取消息,其他消费者都不会获取消息,因为一个分区被分给一个消费者了就不允许分配给其它消费者了。
生产者在发送消息的时候通过轮播的方式将消息发送给主题下的所有分区,这样每个消费者都分配了一个主题分区,每个消费者消费自己的分区中的消息,从而达到消息被相同组的消费者瓜分的情况.
// 示例:生产者发送 0带9 10个消息,消费者1消费偶数消息(0、2、4、8),消费者2消费奇数消息(1、3、5、7、9)
public class KafkaQueueTest {
private static final String topic = "topic-test";
@Test
public void testProducer() throws InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
// acks: 应答模式,发送确认,请求是否成功发送到kafka,“all”将会阻塞消息,这种设置性能最低,但是是最可靠的。
props.put("acks", "0"); // all
// 如果请求失败,生产者会自动重试,我们指定是0次,如果启用重试,则会有重复消息的可能性。
props.put("retries", 0);
// 缓存的大小
props.put("batch.size", 16384);
// 逗留毫秒数
props.put("linger.ms", 1);
// 控制生产者可用的缓存总量,如果消息发送速度比其传输到服务器的快,将会耗尽这个缓存空间。当缓存空间耗尽,其他发送调用将被阻塞,阻塞时间的阈值通过max.block.ms设定,之后它将抛出一个TimeoutException。
props.put("buffer.memory", 33554432);
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<String, String>(props);
for (int i = 0; i < 10; i++){
String key = i + "";
String value = i + "";
int partition = i % 2 == 0 ? 0 : 1;
// 发送消息时指定分区
ProducerRecord<String, String> record = new ProducerRecord<String, String>(topic, partition, key, value);
// send方法异步的,添加消息到缓冲区等待发送,并立即返回。生产者将单个的消息批量在一起发送来提高效率。
producer.send(record, new Callback() {
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e != null) e.printStackTrace();
// 发送的结果是一个RecordMetadata,它指定了消息发送的分区,分配的offset和消息的时间戳
System.out.println(recordMetadata);
}
});
Thread.sleep(1000);
}
producer.close();
}
@Test
public void testConsumer1() throws InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "GroupA");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("session.timeout.ms", "30000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
// 订阅,subscribe动态分配分区
consumer.subscribe(Arrays.asList(topic));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record: records) {
System.out.printf("offset = %d, key = %s, value= %s%n", record.offset(), record.key(), record.value());
Thread.sleep(1000);
}
}
}
@Test
public void testConsumer2() throws InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "GroupA");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("session.timeout.ms", "30000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
consumer.subscribe(Arrays.asList(topic));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record: records) {
System.out.printf("offset = %d, key = %s, value= %s%n", record.offset(), record.key(), record.value());
Thread.sleep(1000);
}
}
}
}
先运行两个消费者,然后再运行生产者。
kafka发布订阅模型
发布订阅:消费者在不同的消费者组中订阅同一个主题的消息,那么所有消费者都会收到相同的消息,消息将会广播给所有订阅者
发布订阅只需要保证消费者不在同一个组中,即每个消费者都在自己的组中
public class KafkaPubSubTest {
private static final String topic = "topic-pub-sub";
@Test
public void testProducer() throws InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
// acks: 应答模式,发送确认,请求是否成功发送到kafka,“all”将会阻塞消息,这种设置性能最低,但是是最可靠的。
props.put("acks", "0"); // all
// 如果请求失败,生产者会自动重试,我们指定是0次,如果启用重试,则会有重复消息的可能性。
props.put("retries", 0);
// 缓存的大小
props.put("batch.size", 16384);
// 逗留毫秒数
props.put("linger.ms", 1);
// 控制生产者可用的缓存总量,如果消息发送速度比其传输到服务器的快,将会耗尽这个缓存空间。当缓存空间耗尽,其他发送调用将被阻塞,阻塞时间的阈值通过max.block.ms设定,之后它将抛出一个TimeoutException。
props.put("buffer.memory", 33554432);
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<String, String>(props);
for (int i = 0; i < 10; i++){
String key = i + "";
String value = i + "";
// 发送消息时指定分区
ProducerRecord<String, String> record = new ProducerRecord<String, String>(topic, key, value);
// send方法异步的,添加消息到缓冲区等待发送,并立即返回。生产者将单个的消息批量在一起发送来提高效率。
producer.send(record, new Callback() {
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e != null) e.printStackTrace();
// 发送的结果是一个RecordMetadata,它指定了消息发送的分区,分配的offset和消息的时间戳
System.out.println(recordMetadata);
}
});
}
producer.close();
}
@Test
public void testConsumer1() throws InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "GroupA");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("session.timeout.ms", "30000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
// 订阅,subscribe动态分配分区
consumer.subscribe(Arrays.asList(topic));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record: records) {
System.out.printf("[consumer1] offset = %d, key = %s, value= %s%n", record.offset(), record.key(), record.value());
Thread.sleep(1000);
}
}
}
@Test
public void testConsumer2() throws InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "GroupB");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("session.timeout.ms", "30000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
consumer.subscribe(Arrays.asList(topic));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record: records) {
System.out.printf("[consumer2] offset = %d, key = %s, value= %s%n", record.offset(), record.key(), record.value());
Thread.sleep(1000);
}
}
}
}
kafka发送对象消息
kafka 消息的key和value支持以下类型,但是不支持对象,为了让kafka发送的消息为对象类型,需要自定义对象的序列化和对象的反序列化。
- org.apache.kafka.common.serialization.ByteArraySerializer
- org.apache.kafka.common.serialization.ByteBufferSerializer
- org.apache.kafka.common.serialization.BytesSerializer
- org.apache.kafka.common.serialization.DoubleSerializer
- org.apache.kafka.common.serialization.IntegerSerializer
- org.apache.kafka.common.serialization.LongSerializer
- org.apache.kafka.common.serialization.StringSerializer
ObjectSerializer
/**
* 对象序列化
*/
public class ObjectSerializer implements Serializer<Object>{
public void configure(Map<String, ?> map, boolean b) {
}
public byte[] serialize(String topic, Object object) {
byte[] bytes = null;
ByteArrayOutputStream byteArrayOutputStream = null;
ObjectOutputStream objectOutputStream = null;
try {
byteArrayOutputStream = new ByteArrayOutputStream();
objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
bytes = byteArrayOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(byteArrayOutputStream!=null){
byteArrayOutputStream.close();
}
if(objectOutputStream!=null){
objectOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return bytes;
}
public void close() {
}
}
ObjectDeserializer
/**
* 对象反序列化
*/
public class ObjectDeserializer implements Deserializer<Object>{
public void configure(Map<String, ?> map, boolean b) {
}
public Object deserialize(String topic, byte[] bytes) {
Object object = null;
ByteArrayInputStream byteArrayInputStream = null;
ObjectInputStream objectInputStream = null;
try {
byteArrayInputStream =new ByteArrayInputStream(bytes);
objectInputStream =new ObjectInputStream(byteArrayInputStream);
object = objectInputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
if(byteArrayInputStream!=null){
byteArrayInputStream.close();
}
if(objectInputStream!=null){
objectInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return object;
}
public void close() {
}
}
User
// 实现序列化接口 Serializable
public class User implements Serializable {
private Long id;
private String username;
private Integer age;
public User(Long id, String username, Integer age) {
this.id = id;
this.username = username;
this.age = age;
}
// Getter & Setter
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", age=" + age +
'}';
}
}
@Test
public void testProducer() throws InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
// acks: 应答模式,发送确认,请求是否成功发送到kafka,“all”将会阻塞消息,这种设置性能最低,但是是最可靠的。
props.put("acks", "0"); // all
// 如果请求失败,生产者会自动重试,我们指定是0次,如果启用重试,则会有重复消息的可能性。
props.put("retries", 0);
// 缓存的大小
props.put("batch.size", 16384);
// 逗留毫秒数
props.put("linger.ms", 1);
// 控制生产者可用的缓存总量,如果消息发送速度比其传输到服务器的快,将会耗尽这个缓存空间。当缓存空间耗尽,其他发送调用将被阻塞,阻塞时间的阈值通过max.block.ms设定,之后它将抛出一个TimeoutException。
props.put("buffer.memory", 33554432);
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 配置ObjectSerializer
props.put("value.serializer", "com.mengday.kafka.ObjectSerializer");
Producer<String, Object> producer = new KafkaProducer<String, Object>(props);
User user = new User(1L, "mengday", 28);
ProducerRecord<String, Object> record = new ProducerRecord<String, Object>("test-topic", "key", user);
producer.send(record);
producer.close();
}
@Test
public void testConsumer() throws InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "GroupA");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("session.timeout.ms", "30000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 配置ObjectSerializer
props.put("value.deserializer", "com.mengday.kafka.ObjectDeserializer");
KafkaConsumer<String, Object> consumer = new KafkaConsumer<String, Object>(props);
consumer.subscribe(Arrays.asList("test-topic"));
while (true) {
ConsumerRecords<String, Object> records = consumer.poll(100);
for (ConsumerRecord<String, Object> record: records) {
System.out.printf("offset = %d, key = %s, value= %s%n", record.offset(), record.key(), record.value());
}
}
}
##API
KafkaProducer
// 发送消息
send(ProducerRecord<K, V> record, Callback callback);
// 关闭连接
void close();
ProducerRecord
public ProducerRecord(String topic, V value);
public ProducerRecord(String topic, K key, V value);
public ProducerRecord(String topic, Integer partition, K key, V value);
public String topic();
public K key();
public V value();
public Long timestamp();
public Integer partition();
KafkaConsumer
// 订阅主题
subscribe(Collection<String> topics);
// 获取消息
public ConsumerRecords<K, V> poll(long timeout);
// 订阅主题的指定分区
public void assign(Collection<TopicPartition> partitions);
// 消费者手动控制分区的偏移量的位置
public void seek(TopicPartition partition, long offset);
public void seekToBeginning(Collection<TopicPartition> partitions);
// 暂停消费指定分配的分区
public void pause(Collection<TopicPartition> partitions);
// 重新开始指定暂停的分区
public void resume(Collection<TopicPartition> partitions);
// 线程安全
public void wakeup();
TopicPartition
// 构造函数
public TopicPartition(String topic, int partition);
四:spring-kafka
1、引入依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.22</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.22</version>
</dependency>
2、 配置application.properties
bootstrap.servers=localhost:9092
group.id=GroupA
retries=0
batch.size=16384
linger.ms=1
buffer.memory=33554432
acks=all
enable.auto.commit=true
session.timeout.ms=30000
# kafka主题
kafka.topic.test=test
kafka.topic.test2=test2
3、 spring-kafka.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath*:conf/settings/application.properties"/>
<!-- 1. 定义producer的参数 -->
<bean id="producerProperties" class="java.util.HashMap">
<constructor-arg>
<map>
<entry key="bootstrap.servers" value="${bootstrap.servers}" />
<entry key="group.id" value="${group.id}" />
<entry key="retries" value="${retries}" />
<entry key="batch.size" value="${batch.size}" />
<entry key="linger.ms" value="${linger.ms}" />
<entry key="buffer.memory" value="${buffer.memory}" />
<entry key="acks" value="${acks}" />
<entry key="key.serializer" value="org.apache.kafka.common.serialization.StringSerializer" />
<entry key="value.serializer" value="org.apache.kafka.common.serialization.StringSerializer" />
</map>
</constructor-arg>
</bean>
<!-- 2. 生产者工厂 -->
<bean id="producerFactory"
class="org.springframework.kafka.core.DefaultKafkaProducerFactory">
<constructor-arg>
<ref bean="producerProperties" />
</constructor-arg>
</bean>
<!-- 3. kafkaTemplate -->
<bean id="kafkaTemplate" class="org.springframework.kafka.core.KafkaTemplate">
<constructor-arg ref="producerFactory" />
<constructor-arg name="autoFlush" value="true" />
<property name="defaultTopic" value="default" />
</bean>
<!-- 1. 定义consumer的参数 -->
<bean id="consumerProperties" class="java.util.HashMap">
<constructor-arg>
<map>
<entry key="bootstrap.servers" value="${bootstrap.servers}" />
<entry key="group.id" value="${group.id}" />
<entry key="enable.auto.commit" value="${enable.auto.commit}" />
<entry key="session.timeout.ms" value="${session.timeout.ms}" />
<entry key="key.deserializer" value="org.apache.kafka.common.serialization.StringDeserializer" />
<entry key="value.deserializer" value="org.apache.kafka.common.serialization.StringDeserializer" />
</map>
</constructor-arg>
</bean>
<!-- 2. 消费者工厂 -->
<bean id="consumerFactory" class="org.springframework.kafka.core.DefaultKafkaConsumerFactory" >
<constructor-arg>
<ref bean="consumerProperties" />
</constructor-arg>
</bean>
<!-- 3.定义消费实现类 -->
<bean id="kafkaConsumerService" class="com.mengdee.manager.service.impl.KafkaConsumerSerivceImpl" />
<!-- 4.消费者容器配置信息 -->
<bean id="containerProperties" class="org.springframework.kafka.listener.config.ContainerProperties">
<!-- topic -->
<constructor-arg name="topics">
<list>
<value>${kafka.topic.test}</value>
<value>${kafka.topic.test2}</value>
</list>
</constructor-arg>
<property name="messageListener" ref="kafkaConsumerService" />
</bean>
<!-- 5.消费者并发消息监听容器,执行doStart()方法 -->
<bean id="messageListenerContainer" class="org.springframework.kafka.listener.ConcurrentMessageListenerContainer" init-method="doStart" >
<constructor-arg ref="consumerFactory" />
<constructor-arg ref="containerProperties" />
<property name="concurrency" value="${concurrency}" />
</bean>
</beans>
4、KafkaConsumerSerivceImpl
public class KafkaConsumerSerivceImpl implements MessageListener<String, String>{
@Override
public void onMessage(ConsumerRecord<String, String> consumerRecord) {
String topic = consumerRecord.topic();
if ("test".equals(topic)) {
System.out.println(consumerRecord);
} else if ("test2".equals(topic)) {
System.out.println(consumerRecord);
} else {
System.out.println(consumerRecord);
}
}
}
5、 SpringKafkaTest
@ContextConfiguration(locations = {"classpath:conf/spring/spring-*.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class SpringTestCase extends AbstractJUnit4SpringContextTests{
}
public class SpringKafkaTest extends SpringTestCase {
@Autowired
private KafkaTemplate kafkaTemplate;
@Test
public void testProducer(){
ListenableFuture<SendResult<String, String>> listenableFuture = kafkaTemplate.send("test", 0,"key","data");
//发送成功回调
SuccessCallback<SendResult<String, String>> successCallback = new SuccessCallback<SendResult<String, String>>() {
@Override
public void onSuccess(SendResult<String, String> result) {
//成功业务逻辑
System.out.println("发送成功");
}
};
//发送失败回调
FailureCallback failureCallback = new FailureCallback() {
@Override
public void onFailure(Throwable ex) {
//失败业务逻辑
System.out.println("发送失败");
}
};
listenableFuture.addCallback(successCallback, failureCallback);
}
}
实际遇到的坑
在开发中遇到这样一个业务:
在service层创建订单后,发一条kafka消息,消费者会处理该消息,拿到订单号去查询订单去做其它业务逻辑处理。注意:很可能service层在操作数据库时insert订单时,还没有插入完成,这时kafka的消息已经发出去了,消费者去查询数据库,会查不到订单。
解决方案:
- 等数据库插入执行完成后(如在controller层)发kafka消息
- 消费者可以先睡眠几秒,等待数据库插入完成再去执行消费者中的逻辑,在消费者的第一行就去睡眠两三秒