上午不想上班,把周六整的kafka贴出来。看网上搜不出来几篇好用的,供大家参考参考。<( ̄▽ ̄)> 哇哈哈…
spring和kafka整合 并没有什么starter项目。 使用的是spring-kafka。如果是在spring-cloud里使用kafka的话,可以使用spring-cloud-stream。这里我用的是springboot+dubbo,而且项目都是单个的,就直接使用spring-kafka了。由于spirng-kafka最新版本需要jdk1.8,我的项目还是1.7的,所以我用老一点版本的。
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>1.3.0.RELEASE</version>
</dependency>
概念。
首先要配置至少一个producerFactory,这个工厂类指定我们要连哪个kafka,也就是设置producer相关的属性。kafkaTemplate。模版类,是用来向kafka发送消息的,它没有收消息的功能。在producer端,只需要这个template就完了,springboot的项目写了配置文件,就默认有个producerFactory, template默认就用这个工厂类。
在消费者端,就复杂点。首先,我们需要至少一个ConsumerFactory工厂类,这个工厂类配置consumer相关的信息,比如你连哪个kafka,你的group_id是多少。然后,再要配置一个ListenerContainerFactory,这个工厂类配置线程池,由这个线程池来负责调用listener来消费消息。listener就是我们实际消费消息的业务类。
生产者端。
KafkaTemplate,可以指定默认的topic,这样发的时候就不用再指定topic了。template的发送是异步的,会返回一个future。给future注册回调,就能处理成功还是失败。
ListenableFuture<SendResult<Integer, String>> future = template.send("foo");
future.addCallback(new ListenableFutureCallback<SendResult<Integer, String>>() {
@Override
public void onSuccess(SendResult<Integer, String> result) {
...
}
@Override
public void onFailure(Throwable ex) {
...
}
});
也可以直接给template注册一个全局的回调ProducerListener来处理。默认template就注册了LoggingProducerListener,这个listener成功的时候什么都不做,失败了打印下日志。Listener不用写,继承ProducerListenerAdapter就可以了。如果需要当前线程阻塞等待future完成,先执行template.flush,然后再future.get()。
消费者端。
配置好consumerFactory之后,就定义好了怎么连到kafka。然后需要配置containerFactory来生成container。container有两种,一种是 KafkaMessageListenerContainer
也就是单线程的。另一种是 ConcurrentMessageListenerContainer
实际上是维护了多个单线程的container,通过设置concurrency来控制并发度。
对于并发度。假设我们一个topic有6个partion,而我们有concurrency设置为3,也就是有3个KafkaMessageListenerContainer,那么每个container都会读两个partition。 如果我们的concurrency设置为7呢? 那么实际上有一个是没用的,所以实际上spring只会new 6个出来。
consumer会一次从kafka拉取一定数量的ConsumerRecord,派发给container处理,这里和kafka api里的poll(long timeout)是一致的。如果consumer设置了enable.auto.commit为true,那么拉取过来的消息会自动commit。否则,需要设置container来决定ack的方式。
RECORD - 由listener方法处理完一行记录就提交
BATCH - 由listener方法处理完所有行记录就提交
TIME - 隔段时间提交
COUNT - 一定次数提交
COUNT_TIME - 一定时间或者一定次数提交
MANUAL - 当listener调用了acknowledge()方法,然后按照batch的方式提交
MANUAL_IMMEDIATE - 每次listener调用了ack方法就提交一次
batch当然性能更好,但是如果你的业务是可能报错或者说不稳定的,那么也好不到哪去。
我们通过设置container的syncCommits 来决定是同步提交偏移,还是异步。
关于从kafka读数据的初始偏移量。如果我是一个新的消费者,可以设置consumer的auto-offset-reset来决定。取值可以是earliest或者是latest。也就是说从最老的消息开始,还是从最新的消息开始。 如果我是个老消费者,只是有段时间下线了,再来,会接着上次的读,和HighLevelApi的行为一致。当然也可以在构造consumer的时候,设置TopicPartitionInitialOffset属性来人为改变偏移量。
序列化
kafka只能发字符串或者字节流。不过spring提供了一个基于jackson的json方案,直接可以把任何javabean先变成json,在变成字节写出去。不过反过来的时候,构造JsonDeserializer 的时候,必须传入java类的class,所以这里不能使用yml或者properties文件来配置。
<!-- json解析器 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
实例
生产者:
spring:
jackson:
serialization:
fail-on-empty-beans: false
kafka:
producer:
# 0不需要确认,1leader确认, all所有人确认收到
acks: 1
# kafka地址,逗号分隔
bootstrap-servers: 175.xxx.xxx.xx:9092
# log里的客户端id
client-id: ${spring.application.name}
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
retries: 0
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MessageServiceApplication.class)
public class TestKafka {
@Autowired
private KafkaTemplate<String, AppUser> kafka;
@Test
public void contextLoads() {
AppUser ap = new AppUser();
ap.setUserId(1l);
ap.setNickName("胡淘");
ap.setAvatar("http://localhost/123");
ListenableFuture<SendResult<String, AppUser>> future = kafka.send("test", "1", ap);
future.addCallback(new ListenableFutureCallback<SendResult<String, AppUser>>() {
@Override
public void onSuccess(SendResult<String, AppUser> result) {
System.out.println(result.getProducerRecord().key()+"发送成功");
}
@Override
public void onFailure(Throwable ex) {
System.out.println(ex.getMessage());
}
});
}
}
这里,我们的producer和template都是提供了配置文件,springboot自动就提供了bean。不需要额外配置。除非我们要连多个kafka。
消费者:
由于deSerializer的配置需要提供一个java类,所以,不能通过springboot的配置文件,来使用默认的consumer。所以我们手动写歌配置类,使用代码自己配置。
spring:
application:
name: xxx
kafka:
consumer:
bootstrap-servers: 175.xx.xx.xx:9092
group-id: ${spring.application.name}
#key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
#value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
max-poll-records: 20
listener:
concurrency: 1
@Configuration
@EnableKafka
public class KafkaConfig {
@Value("${spring.kafka.consumer.bootstrap-servers}")
private String bootstrapServers;
@Value("${spring.kafka.consumer.group-id}")
private String groupId;
@Value("${spring.kafka.consumer.max-poll-records}")
private Integer maxPollRecords;
@Value("${spring.kafka.listener.concurrency}")
private Integer concurrency;
@Bean(name = "userConsumerFactory")
public ConsumerFactory<String, AppUser> userConsumerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(),
new JsonDeserializer<>(AppUser.class));
}
@Bean(name = "kafkaListenerContainerFactory")
public ConcurrentKafkaListenerContainerFactory<String, AppUser> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, AppUser> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(userConsumerFactory());
factory.setConcurrency(concurrency);
factory.getContainerProperties().setAckMode(AckMode.MANUAL_IMMEDIATE);
return factory;
}
}
enableKafka,是打开扫描,扫Listener。所以我们的Listener所在的包要给spring托管。
这里有个坑KafkaAnnotationDrivenConfiguration,会自动装配consumer和Container,但是他默认的哪个containerFactory竟然是指定了名字的,所以,我自己的类,不能随便起名字。如果我只有一个containerFactory,那么我必须叫上面这个名字,否则,springboot还会再初始化一个,但是由于我们自己提供了consumerFactory,导致他的consumerFactory没初始化,这里就会报错。
@Component
public class AppUserListener {
@KafkaListener(id = "messageListener", topics = "test", containerFactory = "kafkaListenerContainerFactory")
public void listen(AppUser user, Acknowledgment ack) {
System.out.println("收到kafka消息");
System.out.println(JsonUtils.toJson(user));
ack.acknowledge();
}
}