3. 生产者客户端:Producer API、发送模式、分区策略与序列化器

好,咱们今天聊聊 Kafka 生产者的核心。说白了,生产者就是往 Kafka 里写数据的那个角色。你想想看,整个数据管道能不能跑得稳,很大程度上取决于生产者写得对不对、快不快。

我记得刚接手第一个 Kafka 项目时,生产者的配置我调了一周。不是因为难,而是坑太多。今天我把这些经验掰开揉碎讲给你听。

3.1 Producer API 的基本用法

Kafka 的 Producer API 其实不复杂。核心就几个步骤:配参数、建客户端、发消息、关连接。

先看一个最基础的例子:

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
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<>(props);
producer.send(new ProducerRecord<>("my-topic", "key1", "hello kafka"));
producer.close();

嗯,这里要注意:bootstrap.servers 是必须的,它告诉客户端去哪找集群。我见过有人只配一个 broker 地址,结果那个 broker 挂了,客户端连不上。其实你配多个地址,客户端会自动发现集群里的其他节点。

我的习惯:bootstrap.servers 至少配 2-3 个 broker 地址,用逗号分隔。这样就算某个节点宕机,客户端也能正常启动。

3.2 发送模式:同步 vs 异步

Kafka 生产者支持三种发送模式。我按实际场景给你拆开讲。

发送模式 实现方式 适用场景 吞吐量
发后即忘 producer.send(record) 日志、监控等可丢失数据 最高
同步发送 producer.send(record).get() 关键交易、需要确认的场景 最低
异步发送 producer.send(record, callback) 大部分业务场景 高

发后即忘:就是只管发,不管结果。我早期做日志采集时用过,吞吐量确实高,但有一次集群抖动,丢了一批数据,排查了半天。所以,重要数据别用这个模式。

同步发送:调用 .get() 阻塞等待结果。我曾经在一个支付对账场景里用过,每条消息都要确认写入成功才发下一条。吞吐量确实低,但心里踏实。

异步发送:这是我最常用的模式。传一个 Callback 进去,发送线程不阻塞,等 broker 返回结果后再回调。你看这个例子:

producer.send(new ProducerRecord<>("orders", orderId, orderData), 
    (metadata, exception) -> {
        if (exception != null) {
            log.error("发送失败,订单ID: {}", orderId, exception);
            // 这里可以重试或写入死信队列
        } else {
            log.info("发送成功,分区: {}, 偏移量: {}", 
                metadata.partition(), metadata.offset());
        }
    });
避坑指南:我曾经在 Callback 里做了耗时操作,结果把网络线程堵死了。记住,Callback 是在 IO 线程里执行的,别在里面做重试、写数据库这些事。真要处理,丢到线程池里去。

3.3 分区策略:数据该去哪

Kafka 的消息最终要落到某个分区上。分区策略决定了数据怎么分布。我画了一张图帮你理解:

Kafka 生产者分区策略流程 生产者客户端 分区策略判断 指定分区?有 key?还是默认轮询? 指定分区 key 哈希取模 轮询 / 粘性分区 写入目标分区 默认策略:有 key 按 key 哈希,无 key 用粘性分区(Sticky Partitioner)

Kafka 默认的分区策略是这样的:

  • 如果消息指定了分区号,直接写入那个分区。我一般只在需要严格顺序的场景用这个,比如同一个订单的所有事件必须进同一个分区。
  • 如果消息有 key,对 key 做哈希,然后对分区数取模。这样相同 key 的消息永远进同一个分区。我做过一个用户行为分析项目,用用户 ID 做 key,同一个用户的行为都在一个分区里,消费端处理起来特别方便。
  • 如果既没分区号也没 key,Kafka 2.4 之前用轮询,之后用粘性分区。粘性分区会把一批消息攒起来发到同一个分区,减少网络开销。吞吐量能提升 30% 左右。
我的经验:如果你需要保证消息顺序,一定要用 key。但要注意,key 的分布要均匀。我见过有人用时间戳做 key,结果所有消息都哈希到同一个分区,其他分区空转。用用户 ID、订单 ID 这类天然分散的字段会好很多。

3.4 序列化器:数据怎么变成字节

Kafka 网络传输只认字节数组。序列化器就是把你的 Java 对象转成字节的东西。

Kafka 内置了几种常用序列化器:

序列化器 说明 适用场景
StringSerializer 字符串转 UTF-8 字节 文本数据、JSON 字符串
IntegerSerializer 整数转 4 字节 数值型 key
ByteArraySerializer 直接传字节数组 已经序列化好的数据
自定义序列化器 实现 Serializer 接口 Protobuf、Avro、自定义对象

我建议你用 Avro 或者 Protobuf。为什么?

  • JSON 虽然可读性好,但体积大、解析慢。我有个项目用 JSON 传日志,一天几个 TB,光序列化就占了 30% 的 CPU。
  • Avro 带 Schema,生产者和消费者可以约定好 Schema,数据格式变了也能兼容。我后来把那个日志项目改成 Avro,CPU 降了一半,带宽也省了 40%。

自定义序列化器也不难,实现 Serializer<T> 接口就行:

public class OrderSerializer implements Serializer<Order> {
    @Override
    public byte[] serialize(String topic, Order data) {
        // 把 Order 对象转成字节数组
        // 可以用 Protobuf、Avro 或者自己拼字节
        return data.toByteArray();
    }
}
我曾经踩过的坑:自定义序列化器里如果抛出异常,生产者会直接报错,消息发不出去。一定要做好异常处理,别让序列化器成为瓶颈。另外,序列化器是单例的,别在里面放线程不安全的对象。

嗯,到这里生产者客户端的主要内容就讲完了。总结一下:API 的用法要熟,发送模式按场景选,分区策略决定了数据分布,序列化器影响性能和兼容性。这些点在实际项目中一个都绕不开。