Spring Boot集成Kafka
前提介紹
由于公司使用了微服務架構,很多業務拆成了很多小模塊。
有個場景是這樣的A服務主要負責寫入或者修改數據庫中的數據,B服務主要負責讀取,B服務使用緩存技術,當A發生了修改后,需要通知B來清除緩存。
中間兩個服務之間通知使用了Kafka,這個是本篇文章主要介紹的,關于 緩存技術 我也簡單介紹過。
Kafka
簡介
Kafka is a distributed,partitioned,replicated commit logservice。它提供了類似于JMS的特性,但是在實現上完全不同,此外它并不是JMS規范的實現。
kafka對消息保存時根據Topic進行歸類,發送消息者成為Producer,消息接受者成為Consumer,此外kafka集群有多個kafka實例組成,每個實例成為broker。
無論是kafka集群,還是producer和consumer都依賴于zookeeper來保證系統可用性集群保存一些meta信息。
Topics & logs
一個Topic可以認為是一類消息,每個topic將被分成多個partition(區),每個partition在存儲層面是append log文件。任何發布到此partition的消息都會被直接追加到log文件的尾部,每條消息在文件中的位置稱為offset(偏移量),offset為一個long型數字,它是唯一標記一條消息。它唯一的標記一條消息。kafka并沒有提供其他額外的索引機制來存儲offset,因為在kafka中幾乎不允許對消息進行“隨機讀寫”。
kafka和JMS(Java Message Service)實現(activeMQ)不同的是:即使消息被消費,消息仍然不會被立即刪除.日志文件將會根據broker中的配置要求,保留一定的時間之后刪除;比如log文件保留2天,那么兩天后,文件會被清除,無論其中的消息是否被消費.kafka通過這種簡單的手段,來釋放磁盤空間,以及減少消息消費之后對文件內容改動的磁盤IO開支.
對于consumer而言,它需要保存消費消息的offset,對于offset的保存和使用,有consumer來控制;當consumer正常消費消息時,offset將會"線性"的向前驅動,即消息將依次順序被消費.事實上consumer可以使用任意順序消費消息,它只需要將offset重置為任意值..(offset將會保存在zookeeper中,參見下文)
kafka集群幾乎不需要維護任何consumer和producer狀態信息,這些信息有zookeeper保存;因此producer和consumer的實現非常輕量級,它們可以隨意離開,而不會對集群造成額外的影響.
partitions的目的有多個.最根本原因是kafka基于文件存儲.通過分區,可以將日志內容分散到多個上,來避免文件尺寸達到單機磁盤的上限,每個partiton都會被當前server(kafka實例)保存;可以將一個topic切分多任意多個partitions,來消息保存/消費的效率.此外越多的partitions意味著可以容納更多的consumer,有效提升并發消費的能力.(具體原理參見下文).
Distribution
一個Topic的多個partitions,被分布在kafka集群中的多個server上;每個server(kafka實例)負責partitions中消息的讀寫操作;此外kafka還可以配置partitions需要備份的個數(replicas),每個partition將會被備份到多臺機器上,以提高可用性.
基于replicated方案,那么就意味著需要對多個備份進行調度;每個partition都有一個為"leader";leader負責所有的讀寫操作,如果leader失效,那么將會有其他follower來接管(成為新的leader);follower只是單調的和leader跟進,同步消息即可..由此可見作為leader的server承載了全部的請求壓力,因此從集群的整體考慮,有多少個partitions就意味著有多少個"leader",kafka會將"leader"均衡的分散在每個實例上,來確保整體的性能穩定.
Producers
Producer將消息發布到指定的Topic中,同時Producer也能決定將此消息歸屬于哪個partition;比如基于"round-robin"方式或者通過其他的一些算法等.
Consumers
本質上kafka只支持Topic.每個consumer屬于一個consumer group;反過來說,每個group中可以有多個consumer.發送到Topic的消息,只會被訂閱此Topic的每個group中的一個consumer消費.
如果所有的consumer都具有相同的group,這種情況和queue模式很像;消息將會在consumers之間負載均衡.
如果所有的consumer都具有不同的group,那這就是"發布-訂閱";消息將會廣播給所有的消費者.
在kafka中,一個partition中的消息只會被group中的一個consumer消費;每個group中consumer消息消費互相獨立;我們可以認為一個group是一個"訂閱"者,一個Topic中的每個partions,只會被一個"訂閱者"中的一個consumer消費,不過一個consumer可以消費多個partitions中的消息.kafka只能保證一個partition中的消息被某個consumer消費時,消息是順序的.事實上,從Topic角度來說,消息仍不是有序的.
kafka的原理決定,對于一個topic,同一個group中不能有多于partitions個數的consumer同時消費,否則將意味著某些consumer將無法得到消息.
Guarantees
發送到partitions中的消息將會按照它接收的順序追加到日志中
對于消費者而言,它們消費消息的順序和日志中消息順序一致.
如果Topic的"replicationfactor"為N,那么允許N-1個kafka實例失效.
Kafka安裝與使用
安裝
我使用的是Mac,下面介紹如何使用安裝。
brew update
brew install kafka
結果
To have launchd start kafka now and restart at login:
brew services start kafka
Or, if you don't want/need a background service you can just run:
zookeeper-server-start /usr/local/etc/kafka/zookeeper.properties & kafka-server-start /usr/local/etc/kafka/server.properties
==> Summary
?? /usr/local/Cellar/kafka/0.11.0.1: 149 files, 35.5MB
結果顯示,需要有2個配置文件
/usr/local/etc/kafka/server.properties
/usr/local/etc/kafka/zookeeper.properties
服務啟動
這里為了簡單,直接使用brew services start kafka
和brew services start zookeeper
來啟動服務。
Topic
首先找到kafka安裝目錄,可以直接使用brew info kafka
,可以看出安裝目錄為/usr/local/Cellar/kafka/0.11.0.1
,然后cd到這個目錄下面。
brew info kafka
kafka: stable 0.11.0.1 (bottled)
Publish-subscribe messaging rethought as a distributed commit log
https://kafka.apache.org/
/usr/local/Cellar/kafka/0.11.0.1 (156 files, 36.0MB) *
Poured from bottle on 2017-11-26 at 14:09:18
From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/kafka.rb
==> Dependencies
Required: zookeeper ?
==> Requirements
Required: java = 1.8 ?
==> Caveats
To have launchd start kafka now and restart at login:
brew services start kafka
Or, if you don't want/need a background service you can just run:
zookeeper-server-start /usr/local/etc/kafka/zookeeper.properties & kafka-server-start /usr/local/etc/kafka/server.properties
創建一個abc123
的topic
/bin/kafka-topics --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic abc123
查看創建的topic
./bin/kafka-topics --list --zookeeper localhost:2181
消息發送與消費
Kafka提供了一個命令行客戶端,它將從文件或標準輸入接收輸入,并將其作為消息發送到Kafka集群。默認情況下,每行都將作為單獨的消息發送。
運行生產者,然后在控制臺中鍵入一些消息發送到服務器。
./bin/kafka-console-producer --broker-list localhost:9092 --topic abc123
Kafka還有一個命令行消費者,將消息轉儲到標準輸出。
./bin/kafka-console-consumer --bootstrap-server localhost:9092 --topic abc123 --from-beginning
如圖,上面的是生產者,下面的是消費者,依次發送aaa,bbb,....ggg,消費者依次會收到對應的消息。
Spring Boot集成
開始
直接使用Idea創建一個Spring Boot項目即可,同時添加Lombok
和Kafka
庫。
也可以接添加依賴庫。
Gralde 依賴
dependencies {
compile('org.springframework.boot:spring-boot-starter')
compile('org.springframework.kafka:spring-kafka')
compile('org.projectlombok:lombok')
}
Maven 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>
配置
配置application.properties
文件中kafka屬性。
# kafka
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=myGroup
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
代碼
創建一個消息結構體
@Data
public class Message {
private Long id;
private String msg;
private Date sendTime;
}
一個消息發送者
@Component
public class KafkaSender {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
private Gson gson = new GsonBuilder().create();
public void send() {
Message message = new Message();
message.setId(System.currentTimeMillis());
message.setMsg(UUID.randomUUID().toString());
message.setSendTime(new Date());
kafkaTemplate.send("abc123", gson.toJson(message));
}
}
一個消息消費者
@Component
@Slf4j
public class KafkaReceiver {
@KafkaListener(topics = {"abc123"})
public void listen(ConsumerRecord<?, ?> record) {
Optional<?> kafkaMessage = Optional.ofNullable(record.value());
if (kafkaMessage.isPresent()) {
Object message = kafkaMessage.get();
log.info("record =" + record);
log.info("message =" + message);
}
}
}
在主程序中調用發送方法,模擬生產者
@SpringBootApplication
public class SpringKafkaDemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringKafkaDemoApplication.class, args);
KafkaSender sender = context.getBean(KafkaSender.class);
for (int i = 0; i < 3; i++) {
sender.send();
try {
Thread.sleep(3_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
運行輸出如下
record =ConsumerRecord(topic = abc123, partition = 0, offset = 17, CreateTime = 1511678827095, checksum = 2229762760, serialized key size = -1, serialized value size = 102, key = null, value = {"id":1511678826816,"msg":"2ff150e4-d7f9-4b4d-9604-b8d13a1d4538","sendTime":"Nov 26, 2017 2:47:06 PM"})
message ={"id":1511678826816,"msg":"2ff150e4-d7f9-4b4d-9604-b8d13a1d4538","sendTime":"Nov 26, 2017 2:47:06 PM"}
record =ConsumerRecord(topic = abc123, partition = 0, offset = 18, CreateTime = 1511678830109, checksum = 1589760372, serialized key size = -1, serialized value size = 102, key = null, value = {"id":1511678830108,"msg":"e1b93a1c-d88e-4b9b-8e1d-98e05edeb7c6","sendTime":"Nov 26, 2017 2:47:10 PM"})
message ={"id":1511678830108,"msg":"e1b93a1c-d88e-4b9b-8e1d-98e05edeb7c6","sendTime":"Nov 26, 2017 2:47:10 PM"}
record =ConsumerRecord(topic = abc123, partition = 0, offset = 19, CreateTime = 1511678833110, checksum = 4176540846, serialized key size = -1, serialized value size = 102, key = null, value = {"id":1511678833109,"msg":"f77fbb85-0eb9-402c-8265-c37987011551","sendTime":"Nov 26, 2017 2:47:13 PM"})
message ={"id":1511678833109,"msg":"f77fbb85-0eb9-402c-8265-c37987011551","sendTime":"Nov 26, 2017 2:47:13 PM"}
同時原先的命令行消費者也會受到程序發送的消息。
總結
本人是剛剛入門的后端工程師,原先做過幾年Java,說的比較簡單,如有出錯的地方,歡迎指正。