原生方式
无论是生产者还是消费者,引入的依赖都是kafka-clients,maven坐标如下:
org.apache.kafka
kafka-clients
1.1.0
生产者
kafka生产者对象就是KafkaProducer,构造方式如下:
Properties props = new Properties();
// kafka集群地址
props.put("bootstrap.servers", "10.0.55.229:9092");
// kafka消息key的序列化方式
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// kafka消息value的序列化方式
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer kafkaProducer = new KafkaProducer<>(props);
消息
-创建生产者
KafkaProducer构造好后,需要构造待发送的消息。kafka消息对象是ProducerRecord,根据源码可知,构造方式有多种:
public class ProducerRecord {
/**
* 所有构造方法最后都是调用这个构造方法, 所以弄明白这个构造方法所有参数含义就可以了
* Creates a record with a specified timestamp to be sent to a specified topic and partition
* @param topic - The topic the record will be appended to, topic名称
* @param partition - The partition to which the record should be sent, 消息发送的目标分区名称, 如果不指定, kafka会根据Partitioner计算目标分区
* @param timestamp - The timestamp of the record, in milliseconds since epoch. If null, the producer will assign
* the timestamp using System.currentTimeMillis(). 消息发送的指定时间戳, 默认为当前时间
* @param key - The key that will be included in the record, 消息的key, kafka根据这个key计算分区
* @param value - The record contents 消息的内容
* @param headers - the headers that will be included in the record
*/
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable headers) {
// topic是构造ProducerRecord的必传参数
if (topic == null)
throw new IllegalArgumentException("Topic cannot be null.");
// 发送的时间戳不能为负数
if (timestamp != null && timestamp < 0)
throw new IllegalArgumentException(
String.format("Invalid timestamp: %d. Timestamp should always be non-negative or null.", timestamp));
// 分区值不能为负数
if (partition != null && partition < 0)
throw new IllegalArgumentException(
String.format("Invalid partition: %d. Partition number should always be non-negative or null.", partition));
this.topic = topic;
this.partition = partition;
this.key = key;
this.value = value;
this.timestamp = timestamp;
this.headers = new RecordHeaders(headers);
}
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value) {
this(topic, partition, timestamp, key, value, null);
}
public ProducerRecord(String topic, Integer partition, K key, V value, Iterable headers) {
this(topic, partition, null, key, value, headers);
}
public ProducerRecord(String topic, Integer partition, K key, V value) {
this(topic, partition, null, key, value, null);
}
public ProducerRecord(String topic, K key, V value) {
this(topic, null, null, key, value, null);
}
public ProducerRecord(String topic, V value) {
this(topic, null, null, null, value, null);
}
... ...
}
- 创建消息
下面构造一个最常用的ProducerRecord,只指定topic和value,由kafka去决定分区:
// ProducerRecord就是发送的信息对象, 包括: topic名称, key(可选), value(发送的内容)
// key的用途主要是:消息的附加信息,用来决定消息被写到哪个分区,拥有相同key的消息会被写到同一个分区
ProducerRecord record = new ProducerRecord<>("ORDER-DETAIL",
JSON.toJSONString(new Order(201806260001L, new Date(), 98000, "desc", "165120001")));
消费者
- 创建消费者
kafka消费者者对象就是KafkaConsumer,构造方式如下:
Properties props = new Properties();
// kafka集群地址
props.put("bootstrap.servers", "10.0.55.229:9092");
// ConsumerGroup即消费者组名称
props.put("group.id", "afei");
// kafka消息key的反序列化方式
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// kafka消息value的序列化方式
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer kafkaConsumer = new KafkaConsumer<>(props);
- 订阅并消费
// 订阅的topic名称
kafkaConsumer.subscribe(Lists.newArrayList("ORDER-DETAIL"));
try {
while (true) {
// 消费者必须持续从kafka进行轮询, 否则会被认为死亡, 从而导致它处理的分区被交给同一ConsumerGroup的其他消费者
ConsumerRecords records = kafkaConsumer.poll(1000);
// 为了防止消费者被认为死亡, 需要尽可能确保处理消息工作尽快完成
for (ConsumerRecord record : records) {
System.out.println("message content: "+GSON.toJson(record));
System.out.println("message value : "+record.value());
}
// 每次消费完后异步提交
kafkaConsumer.commitAsync();
}
}finally {
// 消费者关闭之前调用同步提交
kafkaConsumer.commitSync();
kafkaConsumer.close();
}
集成spring方式
现在的项目一般标配了spring,通过spring集成kafka能够大大的方便业务开发。集成方式也比较简单,只需增加如下maven坐标:
org.springframework.integration
spring-integration-kafka
3.0.3.RELEASE
生产者
- 定义生产者
spring集成kafka的生产者配置方式如下(部分属性配置通过properties解耦,用户使用时可以自定义):
- 发送消息
发送消息进行如下封装,封装后如果要发送kafka消息,只需一行代码即可,例如kafkaProducer.send(topicName, obj);
(obj就是要发送的消息对象):
/**
* @author afei
* @version 1.0.0
* @since 2018年06月11日
*/
@Component
public class MyKafkaProducer {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
// 发送的消息统一通过google-gson序列化
private static final Gson GSON = new Gson();
// KafkaTemplate就是上面xml文件中定义的bean
@Autowired
private KafkaTemplate kafkaTemplate;
public boolean send(String topic, String key, Object msg){
// 将发送的消息序列化为json
String json = toJsonString(msg);
try {
ListenableFuture> futureResult = kafkaTemplate.send(
topic, key, json);
logger.info("Kafka send json: {}, topicName: {}", json, topic);
SendResult result = futureResult.get();
// 这里的输出日志, 不能用fastjson, fastjson默认依赖bean的setter/getter方法,
// 而SendResult中的RecordMetadata的属性并没有setter/getter方法
logger.info("Kafka send result: {}", GSON.toJson(result));
return result!=null;
} catch (Throwable e) {
logger.error("Kafka send failed.", e);
}
return false;
}
public boolean send(String topic, Object msg){
return send(topic, null, msg);
}
private String toJsonString(Object o) {
String value;
if (o instanceof String) {
value = (String) o;
} else {
value = JSON.toJSONString(o);
}
return value;
}
}
消费者
- 定义消费者
spring集成kafka的消费者配置方式如下(部分属性配置通过properties解耦,用户使用时可以自定义):
// 消费者消费的topic名称
// 以开户为例, 消息由OpenAccountKafkaListener处理
// 即表示ConsumerGroup的groupId
- 消息处理
由上面的配置可以,${kafka.topic.name}
指定的topic,其消息由OpenAccountKafkaListener处理,OpenAccountKafkaListener的核心源码如下:
/**
* 钱包开户后送积分
* @author afei
* @version 1.0.0
* @since 2018年06月26日
*/
@Component
public class OpenAccountKafkaListener implements MessageListener {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void onMessage(ConsumerRecord record) {
logger.debug("msg: {}", record.value());
// 拿到消息后, 反序列化为OpenAccount
OpenAccount mqInput = JSON.parseObject(record.value(), OpenAccount.class);
//TODO 拿到开户信息后, 可以送积分, 送优惠券等
}
}
显而易见,spring集成kafka后,消费端的简单的很多。另外,我们在没用使用spring集成kafka时可以拿到kafak消费者异步提交,也可以同步提交,但是集成spring后,如何实现呢?客官老爷们稍安勿躁,继续往下看。
上面的开发消息监听器OpenAccountKafkaListener实现了接口org.springframework.kafka.listener.MessageListener,有另外一个接口org.springframework.kafka.listener.ConsumerAwareMessageListener实现了这个接口,这个接口源码如下:
@FunctionalInterface
public interface ConsumerAwareMessageListener extends MessageListener {
// 看这个方法的定义,增加了"default",即我们的业务类如果实现这个这个接口,就不需要实现这个接口,而只需要实现下面的接口即可,下面的接口有Consumer,我们就能主动执行同步提交或者异步提交了
@Override
default void onMessage(ConsumerRecord data) {
throw new UnsupportedOperationException("Container should never call this");
}
@Override
void onMessage(ConsumerRecord data, Consumer, ?> consumer);
}
- 消息处理(第二版)
@Component
public class OpenAccountKafkaListener implements ConsumerAwareMessageListener {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void onMessage(ConsumerRecord record, Consumer, ?> consumer) {
logger.info("Receive msg with consumer: {}", record.value());
OpenAccount mqInput = JSON.parseObject(record.value(), OpenAccount.class);
try {
//TODO 拿到开户信息后, 可以送积分, 送优惠券等
} finally {
// 消息处理完后异步提交
consumer.commitAsync((offsets, exception) -> {
if (exception==null){
// offsets需要用gson序列化输出
logger.info("The offset info of commit async: {}", GsonUtils.format(offsets));
}else{
logger.error("Commit async failed. ", exception);
}
});
}
}
}
深入发送消息
前面已经介绍了如何使用kafka生产者发送消息,以及如何用消费者接收消息,包括原生方式和spring集成方式,接下来我们跟踪源码看看消息在调用KafkaProducer中的send()后发送到kafka broker之前需要经过哪些处理。
- 拦截器
无论是同步调用send(),还是异步调用send()发送消息,最终都是调用下面的方法:
@Override
public Future send(ProducerRecord record, Callback callback) {
// intercept the record, which can be potentially modified; this method does not throw exceptions
ProducerRecord interceptedRecord = this.interceptors.onSend(record);
return doSend(interceptedRecord, callback);
}
由这段代码可知,消息发送前第一步就是调用拦截器(如果有的话),拦截器可以对消息进行加工。后面会单独有一篇文章详细的分析拦截器。
接下来调用doSend()方法,源码如下:
private Future doSend(ProducerRecord record, Callback callback) {
TopicPartition tp = null;
try {
// first make sure the metadata for the topic is available
// 得到集群信息和已经消耗的时间
ClusterAndWaitTime clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);
// 根据参数max.block.ms和已经消息的时间差,得到剩余时间
long remainingWaitMs = Math.max(0, maxBlockTimeMs - clusterAndWaitTime.waitedOnMetadataMs);
// 集群信息本地变量化
Cluster cluster = clusterAndWaitTime.cluster;
// 序列化key
byte[] serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
// 序列化value
byte[] serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
// 选择分区
int partition = partition(record, serializedKey, serializedValue, cluster);
tp = new TopicPartition(record.topic(), partition);
setReadOnly(record.headers());
Header[] headers = record.headers().toArray();
int serializedSize = AbstractRecords.estimateSizeInBytesUpperBound(apiVersions.maxUsableProduceMagic(),
compressionType, serializedKey, serializedValue, headers);
// 发送的消息size不允许超过max.request.size和buffer.memory两个参数的值
ensureValidRecordSize(serializedSize);
// 得到消息发送时间,默认为当前时间,除非构造ProducerRecord时指定了timestamp
long timestamp = record.timestamp() == null ? time.milliseconds() : record.timestamp();
log.trace("Sending record {} with callback {} to topic {} partition {}", record, callback, record.topic(), partition);
// producer callback will make sure to call both 'callback' and interceptor callback
Callback interceptCallback = new InterceptorCallback<>(callback, this.interceptors, tp);
if (transactionManager != null && transactionManager.isTransactional())
transactionManager.maybeAddPartitionToTransaction(tp);
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey,
serializedValue, headers, interceptCallback, remainingWaitMs);
if (result.batchIsFull || result.newBatchCreated) {
log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
this.sender.wakeup();
}
return result.future;
// handling exceptions and record the errors;
// for API exceptions return them in the future,
// for other exceptions throw directly
} catch (Exception e) {
this.errors.record();
// we notify interceptor about all exceptions, since onSend is called before anything else in this method
// 如果发送消息时有异常,那么调用所有拦截器上的onAcknowledgement()方法,所以通过拦截器种onAcknowledgement()方法的exception是否为空,判断消息是否发送成功,从而可以统计发送成功率
this.interceptors.onSendError(record, tp, e);
throw e;
}
}
- 获取集群信息
由doSend()方法源码可知,获取集群信息的源码就在waitOnMetadata()中,其源码如下:
private ClusterAndWaitTime waitOnMetadata(String topic, Integer partition, long maxWaitMs) throws InterruptedException {
// add topic to metadata topic list if it is not there already and reset expiry
metadata.add(topic);
// 第一次获取集群信息,初始化的集群信息为bootstrap.servers参数指定的集群信息
Cluster cluster = metadata.fetch();
// 从缓存的主题&分区信息map(Map>)中获取分区总数
Integer partitionsCount = cluster.partitionCountForTopic(topic);
if (partitionsCount != null && (partition == null || partition < partitionsCount))
// 如果已经缓存了,那么直接返回,且因为没有花时间在获取集群信息上,所以构造方法的第二个参数为0
return new ClusterAndWaitTime(cluster, 0);
// 记下开始时间(为了计算获取集群信息消耗的时间)
long begin = time.milliseconds();
// 初始化剩余时间就是max.block.ms参数指定的时间(即获取集群信息最大允许阻塞时间,官方文档指KafkaProducer.send() and KafkaProducer.partitionsFor()两步的时间差)
long remainingWaitMs = maxWaitMs;
long elapsed;
do {
log.trace("Requesting metadata update for topic {}.", topic);
metadata.add(topic);
// needUpdate置为true,并返回版本
int version = metadata.requestUpdate();
sender.wakeup();
try {
// 等待元数据信息更新,直到当前版本号超过上一次版本号version。另外,这个更新过程不能耗时不允许超过remainingWaitMs
metadata.awaitUpdate(version, remainingWaitMs);
} catch (TimeoutException ex) {
// Rethrow with original maxWaitMs to prevent logging exception with remainingWaitMs
throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
}
// 更新后,再次获取集群信息
cluster = metadata.fetch();
// 计算此次更新过程耗时
elapsed = time.milliseconds() - begin;
// 如果耗时超过了max.block.ms参数指定的时间,那么抛出异常
if (elapsed >= maxWaitMs)
throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
if (cluster.unauthorizedTopics().contains(topic))
throw new TopicAuthorizationException(topic);
// 根据刚才计算的此次更新消耗的时间,计算剩余时间
remainingWaitMs = maxWaitMs - elapsed;
// 得到这个topic的分区数
partitionsCount = cluster.partitionCountForTopic(topic);
// 如果这个topic分区数获取失败,那么继续获取,直到耗尽max.block.ms指定的时间
} while (partitionsCount == null);
// 如果构造ProducerRecord时指定了分区,且指定的值大于或等于分区数,那么抛出异常(例如,名为"ORDER-DETAIL"的topic,有7个分区,如果构造ProducerRecord时指定了partition的值且为7或者大于7,那么就会抛出这个异常,异常信息为:Invalid partition given with record: 7 is not in the range [0...7).)
if (partition != null && partition >= partitionsCount) {
throw new KafkaException(
String.format("Invalid partition given with record: %d is not in the range [0...%d).", partition, partitionsCount));
}
// 返回集群信息和剩余时间
return new ClusterAndWaitTime(cluster, elapsed);
}
通过这段源码分析可知,当我们构造KafkaProducer时指定的bootstrap.servers的值,不一定要和kafka集群信息完全一致,kafka-client可以通过参数bootstrap.servers指定的broker,然后从broker上获取到整个kafka集群元数据信息。但是即使是这样,参数bootstrap.servers也建议尽量完整。例如整个集群有3个broker,如果bootstrap.servers只指定了1个broker,那么当这个broker宕机后,虽然集群状态可用。但是
- 序列化
即经过拦截器链后另一个非常重要的操作:对key&value的序列化。核心代码是如下两行,对key的序列化,调用的方法是构造KafkaProducer时参数key.serializer指定的serializer,对value的序列化,调用的方法是构造KafkaProducer时参数value.serializer指定的serializer:
keySerializer.serialize(record.topic(), record.headers(), record.key());
valueSerializer.serialize(record.topic(), record.headers(), record.value());
- 分区
接下来就是选择分区,核心代码如下:
int partition = partition(record, serializedKey, serializedValue, cluster);
- 总结
根据上面的分析可知,消息发送经过的几个重要过程按照先后顺序依次是:拦截器,获取元数据,序列化,选择分区。接下来的文章会一一详细分析这些必要重要的过程。