Spring Kafka 可以为我们提供非常简单易用的的上层 API 支持. 在一般处理无需幂等的数据场景下, 我们可以使用默认配置 enable-auto-commit 来使用消息队列. 这里有个疑问, auto-commit 究竟是怎么实现的, 具体在怎样的场景中适用 auto-commit 配置? 带着这些问题, 本节基于 spring-kafka2.7.0 跟踪源码, 观察自动提交的底层实现.
依赖
Spring Kafka 是对 kafka-client 的再封装, 这里列一下, 本文使用的 jar 包版本
- spring-kafka2.7.0
- kafka-client2.6.0
结论
为了方便大家往后理解源码实现, 先说结论.
spring-kafka 中并没有实现自动提交的相关功能, 它只是将 ‘enable-auto-commit=true’ 这个参数交给了 kafka-client. kafka-client 消费者每次成功从 Topic 拉取 (poll) 数据后, 都会递增更新消费者偏移量. kafka-client 中有个 SubscriptionState 的类, 专门存储当前消费者监听的 topics、partition、offset 信息.
消费者 poll 消息前, 会触发对 GroupCoordinator 心跳机制校验, 在缺省配置中, kafka-client 每 5 秒会从 SubscriptionState 中获取当前消费者的所有 topics、partition、offset 信息, 并主动发起异步更新的请求.

源码跟踪
SpringKafka源码doPoll
KafkaMessageListenerContainer.doPoll
前置章节介绍了 spring-kafka 的结构, 我们知道 KafkaMessageListenerContainer 与 kafka-client 实例是一一对应的关系. 并且 KafkaMessageListenerContainer.doPoll() 这个方法是 consumer 获取消息 (Message) 的入口.
在 doPoll() 中, 是调用 this.consumer.poll(this.pollTimeout) 主动拉取数据
@Nullable
private ConsumerRecords<K, V> doPoll() {
ConsumerRecords<K, V> records;
if (this.isBatchListener && this.subBatchPerPartition) {
if (this.batchIterator == null) {
this.lastBatch = this.consumer.poll(this.pollTimeout);
if (this.lastBatch.count() == 0) {
return this.lastBatch;
}
else {
this.batchIterator = this.lastBatch.partitions().iterator();
}
}
TopicPartition next = this.batchIterator.next();
List<ConsumerRecord<K, V>> subBatch = this.lastBatch.records(next);
records = new ConsumerRecords<>(Collections.singletonMap(next, subBatch));
if (!this.batchIterator.hasNext()) {
this.batchIterator = null;
}
}
else {
records = this.consumer.poll(this.pollTimeout);
checkRebalanceCommits();
}
return records;
}
前置章节内容请优先阅读
SpringKafka原理解析及源码学习-Spring生态(一): http://blog.diswares.cn/kafka-spring-kafka-structure/
KafkaConsumer.poll
- 这个方法中可以看到一个 do while 的结构, while 中的判断我们可以知道, 当一次 poll 超过一定时间还是没有拉取到数据, 会认为本次拉取失败从而不继续 poll
- do while 里的代码块, 有两个比较关键的方法: updateAssignmentMetadataIfNeeded(timer, false) 和 pollForFetches(timer)
- updateAssignmentMetadataIfNeeded(timer, false): 此方法为自动提交偏移量的入口, 也是更新 fetcher 信息的入口
- pollForFetches(timer): 拉取新数据
@Override
public ConsumerRecords<K, V> poll(final Duration timeout) {
return poll(time.timer(timeout), true);
}
private ConsumerRecords<K, V> poll(final Timer timer, final boolean includeMetadataInTimeout) {
acquireAndEnsureOpen();
try {
// 记录一下 本次 poll 的时间信息
this.kafkaConsumerMetrics.recordPollStart(timer.currentTimeMs());
if (this.subscriptions.hasNoSubscriptionOrUserAssignment()) {
throw new IllegalStateException("Consumer is not subscribed to any topics or assigned any partitions");
}
// do while 直至超时
do {
client.maybeTriggerWakeup();
if (includeMetadataInTimeout) {
// 更新 Fetcher 的元信息并自动提交 offset
updateAssignmentMetadataIfNeeded(timer, false);
} else {
while (!updateAssignmentMetadataIfNeeded(time.timer(Long.MAX_VALUE), true)) {
log.warn("Still waiting for metadata");
}
}
// nio 拉取新消息
final Map<TopicPartition, List<ConsumerRecord<K, V>>> records = pollForFetches(timer);
if (!records.isEmpty()) {
if (fetcher.sendFetches() > 0 || client.hasPendingRequests()) {
client.transmitSends();
}
return this.interceptors.onConsume(new ConsumerRecords<>(records));
}
// 超时校验
} while (timer.notExpired());
return ConsumerRecords.empty();
} finally {
release();
this.kafkaConsumerMetrics.recordPollEnd(timer.currentTimeMs()

本文详细解读了SpringKafka中自动提交偏移量的底层实现,涉及KafkaConsumer.poll、ConsumerCoordinator和fetcher的交互过程。通过源码跟踪,揭示了自动提交是基于心跳机制异步进行的,每5秒更新消费者位置信息并主动请求更新。
最低0.47元/天 解锁文章
1154

被折叠的 条评论
为什么被折叠?



