解决问题
生产方生产数据与消费方消费数据异步的问题。 如果生产方和消费方是多对多的关系,程序会变得十分复杂,主要会造成三个问题。
- 各个程序之间会进行重复的工作,造成信息的浪费。
- 信息过多无法及时同步时,会造成信息的丢失。
- 各个程序之间耦合度过高,牵一发而动全身。
所以用kafka来做中间转接,就变成了生产者发布信息,消费者订阅信息。
核心概念
- Topic: 以Topic为标识,生产者将不同主题的信息发布到kafka,消费者通过订阅不同的主题来消费自己想要的信息。
- Partition: 每一个Topic下有Partition,可以实现不同的Partition分布到不同的server上。Kafka 只保证分区内的记录是有序的,而不保证主题中不同分区的顺序。
- Partition Key: 传递给一个 Hash 函数,由计算结果决定写入哪个 Partition。例如使用 User ID 作为 Partition Key,那么此 ID 的消息就都在同一个 Partition,这样可以保证此类消息的有序性。
- offset: 在每一个消费者中唯一保存的元数据是offset(偏移量)即消费在log中的位置.偏移量由消费者所控制:通常在读取记录后,消费者会以线性的方式增加偏移量,但是实际上,由于这个位置由消费者控制,所以消费者可以采用任何顺序来消费记录。
- Consumer Group: 消费者使用一个 消费组 名称来进行标识,发布到topic中的每条记录被分配给订阅消费组中的一个消费者实例.消费者实例可以分布在多个进程中或者多个机器上。
kafka-go实践
//生产者
writer := &kafka.Writer{
Addr: kafka.TCP("localhost:9092"),
Topic: topic,
Balancer: &kafka.Hash{},
WriteTimeout: 1 * time.Second,
RequiredAcks: kafka.RequireNone,
AllowAutoTopicCreation: true,
}
Balancer: &kafka.Hash{}:负载均衡器将根据消息的 key 使用哈希函数将消息分发到不同的分区。RequiredAcks: kafka.RequireNone: 这个设置表示写入消息后,不需要等待任何确认(acknowledgment)AllowAutoTopicCreation: true:这个设置允许自动创建主题,如果指定的主题不存在,程序将会自动创建它。如果设置为false,则不会自动创建主题(通常是运维创建,此处是为了方便测试)。
for i := 0; i < 3; i++ {
if err := writer.WriteMessages(
ctx,
kafka.Message{Key: []byte("1"), Value: []byte("big")},
kafka.Message{Key: []byte("1"), Value: []byte("big")},
); err != nil {
if err == kafka.LeaderNotAvailable {
time.Sleep(500 * time.Millisecond)
continue
} else {
fmt.Printf("批量写入kafka失败:%v\n", err)
}
} else {
break
}
}
if err == kafka.LeaderNotAvailable这可能意味着主题还未完全创建,因此无法写入,等一会就好了。- 循环3次是给重试的机会,成功就break。
reader = kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{"localhost:9092"},
Topic: topic,
CommitInterval: 1 * time.Second,
GroupID: "rec_team",
StartOffset: kafka.FirstOffset, //从最开始消费(创建时生效)
})
CommitInterval: 1 * time.Second:指定消息的提交间隔,即在多长时间内自动提交消费的消息。在这里,设置为每秒提交一次。GroupID: "rec_team":指定消费者组的标识符。这是为了实现多个消费者协同消费一个主题时的协调。StartOffset: kafka.FirstOffset:设置从最早的消息开始消费。如果设置为kafka.LastOffset,则从最新的消息开始消费。
// 监听2和15,收到时关闭reader,避免reader直接kill无法正常关闭
func listenSignal() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
sig := <-c
fmt.Printf("接收到信号 %s", sig.String())
if reader != nil {
reader.Close()
}
os.Exit(0)
}
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM):通过调用signal.Notify函数,将SIGINT和SIGTERM两个信号注册到通道c,以便在接收到这些信号时进行处理。
总结
kafka作为消息队列可以更高效地完成消息的订阅和发布功能,不只kafka,还有RocketMQ、RabbitMQ等着学习,要根据不同的场景来选择不同的中间件。