3.生产者-客户端开发

客户端开发

一个正常的生产逻辑需要具备以下几个步骤:
1.配置生产者客户端参数及创建相应的生产者实例
2.构建待发送的消息
3.发送消息
4.关闭生产者实例

//代码清单3-1 生产者客户端示例
public class KafkaProducerAnalysis {
    public static final String brokerList = "localhost:9092";
    public static final String topic="topic-demo";

    public static Properties initConfig(){
        Properties properties=new Properties();
        //broker 地址清单
        properties.put("bootstrap.servers",brokerList);
        //序列化key
        properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
        //序列化value
        properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
        //设定 KafkaProducer 对应的客户端id,默认值为“”,如果不设置则自动生成,内容如“producer-1”、“producer-2”等
        properties.put("client.id","producer.client.id.demo");
        return properties;
    }

    public static void main(String[] args) {
        Properties properties=initConfig();
        Producer<String,String> producer=new KafkaProducer<String, String>(properties);
        producer.send(new ProducerRecord<>(topic,"Hello,Kafka"));
        producer.close();
    }
}

构建的消息对象 ProduceRecord并不是单纯意义上的消息,它包含了多个属性,原本需要发送的与业务相关的消息体只是其中的一个 value 属性,比如“Hello,kafka”只是 ProduceRecord 对象中的一个属性。
注意:这样直接发送是发送不成功的,需要在kafka的配置文件中修改host.name=本机ip,同时使用kafka自带的生产者和消费者客户端测试时,不能使用localhost,须明确指定本机域名。原因如下

public class ProducerRecord<K, V> {
    private final String topic; //主题
    private final Integer partition; //分区号
    private final Headers headers; //消息头部
    private final K key; //键
    private final V value; //值
    private final Long timestamp; //消息的时间戳
    //省略其他成员方法和构造方法
}

其中 topic 和 partition 字段分别代表消息要发往的主题和分区号。headers 字段是消息的头部,它大多用来设定一些与应用相关的信息,如果不需要也可以不用设定。key 是用来指定消息的键,它不仅仅是附加消息,还可以用来计算进而发送消息到特定的分区。前面提及消息以主题进行归类,而这个 key 可以使消息进行二次归类,同一个 key 的消息会发送到同一个分区。

有 key 的消息还可以支持日志压缩的功能。timestamp是消息的时间戳,它有 CreateTime 和 LogAppendTime 两种类型,前者表示消息创建时间,后者表示消息追加到日志文件的时间。

必要的参数配置

在创建生产者实例前需要配置相应参数,比如连接 Kafka 集群地址。在 Kafka 生产者客户端 KafkaProducer 中有3个参数是必填的。

bootstrap.servers: 该参数用来指定生产者客户端连接 Kafka 集群所需要的 broker 地址清单,具体的内容格式为 host1:port1,host2:port2,可以设置一个或多个地址,中间以逗号隔开,此参数默认值为“”。注意,这里并不需要指定所有的 broker ,指定一个后生产者会从给定的 broker 中查找到集群中其他的 broker。但是,一般情况下都会设置两个以上,这样可以保证当一个 broker 宕机时,生产者仍然可以连接到 Kafka 集群。

key.serializer 和 value.serializer: broder 端接收的消息必须是以字节数组(byte[])的形式存在。在代码中生产者使用的 KafkaProducer<String,String>和ProducerRecord<Strinig,String>中的泛型 <String,String> 对应的就是消息中key和value的类型,这种方式使代码具有很好的可读性,不过在发往 broker 前需要将消息的key和value序列化成字节数组(消息的key和value是什么类型,相应的序列化器必须为对应类型)。key.serializer 和 vaue.serializer 这两个参数分别用来指定key和value的序列化器,这两个参数无默认值。这里必须写序列化器的全名。

在实际使用过程中,由于参数众多,常由于人为因素而书写错误,为此,我们可以直接使用客户端中的org.apache.kafka.clients.producer.ProducerConfig类来做一定的预防措施,每个参数在 ProducerConfig 类都有对应的名称:

public static Properties initConfig(){
        Properties properties=new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,brokerList);
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
        properties.put(ProducerConfig.CLIENT_ID_CONFIG,"producer.client.id.demo");
        return properties;
    }

上面代码中 key.serializer 和 value.serializer 参数对应类的全限定名比较长,也比较容易错,这里通过Java中的技巧做进一步改进:

		properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());

KafkaProducer中有多个构造方法,比如说创建 KafkaProducer实例的时候没有设定 key.serializer 和 value.serializer 这两个配置参数,那么就需要在构造方法中添加对应的序列化器:

Producer<String,String> producer=new KafkaProducer<String, String>(properties,new StringSerializer(),new StringSerializer());

其内部原理和无序列化器的构造方法一样,不过就实际应用而言,一般都选用 public KafkaProducer(Properties properties)这个构造方法来创建 KafkaProducer 实例。
KafkaProducer 是线程安全的,可以在多个线程中共享单个 KafkaProducer 实例,也可以将 KafkaProducer 实例进行池化来供其他线程调用。

消息的发送

在创建完生产者实例后,接下来就是构建消息,即创建 ProducerRecord 对象。其中,topic 属性和 value 属性是必填项,其余项是选填项,对应的 ProducerRecord 的构造方法有:

public ProducerRecord(String topic, Integer partition, Long timestamp, 
                      K key, V value, Iterable<Header> headers)
public ProducerRecord(String topic, Integer partition, Long timestamp,
                      K key, V value)
public ProducerRecord(String topic, Integer partition, K key, V value, 
                      Iterable<Header> headers)
public ProducerRecord(String topic, Integer partition, K key, V value)
public ProducerRecord(String topic, K key, V value)
public ProducerRecord(String topic, V value)

如果发送的时候没有指定主题的分区,那么消息会均衡的分配到各个分区。
针对不同的消息,需要构建不同的 ProducerRecord 对象,在实际应用中创建 ProducerRecord 对象是一个非常频繁的动作。创建生产者实例和构建消息之后,就可以开始发送消息了。发送消息主要有三种模式:发后即忘(fire-and-forget)、同步(sync)及异步(async)。

代码清单3-1中的这种发送方式就是发后即忘,它只管往Kafka发送消息,但是不去关注消息是否正确到达。在大多数情况下,这种发送方式没什么问题,不过在某些时候(比如发生不可重试异常时)会造成消息的丢失。这种方式性能最高,但是可靠性相对也最差。

KafkaProducer的send()方法并非是void类型,而是Future类型,**send()**方法有2个重载方法,具体定义如下:

	Future<RecordMetadata> send(ProducerRecord<K, V> var1);

    Future<RecordMetadata> send(ProducerRecord<K, V> var1, Callback var2);

要实现同步的发送方式,可以利用返回的 Future 对象实现,实例如下:

		try {
            producer.send(new ProducerRecord<>(topic,"Hello,Kafka")).get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

send()方法是异步的,添加消息到缓冲区等待发送,并立即返回。生产者将单个的消息批量在一起发送来提高效率。

生产者缓存每个分区未发送的消息,多条消息组成一个批次,大小是通过 batch.size 配置指定的。值较大的话将会产生更大的批。并需要更多的内存(因为每个“活跃”的分区都有1个缓冲区)。

默认缓冲可立即发送,即便缓冲空间还没有满,但是,如果你想减少请求的数量,可以设置linger.ms大于0。这将指示生产者发送请求之前等待一段时间,希望更多的消息填补到未满的批中。这类似于TCP的算法,比如我们设置了linger(逗留)时间为1毫秒,然后,如果我们没有填满缓冲区,这个设置将增加1毫秒的延迟请求以等待更多的消息。需要注意的是,在高负载下,相近的时间一般也会组成批,即使是 linger.ms=0。在不处于高负载的情况下,如果设置比0大,以少量的延迟代价换取更少的,更有效的请求。

buffer.memory 控制生产者可用的缓存总量,如果消息发送速度比其传输到服务器的快,将会耗尽这个缓存空间。当缓存空间耗尽,其他发送调用将被阻塞,阻塞时间的阈值通过max.block.ms设定,之后它将抛出一个TimeoutException。

send() 方法返回的 Future 对象可以使调用方稍后获得发送的结果。示例中在执行 send() 方法之后直接链式调用了 get() 方法来阻塞等待 Kafka的响应,直到消息发送成功或发生异常,如果发生异常那么就需要捕获异常并交由外层逻辑处理。
也可以在执行完send()方法之后不直接调用get()方法,比如下面的一种同步发送方式的实现:

		try {
		
           Future<RecordMetadata> future =  producer.send(new ProducerRecord<>(topic,"Hello,Kafka"));
           RecordMetadata metadata = future.get();
            System.out.println(metadata.topic() + "-" +metadata.partition() + ":" + metadata.offset());
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

这样可以获取一个 RecordMetadata 对象,在 RecordMetadata 对象里面包含了消息的一些元数据信息,比如当前消息的主题、分区号、分区中的偏移量、时间戳等。

Future 表示一个任务的生命周期,并提供了相应的方法判断任务已经完成还是取消,以及获取任务的结果和取消任务等。既然 KafkaProducer.send() 方法返回的是一个 Future 类型的对象,那么完全可以用Java语言层面技巧来丰富应用的实现,比如使用 Future中的get(long timeout, TimeUnit unit)方法实现可超时的阻塞(Future是Java并发包中的接口。主要用于获取目标线程的结果,但是可能会使当前线程阻塞。V get(long timeout, TimeUnit unit):带超时限制的get(),等待超时之后,该方法会抛出TimeoutException。)

KafkaProducer中一般会发送两种异常:可重试异常和不可重试异常。常见的可重试异常有:NetworkException、LeaderNotAvailableException、UnknownTopicOrPartitionException、NotEnoughReplicasException、NotCoordinatorException 等。比如 NetworkException 表示网络异常,这个有可能是由于网络瞬时故障而导致的异常,可以通过重试解决;又比如 LeaderNotAvailableException 表示分区的 leader 副本不可用,这个异常通常发生在 leader 副本下线而新的 leader 副本选举完成之前,重试之后可以重新恢复。不可重试的异常,比如第2节中提及的 RecordTooLargeException 异常,暗示了所发送的消息太大,KafkaProducer 对此不会进行任何重试,直接抛出异常。对于可重试异常,如果配置了retries参数,那么只要在规定的重试次数内自行恢复了,就不会抛出异常。retries参数的默认值为0,配置方式参考:

properties.put(ProducerConfig.RETRIES_CONFIG, 10);

示例中配置了10次重试。如果10次后还没有恢复,那么仍然会抛出异常,进而发送的外层逻辑就要处理这些异常了。

同步发送的方式可靠性高,要么消息发送成功要么失败。如果发送异常,则可以捕获并进行相应的处理,而不会像“发后即忘”的方式直接造成消息丢失。同步的发送方式性能要低很多,需要阻塞等待一条消息发送成功后才可以发送下一条。

异步发送
再了解下异步发送,一般是在send()方法里指定一个 Callback的回调函数,Kafka在返回响应时调用该函数来实现异步的发送确认。send()方法返回类型就是 Future,而 Future 本身就可以用作异步的逻辑处理。这样做不是不行,只不过 Future 里的get()方法在何时调用,以及怎么调用都是面对的问题,消息不停发送,就有很多消息对应的 Future对象的处理难免会引起代码处理逻辑的混乱。而使用Callback 的方法非常简单明了,Kafka有响应了就回调,要么发送成功要么抛出异常。

producer.send(new ProducerRecord<>(topic, "Hello,Kafka"), new Callback() {
            @Override
            public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                if (e != null){
                    e.printStackTrace();
                }else {
                    System.out.println("回调值:"+recordMetadata.topic() + "-" +
                            recordMetadata.partition() + ":" + recordMetadata.offset());
                }
            }
        });

onCompletion方法的两个参数是互斥的,消息发送成功时候,recordMetadata不为null而e为null;消息发送异常时,recordMetadata为null而e不为null;

producer.send(record1, callback1);
producer.send(record2, callback2);

对同一个分区来说,如果消息record1于record2之前先发送(参考上面代码),那么 KafkaProducer 就可以保证对应的 callback1 在 callback2 之前调用,也就是说回调函数的调用可以保证 按分区有序

通常,一个 KafkaProducer 不会只负责发送单条消息,更多的是发送多条。在发送完成之后,会调用 KafkaProducer的 close() 方法来回收资源。close() 方法会阻塞等待之前所有的发送请求完成后在关闭 KafkaProducer。于此同时,还提供了一个带超时时间的 close() 方法,如果调用了带超时时间的方法,那么超过 timeout 时间会强行退出。

public void close(long timeout, TimeUnit timeUnit)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值