依赖
<dependency>
<groupId>org.apache.kafkagroupId>
<artifactId>kafka-clientsartifactId>
<version>2.0.0version>
dependency>
发送端代码
public class MyKafkaProducer extends Thread{
//producer api
KafkaProducer<Integer,String> producer;
String topic; //主题
public MyKafkaProducer(String topic) {
Properties properties=new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.10.150:9092,192.168.10.151:9092,192.168.10.152:9092");
properties.put(ProducerConfig.CLIENT_ID_CONFIG,"my-producer");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
//连接的字符串
//通过工厂
//new
producer=new KafkaProducer<Integer, String>(properties);
this.topic = topic;
}
@Override
public void run() {
int num=0;
while(num<20) {
//get 会拿到发送的结果
//同步 get() -> Future()
String msg="pratice test message:"+num;
try {
producer.send(new ProducerRecord<Integer, String>
(topic,msg)).get();
TimeUnit.SECONDS.sleep(2);
num++;
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
/*try {
String msg="my kafka practice msg:"+num;
//回调通知
//异步
producer.send(new ProducerRecord<>(topic, msg), (metadata, exception) -> {
System.out.println(metadata.offset()+"->"+metadata.partition()+"->"+metadata.topic());
});
TimeUnit.SECONDS.sleep(2);
++num;
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
}
public static void main(String[] args) {
new MyKafkaProducer("test_partition").start();
}
}
消费端代码
public class MyKafkaConsumer extends Thread{
KafkaConsumer<Integer,String> consumer;
String topic;
public MyKafkaConsumer(String topic) {
Properties properties=new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.10.150:9092,192.168.10.151:9092,192.168.10.152:9092");
properties.put(ConsumerConfig.CLIENT_ID_CONFIG,"my-consumer");
properties.put(ConsumerConfig.GROUP_ID_CONFIG,"my-gid3");
properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG,"30000");
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"5000"); //自动提交(批量确认)
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
//一个新的group的消费者去消费一个topic
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest"); //从头开始消费
consumer=new KafkaConsumer<Integer, String>(properties);
this.topic = topic;
}
@Override
public void run() {
consumer.subscribe(Collections.singleton(this.topic));
while(true){
ConsumerRecords<Integer,String> consumerRecords=consumer.poll(Duration.ofSeconds(1));
consumerRecords.forEach(record->{
//null->my kafka practice msg:0->63
System.out.println(record.key()+"->"+record.value()+"->"+record.offset());
});
}
}
public static void main(String[] args) {
new MyKafkaConsumer("test_partition").start();
}
}
kafka对于消息的发送,可以支持同步和异步,前面演示的案例中,我们是基于同步发送消息。
同步会 需要阻塞,而异步不需要等待阻塞的过程。 从本质上来说,kafka 都是采用异步的方式来发送消息到 broker,但是 kafka 并不是每次发送消息都会直 接发送到 broker上,而是把消息放到了一个发送队列中,然后通过一个后台线程不断从队列取出消息进 行发送,发送成功后会触发 callback。kafka 客户端会积累一定量的消息统一组装成一个批量消息发送出去,触发条件是前面提到的batch.size
和linger.ms
而同步发送的方法,无非就是通过future.get()来等待消息的发送返回结果,但是这种方法会严重影响消 息发送的性能。
//发送端中run方法修改
@Override
public void run() {
int num=0;
while(num<20) {
try {
String msg="my kafka practice msg:"+num;
//get 会拿到发送的结果
//同步 get() -> Future()
//回调通知
producer.send(new ProducerRecord<>(topic, msg), (metadata, exception) -> {
System.out.println(metadata.offset()+"->"+metadata.partition()+"->"+metadata.topic());
});
TimeUnit.SECONDS.sleep(2);
++num;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
生产者发送多个消息到 broker上 的同一个分区时,为了减少网络请求带来的性能开销,通过批量的方式 来提交消息,可以通过这个参数来控制批量提交的字节数大小,默认大小是16384byte,也就是16kb, 意味着当一批消息大小达到指定的 batch.size 的时候会统一发送
Producer 默认会把两次发送时间间隔内收集到的所有 Requests 进行一次聚合然后再发送,以此提高吞 吐量,而 linger.ms 就是为每次发送到 broker 的请求增加一些 delay,以此来聚合更多的 Message 请求。 这个有点像 TCP 里面的 Nagle 算法,在 TCP 协议的传输中,为了减少大量小数据包的发送,采用了 Nagle 算法,也就是基于小包的等-停协议。
batch.size 和 linger.ms 这两个参数是 kafka 性能优化的关键参数,我们会发现 batch.size 和 linger.ms 这两者的作用是一样的,如果两个都配置了,那么怎么工作的呢?实际上,当二者都配 置的时候,只要满足其中一个要求,就会发送请求到broker上
consumer group 是 kafka 提供的可扩展且具有容错性的消费者机制。既然是一个组,那么组内必然可以 有多个消费者或消费者实例(consumer instance),它们共享一个公共的ID,即group ID。组内的所有 消费者协调在一起来消费订阅主题(subscribed topics)的所有分区(partition)。当然,每个分区只能由 同一个消费组内的一个 consumer 来消费。
如下图所示,分别有三个消费者,属于两个不同的 group,那 么对于 firstTopic 这个 topic 来说,这两个组的消费者都能同时消费这个 topic 中的消息,对于此时的架构来说,这个 firstTopic 就类似于 ActiveMQ 中的 topic 概念。
如下图所示,如果3个消费者都属于同一个 group,那么此时 firstTopic 就是一个 Queue 的概念
消费者消费消息以后自动提交,只有当消息提交以后,该消息才不会被再次接收到,还可以配合 auto.commit.interval.ms 控制自动提交的频率。
当然,我们也可以通过 consumer.commitSync() 的方式实现手动提交
这个参数是针对新的 groupid 中的消费者而言的,当有新 groupid 的消费者来消费指定的 topic 时,对于该参数的配置,会有不同的语义
此设置限制每次调用 poll 返回的消息数,这样可以更容易的预测每次 poll 间隔要处理的最大值。通过调 整此值,可以减少 poll 间隔
springboot 的版本和 kafka 的版本,有一个对照表格,如果没有按照正确的版本来引入,那么会存在版 本问题导致 ClassNotFound 的问题,具体请参考
https://spring.io/projects/spring-kafka
jar包依赖
<dependency>
<groupId>org.springframework.kafkagroupId>
<artifactId>spring-kafkaartifactId>
<version>2.8.4version>
dependency>
KafkaProducer
@Component
public class KafkaProducer {
@Autowired
private KafkaTemplate<String,String> kafkaTemplate;
public void send(){
kafkaTemplate.send("test","msgKey","msgData");
}
}
KafkaConsumer
@Component
public class KafkaConsumer {
@KafkaListener(topics = {"test"})
public void listener(ConsumerRecord record){
Optional<?> msg=Optional.ofNullable(record.value());
if(msg.isPresent()){
System.out.println(msg.get());
}
}
}
application配置
spring.kafka.bootstrap.servers=192.168.10.150:9092,192.168.10.151:9092,192.168.10.152:9092
spring.kafka.producer.key.serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value.serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.consumer.group-id=test-consumer-group
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.enable-auto-commit=true
spring.kafka.consumer.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
测试
public static void main(String[] args) {
ConfigurableApplicationContext context=SpringApplication.run
(KafkaDemoApplication.class, args);
KafkaProducer kafkaProducer=context.getBean(KafkaProducer.class);
for(int i=0;i<3;i++){
kafkaProducer.send();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
MyPartition
public class MyPartition implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
//这里可以根据 key 来返回分区,实现某类消息统一进入哪个分区,key发送消息时可以指定
// if (key == X) {...}
System.out.println("enter");
// 根据topic获取全部的分区
List<PartitionInfo> list=cluster.partitionsForTopic(topic);
int leng=list.size();
// 为空随机分配消息到分区
if(key==null){
Random random=new Random();
return random.nextInt(leng);
}
// 不为空取模分配消息
return Math.abs(key.hashCode())%leng;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
public static void main(String[] args) {
System.out.println(Math.abs("my-gid1".hashCode())%50);
}
}
发送端代码添加自定义分区
public MyKafkaProducer(String topic) {
Properties properties=new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.10.150:9092,192.168.10.151:9092,192.168.10.152:9092");
properties.put(ProducerConfig.CLIENT_ID_CONFIG,"my-producer");
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.kafka.MyPartition");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
//连接的字符串
//通过工厂
//new
producer=new KafkaProducer<Integer, String>(properties);
this.topic = topic;
}