4、消费者客户端:Consumer API、消费组与重平衡、位移提交、反序列化器
好,咱们今天聊聊 Kafka 消费者。说实话,生产环境中大部分问题都出在消费者端。生产者那边只要集群稳定、网络不抖,基本不会出大乱子。但消费者?嗯,坑多得很。
我刚开始用 Kafka 那会儿,觉得消费者 API 挺简单的——不就是 poll 一下拿数据嘛。结果线上第一个版本就踩了重平衡的坑,整个消费组停了十几秒。从那以后,我再也不敢小看消费者了。
4.1 Consumer API 基础用法
Kafka 消费者最核心的操作就是订阅主题、拉取消息。说白了,就是一个循环:subscribe() 然后 poll()。
先看一段最基础的代码:
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "my-group");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("my-topic"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("offset = %d, key = %s, value = %s%n",
record.offset(), record.key(), record.value());
}
}
这段代码看着简单,但有几个细节我提醒一下:
poll()的超时时间:100 毫秒是常用的值。太短了空转浪费 CPU,太长了影响消费延迟。我个人习惯设 100~200ms。- 循环不能停:消费者必须持续 poll,否则超过
max.poll.interval.ms(默认 5 分钟)会被判定为死亡,触发重平衡。 - 关闭资源:生产环境记得加
finally块调用consumer.close(),否则连接泄漏。
@KafkaListener 注解帮你封装了 poll 循环和位移提交。但底层原理还是这套 API,出了问题排查时,你得知道它背后怎么跑的。
4.2 消费组与重平衡
消费组是 Kafka 实现水平扩展的核心机制。你想想看,一个主题有 10 个分区,单机消费太慢怎么办?加机器。把消费者放到同一个组里,Kafka 会自动分配分区。
我画了一张图,帮你理解消费组的分区分配逻辑:
重平衡(Rebalance)是什么?说白了就是消费组成员发生变化时,Kafka 重新分配分区的过程。触发条件有三个:
- 消费者加入或离开:比如新启动一个消费者,或者某个消费者挂了。
- 主题分区数变化:你给主题增加了分区。
- 消费者超时:超过
session.timeout.ms(默认 45 秒)没发心跳。
怎么减少重平衡的影响?我建议你关注这几个参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
session.timeout.ms |
45000 | 心跳超时时间。网络不稳定时可以适当调大 |
heartbeat.interval.ms |
3000 | 心跳间隔。建议设为 session.timeout.ms 的 1/3 |
max.poll.interval.ms |
300000 | 两次 poll 的最大间隔。处理慢的业务可以调大 |
partition.assignment.strategy |
RangeAssignor | 分区分配策略。推荐用 StickyAssignor,减少重平衡时的分区变动 |
4.3 位移提交
位移提交,就是消费者告诉 Kafka:「我已经处理到 offset=X 了,下次从这个位置继续消费。」
这里有个关键选择:自动提交还是手动提交?
自动提交很简单,设置 enable.auto.commit=true(默认就是 true),然后配个 auto.commit.interval.ms(默认 5 秒)。Kafka 会在后台定时提交当前 poll 到的最大 offset。
但自动提交有个坑:可能丢数据或重复消费。举个例子,你 poll 了一批消息,处理到一半程序挂了。自动提交还没触发,重启后从上次提交的 offset 开始消费,那已经处理完的消息会重复处理。反过来,如果自动提交先触发了,但消息还没处理完,程序挂了,那这些消息就丢了。
所以生产环境,我强烈建议用手动提交:
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
// 处理业务逻辑
process(record);
}
// 同步提交,确保处理完才提交
consumer.commitSync();
}
手动提交有两种方式:
commitSync():同步阻塞,提交成功才返回。可靠性高,但吞吐量受影响。commitAsync():异步提交,不阻塞。吞吐量高,但可能提交失败。
我个人习惯的做法是:正常流程用 commitAsync(),程序退出前或发生异常时用 commitSync() 兜底。这样既保证了吞吐量,又不会丢位移。
commitSync(Map<TopicPartition, OffsetAndMetadata>) 提交指定分区的指定 offset。比如你处理完一批数据后,想精确控制每个分区的提交位置。
4.4 反序列化器
生产者把对象序列化成字节数组发到 Kafka,消费者就得反序列化回来。Kafka 内置了几种常用的反序列化器:
| 反序列化器 | 说明 | 适用场景 |
|---|---|---|
StringDeserializer |
字节数组转 String | 文本数据、JSON 字符串 |
IntegerDeserializer |
字节数组转 Integer | 整数类型的 key 或 value |
LongDeserializer |
字节数组转 Long | 时间戳、ID 等长整型 |
ByteArrayDeserializer |
直接返回原始字节数组 | 自定义反序列化、二进制数据 |
但实际项目中,我们很少直接用这些基础类型。更多时候,消息是 JSON、Avro、Protobuf 格式。这时候需要自定义反序列化器。
举个例子,假设消息是 JSON 格式的用户信息:
public class UserDeserializer implements Deserializer<User> {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public User deserialize(String topic, byte[] data) {
if (data == null) return null;
try {
return objectMapper.readValue(data, User.class);
} catch (Exception e) {
throw new SerializationException("反序列化失败", e);
}
}
@Override
public void close() {
// 清理资源
}
}
然后在配置里指定:
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
UserDeserializer.class.getName());
如果你用 Avro 或 Protobuf,建议配合 Schema Registry 使用。这样生产者和消费者之间通过 schema 版本协商,避免了字段不兼容的问题。嗯,这个后面讲序列化的时候会细说。
4.5 消费者端的最佳实践
最后,我总结几条消费者端的实战经验:
- 消费逻辑要轻量:不要在 poll 循环里做耗时操作,比如调用外部 API、写数据库。否则容易触发
max.poll.interval.ms超时。建议用线程池异步处理。 - 优雅关闭:用
Runtime.getRuntime().addShutdownHook()捕获 JVM 退出信号,先调用consumer.wakeup()中断 poll,再consumer.close()。 - 监控消费延迟:通过
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group my-group --describe查看 LAG(积压量)。LAG 持续增长说明消费能力跟不上。 - 幂等消费:因为可能重复消费,业务逻辑最好设计成幂等的。比如数据库操作用 UPSERT 而不是 INSERT。
消费者这块内容不少,但核心就三个词:组、位移、反序列化。把这三点搞透了,大部分消费端的问题你都能自己排查。