一、快速瞭解Kafka
在把Kafka集成到spring之前,我們首先要了解Kafka是什麼?由什麼東西組成?主要的使用場景是哪些?
Apache Kafka是一個開源消息系統,由Scala寫成。是由Apache軟件基金會開發的一個開源消息系統項目。
Kafka是一個分佈式消息隊列。Kafka對消息保存時根據Topic進行歸類,發送消息者稱爲Producer,消息接受者稱爲Consumer,此外kafka集羣有多個kafka實例組成,每個實例(server)稱爲broker。
無論是kafka集羣,還是consumer都依賴於zookeeper集羣保存一些meta信息,來保證系統可用性。
簡單架構理解圖
詳細架構圖
-
1)Producer :消息生產者,就是向kafka broker發消息的客戶端;
-
2)Consumer :消息消費者,向kafka broker取消息的客戶端;
-
3)Topic :可以理解爲一個隊列;
-
4) Consumer Group (CG):這是kafka用來實現一個topic消息的廣播(發給所有的consumer)和單播(發給任意一個consumer)的手段。一個topic可以有多個CG。topic的消息會複製(不是真的複製,是概念上的)到所有的CG,但每個partion只會把消息發給該CG中的一個consumer。如果需要實現廣播,只要每個consumer有一個獨立的CG就可以了。要實現單播只要所有的consumer在同一個CG。用CG還可以將consumer進行自由的分組而不需要多次發送消息到不同的topic;
-
5)Broker :一臺kafka服務器就是一個broker。一個集羣由多個broker組成。一個broker可以容納多個topic;
-
6)Partition:爲了實現擴展性,一個非常大的topic可以分佈到多個broker(即服務器)上,一個topic可以分爲多個partition,每個partition是一個有序的隊列。partition中的每條消息都會被分配一個有序的id(offset)。kafka只保證按一個partition中的順序將消息發給consumer,不保證一個topic的整體(多個partition間)的順序;
-
7)Offset:kafka的存儲文件都是按照offset.kafka來命名,用offset做名字的好處是方便查找。例如你想找位於2049的位置,只要找到2048.kafka的文件即可。當然the first offset就是00000000000.kafka。
二、環境準備
這邊的話,我們簡單的在windows搭建一個環境即可
主要的就是以下三個環境:
- jdk
- zookeeper
- kafka
具體步驟這裏就不詳細介紹了,網上有很多案例,照着做一遍即可。
本地測試的話,搭建個單機的即可。
三、Spring Kafka集成
1、添加架包依賴
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.1.0</version>
</dependency>
注意:這裏有一個很大的坑,因爲版本的問題,spring-kafka和kafka-clients的版本一定要按照下圖對應。
2、簡單配置
推薦使用spring-boot的項目,配置既簡單又方便
直接在application.yml配置文件加入以下內容即可。
spring:
kafka:
# 消費者
consumer:
group-id: foo
auto-offset-reset: earliest
bootstrap-servers: localhost:9092
# 生產者
producer:
bootstrap-servers: localhost:9092
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
如果你想了解更多的配置,可以參考文檔 Apache Kafka Documentation
注意:運行項目之前一定要先開啓zookeeper和kafka服務
3、簡單的例子
監聽器主要是使用@KafkaListenter註解即可,可以監聽多個topic也可以監聽單個。
@Component
public class SimpleListener {
@KafkaListener(topics = {"topic1", "topic2"})
public void listen1(String data) {
System.out.println(data);
}
}
消息發送主要是使用KafkaTemplate,它具有多個方法可以發送消息,這裏我們用簡單的。
@RestController
@AllArgsConstructor
public class SimpleController {
private final KafkaTemplate<Object, Object> kafkaTemplate;
@GetMapping("/send/{messge}")
public String send(@PathVariable String messge) {
kafkaTemplate.send("topic1", "topci1:" + messge);
kafkaTemplate.send("topic2", "topci2:" + messge);
return messge;
}
}
我們用postman測試一下,看看控制檯有沒有輸出,有沒有接受到消息。
4、發送實體類封裝的消息
4.1實體類
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Bar {
private Integer id;
private Integer age;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Foo {
private Integer id;
private String name;
}
4.2 配置文件
@Configuration
public class KafkaConfig {
@Bean
public ConcurrentKafkaListenerContainerFactory<?, ?> kafkaListenerContainerFactory(
ConcurrentKafkaListenerContainerFactoryConfigurer configurer,
ConsumerFactory<Object, Object> kafkaConsumerFactory,
KafkaTemplate<Object, Object> template) {
ConcurrentKafkaListenerContainerFactory<Object, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
configurer.configure(factory, kafkaConsumerFactory);
factory.setErrorHandler(new SeekToCurrentErrorHandler(
new DeadLetterPublishingRecoverer(template), 3));
return factory;
}
// 當傳輸的是個實體類時,進行消息格式轉換
@Bean
public RecordMessageConverter converter() {
StringJsonMessageConverter converter = new StringJsonMessageConverter();
DefaultJackson2JavaTypeMapper typeMapper = new DefaultJackson2JavaTypeMapper();
typeMapper.setTypePrecedence(TypePrecedence.TYPE_ID);
typeMapper.addTrustedPackages("com.lzx.kafka.example2");
Map<String, Class<?>> mappings = new HashMap<>();
mappings.put("foo", Foo.class);
mappings.put("bar", Bar.class);
typeMapper.setIdClassMapping(mappings);
converter.setTypeMapper(typeMapper);
return converter;
}
@Bean
public NewTopic foos() {
return new NewTopic("foo", 1, (short) 1);
}
@Bean
public NewTopic bars() {
return new NewTopic("bar", 1, (short) 1);
}
}
4.3 application.yml配置文件
spring:
kafka:
consumer:
group-id: foo
auto-offset-reset: earliest
bootstrap-servers: localhost:9092
producer:
bootstrap-servers: localhost:9092
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
properties:
spring.json.type.mapping: foo:com.lzx.kafka.entity.Foo,bar:com.lzx.kafka.entity.Bar
4.4 代碼
監聽器
@Component
@KafkaListener(id = "handler", topics = {"foo", "bar"})
public class ListenHandler {
@Autowired
private KafkaTemplate<Object, Object> kafkaTemplate;
@KafkaHandler
public void foo(@Payload Foo foo, @Header(KafkaHeaders.RECEIVED_MESSAGE_KEY) String key) {
System.out.println("key:" + key);
System.out.println("foo:" + foo.toString());
}
@KafkaHandler
public void foo(Bar bar) {
System.out.println("bar:" + bar.toString());
}
}
Controlller
@RestController
@AllArgsConstructor
public class Example2Controller {
private final KafkaTemplate kafkaTemplate;
@PostMapping("/foo")
public void send(Foo foo){
kafkaTemplate.send("foo", "modelOne", foo);
}
@PostMapping("/bar")
public void send(Bar bar){
kafkaTemplate.send("bar", bar);
}
}
4.5結果
5、消息發送的同步方法和異步方法
方法
@Service
@AllArgsConstructor
public class SendService {
private final KafkaTemplate<Object, Object> template;
// 異步
public void sendAnsyc(final Bar bar) {
// ProducerRecord<Object, Object> producerRecord = new ProducerRecord<>("ansyc", bar);
ListenableFuture<SendResult<Object, Object>> future = template.send("ansyc",bar);
future.addCallback(new ListenableFutureCallback<SendResult<Object, Object>>() {
@Override
public void onSuccess(SendResult<Object, Object> result) {
System.out.println("發送消息成功:" + result);
}
@Override
public void onFailure(Throwable ex) {
System.out.println("發送消息失敗:"+ ex.getMessage());
}
});
}
// 同步
public void sendSync(final Bar bar) {
ProducerRecord<Object, Object> producerRecord = new ProducerRecord<>("sync", bar);
try {
template.send(producerRecord).get(10, TimeUnit.SECONDS);
System.out.println("發送成功");
}
catch (ExecutionException e) {
System.out.println("發送消息失敗:"+ e.getMessage());
}
catch (TimeoutException | InterruptedException e) {
System.out.println("發送消息失敗:"+ e.getMessage());
}
}
}
監聽器
@Component
public class Example3Listenter {
@KafkaListener(topics = "ansyc")
public void listenAnsyc(Bar bar) {
System.out.println(bar);
}
@KafkaListener(topics = "sync")
public void listenSync(Bar bar) {
System.out.println(bar);
}
}
Controller
@RestController
@AllArgsConstructor
public class Example3Controller {
private final SendService sendService;
@PostMapping("/ansyc")
public void sendAnsyc(Bar bar){
sendService.sendAnsyc(bar);
}
@PostMapping("/sync")
public void sendSync(Bar bar){
sendService.sendSync(bar);
}
}
異步結果
同步結果
6、使用事務的消息發送方式
在4.3application.yml中的properties配置上方添加這樣的一句配置即可
transaction-id-prefix: tx.
代碼
@RestController
@AllArgsConstructor
public class Example1Controller {
private final KafkaTemplate<Object, Object> kafkaTemplate;
@PostMapping("/send/foo")
public void sendFoo(Foo foo) {
kafkaTemplate.executeInTransaction(kafkaTemplate -> {
kafkaTemplate.send("foo", foo);
return true;
});
}
}