之前也學習過訊息佇列,但一直沒有使用的場景,今天專案中遇到了 kafka 那便有了應用場景
1. Kafka
Kafka 是一個分散式、支援分割槽,多副本的基於 zookeeper 的訊息佇列。使用訊息佇列,是應用 A 將要處理的資訊傳送到訊息佇列然後繼續下面的任務,需要該資訊的應用 B 從訊息佇列裡面獲取資訊再做處理,這樣做像是多此一舉,應用 A 直接發資訊給應用 B 不就可以了嗎?存在即合理,使用訊息佇列其作用如下:
- 非同步處理:使用者註冊後傳送郵件、簡訊、驗證碼等可以非同步處理,使註冊這個過程寫入資料庫後就可立即返回
- 流量消峰:秒殺活動超過閾值的請求丟棄轉向錯誤頁面,然後根據訊息佇列的訊息做業務處理
- 日誌處理:可以將error的日誌單獨給訊息佇列進行持久化處理
- 應用解耦:購物的下單操作,訂單系統與庫存系統中間加訊息佇列,使二者解耦,若後者故障也不會導致訊息丟失
之前 筆者也寫過 RabbitMQ 的筆記,傳送門
2. 生產消費模型
結合 kafka 的下面這些名詞來解釋其模型會更加容易理解
名稱 | 解釋 |
---|---|
Broker | kafka 的例項,部署多臺 kafka 就是有多個 broker |
Topic | 訊息訂閱的話題,是這些訊息的分類,類似於訊息訂閱的頻道 |
Producer | 生產者,負責往 kafka 傳送訊息 |
Consumer | 消費者,從 kafka 讀取訊息來進行消費 |
3. 安裝部署
kafka 和依賴的 zookeeper 是 java 編寫的工具,其需要 jdk8 及其以上。筆者這裡使用 Docker 安裝,偷懶了貪圖方便快捷
# 使用 wurstmeister 製作的映象
docker pull wurstmeister/zookeeper
docker pull wurstmeister/kafka
# 啟動 zookeeper
docker run -d --name zookeeper -p 2181:2181 wurstmeister/zookeeper
# 單機啟動 kafka
docker run -d --name kafka -p 9092:9092 \
-e KAFKA_BROKER_ID=0 \
-e KAFKA_ZOOKEEPER_CONNECT=xxx.xxx.xxx.xxx:2181 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://xxx.xxx.xxx.xxx:9092 \
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 wurstmeister/kafka
4. Quickstart
kafka 官網也有很好的介紹,quickstart
# 進入kafka容器
docker exec -it kafka /bin/sh
# 進入 bin 目錄
cd /opt/kafka_2.13-2.8.1/bin
# partitions 分割槽
# replication 副本因子
# 建立一個主題(引數不懂可直接填寫,後面會講解說明)
./kafka-topics.sh --create --partitions 1 --replication-factor 1 --topic quickstart-events --bootstrap-server localhost:9092
# 檢視
./kafka-topics.sh --describe --topic quickstart-events --bootstrap-server localhost:9092
# 寫入 topic(回車表示一條訊息,ctrl + c 結束輸入)
# 訊息預設儲存 7 天,下一步的消費可以驗證
./kafka-console-producer.sh --topic quickstart-events --bootstrap-server localhost:9092
This is my first event
This is my second event
# 讀取 topic(執行多次可以讀取訊息,因為預設儲存 7 天)
./kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092
./kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092
5. SpringBoot 整合
SpringBoot 整合了 Kafka,新增依賴後可使用內建的 KafkaTemplate 模板方法來操作 kafka 訊息佇列
5.1 新增依賴
<!-- sprinboot版本管理中有kafka可不寫版本號 -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
5.2 配置檔案
server:
port: 8080
spring:
# 訊息佇列
kafka:
producer:
# broker地址,重試次數,確認接收個數,訊息的編解碼方式
bootstrap-servers: 101.200.197.22:9092
retries: 3
acks: 1
key-serializer: org.springframework.kafka.support.serializer.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.StringSerializer
consumer:
# broker地址,自動提交,分割槽offset設定
bootstrap-servers: 101.200.197.22:9092
enable-auto-commit: false
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
5.3 生產者
@RestController
@RequestMapping("/kafka")
public class Producer {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
@GetMapping("/producer1")
public String sendMessage1(@RequestParam(value = "message", defaultValue = "123") String message) throws ExecutionException, InterruptedException {
ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send("topic1", message);
SendResult<String, Object> sendResult = future.get();
return sendResult.toString();
}
@GetMapping("/producer2")
public String sendMessage2(@RequestParam(value = "message", defaultValue = "123") String message) throws ExecutionException, InterruptedException {
ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send("topic1", message);
future.addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
@Override
public void onFailure(Throwable ex) {
System.out.println("faile");
}
@Override
public void onSuccess(SendResult<String, Object> result) {
System.out.println("success");
}
});
return "";
}
}
5.4 消費者
@Component
public class Consumer {
@KafkaListener(topics = {"topic1"})
public void onMessage(ConsumerRecord<?, ?> record) {
System.out.println(record.value());
}
}
6. 儲存目錄結構
kafka
|____kafka-logs
|____topic1
| |____00000000000000000000.log(儲存接收的訊息)
| |____consumer_offsets-01(消費者偏移量)
| |____consumer_offsets-02
|____topic2
|____00000000000000000000.log
|____consumer_offsets-01
|____consumer_offsets-02
每臺 broker 例項接收到訊息後將之儲存到 00000.log 裡面,儲存的方式是先入先出。訊息被消費後不會被刪除,相反可以設定 topic 的訊息保留時間,重要的是 Kafka 的效能在資料大小方面實際上是恆定的,因此長時間儲存資料是完全沒問題的
消費者會將自己消費偏移量 offset 提交給 topic 在 _consumer_offsets 裡面儲存,然後通過偏移量來確定訊息的位置,預設從上次消費的位置開始,新增引數 --frombeginning 則從頭開始消費,可獲取之前所有儲存的訊息。kafka 也會定期清除內部的訊息,直到儲存最新的一條(檔案儲存的訊息預設儲存 7 天)
7. 消費組
這個在筆者配置消費者的時候發現的問題,啟動時報錯說沒有指定消費組
- 每條分割槽訊息只能被同組的一個消費者消費,consumer1 和 consumer2 同組,所以只有其中一個能消費同條訊息
- 每條分割槽訊息能被不同組的單個消費者消費,consumer2 和 consumer4 不同組,所以都能消費同條訊息
- 以上二個規則同時成立
- 其作用是可以保證消費順序,同個分割槽裡的訊息會被同個消費者順序消費
8. 分割槽和副本
topic 訊息儲存的檔案 0000.log 可以進行物理切分,這就是分割槽的概念,類似於資料庫的分庫分表。這樣做的好處在於單個儲存的檔案不會太大從而影響效能,最重要的是分割槽後不是單個檔案序列執行了,而是多區多檔案可並行執行提高了併發能力
分割槽:消費者會消費同一 topic 的不同分割槽,所以會儲存不同分割槽的偏移量,其格式為:GroupId + topic + 分割槽號
副本:副本是對分割槽的備份,叢集中不同的分割槽在不同的 broker 上,但副本會對該分割槽備份到指定數量的 broker 上,這些副本有 leader 和 follower 的區別,leader負責讀寫,掛了再重新選舉,副本為了保持資料一致性
9. 常見問題
9.1 生產者同步和非同步訊息
生產者傳送訊息給 broker,之後 broker 會響應 ack 給生產者,生產者等待接收 ack 訊號 3 秒,超時則重試 3 次
生產者 ack 確認配置:
- ack = 0:不需要同步訊息
- ack = 1:則 leader 收到訊息,並儲存到本地 log 之後才響應 ack 資訊
- ack 預設配置為 2
9.2 消費者自動提交和手動提交
- 自動提交:消費者 pull 訊息之後馬上將自身的偏移量提交到 broker 中,這個過程是自動的
- 手動提交:消費者 pull 訊息時或之後,在程式碼裡將偏移量提交到 broker
- 二者區別:防止消費者 pull 訊息之後掛掉,在訊息還沒消費但又提交了偏移量
9.3 訊息丟失和重複消費
- 訊息丟失
- 生產者:配置 ack ,以及配置副本和分割槽數值一致
- 消費者:設定手動提交
- 重複消費
- 設定唯一主鍵,Mysql 主鍵唯一則插入失敗
- 分散式鎖
9.4 順序消費方案
- 生產者:關閉重試,使用同步傳送,成功了再發下一條
- 消費者:訊息傳送到一個分割槽中,只有一個消費組的消費者能接收訊息