走进消息队列 | 青训营

143 阅读6分钟

消息队列简介

消息队列(MQ),指保存消息的一个容器,本质是个队列。但这个队列需要支持高吞吐,高并发,高可用。

image.png

image.png

消息队列-Kafla

使用场景

  • 用在离线的情况下,比如日志信息,可以进行分析
  • Metrics数据:对程序状态的采集比如搜索服务,直播服务,订单服务,支付服务等,可以采集耗时等
  • 用户行为:点赞、评论、收藏、搜索

如何使用

创建集群 -> 新增topic -> 编写生产者逻辑-> 编写消费者逻辑

基本概念

image.png

image.png

image.png 上图第三个副本与第一个leader的差距过大,就会被踢出In-Sync Replicas中,这个ISR机制主要用于,如果leader发生了宕机,就可以在ISR中选择其他的follower作为leader提供服务,保证高可用

数据复制

下面这幅图代表着Kafka中副本的分布图。途中Broker代表每一个Kafka的节点,所有的Broker节点最终组成了一个集群。整个图表示,图中整个集群包含了4个Broker机器节点,集群有两个Topic,分别是Topic1和Topic2,Topic1有两个分片,Topic2有1个分片,每个分片都是三副本的状态。这里间有一个Broker同时也扮演了Controller的角色,Controller是整个集群的大脑,负责对副本和Broker进行分配 image.png

Kafka架构

而在集群的基础上,还有一个模块是ZooKeeper,这个模块其实是存储了集群的元数据信息,比如副本的分配信息等等,Controller计算好的方案都会放到这个地方

image.png

producer

如果发送一条消息,等到其成功后再发一条会太慢了。producer就是批量patch发送消息。但是如果消息量太大的话,带宽会不够用,所以producer会支持ZSTD等压缩方法。

broker

消息会有过期机制,所以需要切分 image.png 同时broker是顺序写以减少磁盘寻道的时间,用二分查找来读,用的也是基于sendfile的零拷贝。Consumer从Broker中读取数据,通过sendfile的方式,将磁盘读到os内核线冲区后,直接转到socket buffer进行网络发送Producer生产的数据持久化到broker,采用mmap文件映射,实现顺序的快速写入。 这些技术都可以减少时间

consumer

image.png 手动分配的缺点是,如果有一个consumer宕机了,其他的也会停,所以提供了自动分配的办法

image.png

青训营项目消息队列代码示例

引用 : github.com/HammerCloth…

需求分析

实现一个点赞功能的消息队列处理系统,使用 RabbitMQ 来处理点赞和取消点赞操作的消息。根据消息队列中的消息,它会执行相应的数据库操作,以确保数据的一致性和可靠性。这种方式可以有效地将点赞操作异步处理,提高系统的性能和可扩展性。

具体实现

  1. NewLikeRabbitMQ 函数:这个函数用于创建一个 LikeMQ 实例,初始化 RabbitMQ 连接并返回。它接受一个队列名作为参数,用于标识不同的点赞队列。

  2. Publish 函数:这个函数用于发布点赞消息到 RabbitMQ 队列。它创建一个队列(如果不存在),然后将消息发布到队列中。

  3. Consumer 函数:这个函数用于消费 RabbitMQ 队列中的消息。它创建一个队列(如果不存在),然后开始接收消息并根据队列名调用相应的消费函数。

  4. consumerLikeAddconsumerLikeDel 函数:这两个函数分别用于处理点赞和取消点赞操作的消息。它们解析消息体,执行数据库操作,并支持重试逻辑。

  5. InitLikeRabbitMQ 函数: 这个函数用于初始化点赞操作的 RabbitMQ 队列,创建两个 LikeMQ 实例,一个用于点赞操作,另一个用于取消点赞操作,并启动它们的消费者。

代码示例

package rabbitmq  
  
import (  
"TikTok/config"  
"TikTok/dao"  
"errors"  
"fmt"  
"github.com/streadway/amqp"  
"log"  
"strconv"  
"strings"  
)  
  
type LikeMQ struct {  
RabbitMQ  
channel *amqp.Channel  
queueName string  
exchange string  
key string  
}  
  
// NewLikeRabbitMQ 获取likeMQ的对应队列。  
func NewLikeRabbitMQ(queueName string) *LikeMQ {  
likeMQ := &LikeMQ{  
RabbitMQ: *Rmq,  
queueName: queueName,  
}  
cha, err := likeMQ.conn.Channel()  
likeMQ.channel = cha  
Rmq.failOnErr(err, "获取通道失败")  
return likeMQ  
}  
  
// Publish like操作的发布配置。  
func (l *LikeMQ) Publish(message string) {  
  
_, err := l.channel.QueueDeclare(  
l.queueName,  
//是否持久化  
false,  
//是否为自动删除  
false,  
//是否具有排他性  
false,  
//是否阻塞  
false,  
//额外属性  
nil,  
)  
if err != nil {  
panic(err)  
}  
  
err1 := l.channel.Publish(  
l.exchange,  
l.queueName,  
false,  
false,  
amqp.Publishing{  
ContentType: "text/plain",  
Body: []byte(message),  
})  
if err1 != nil {  
panic(err)  
}  
  
}  
  
// Consumer like关系的消费逻辑。  
func (l *LikeMQ) Consumer() {  
  
_, err := l.channel.QueueDeclare(l.queueName, false, false, false, false, nil)  
  
if err != nil {  
panic(err)  
}  
  
//2、接收消息  
messages, err1 := l.channel.Consume(  
l.queueName,  
//用来区分多个消费者  
"",  
//是否自动应答  
true,  
//是否具有排他性  
false,  
//如果设置为true,表示不能将同一个connection中发送的消息传递给这个connection中的消费者  
false,  
//消息队列是否阻塞  
false,  
nil,  
)  
if err1 != nil {  
panic(err1)  
}  
  
forever := make(chan bool)  
switch l.queueName {  
case "like_add":  
//点赞消费队列  
go l.consumerLikeAdd(messages)  
case "like_del":  
//取消赞消费队列  
go l.consumerLikeDel(messages)  
  
}  
  
log.Printf("[*] Waiting for messagees,To exit press CTRL+C")  
  
<-forever  
  
}  
  
//consumerLikeAdd 赞关系添加的消费方式。  
func (l *LikeMQ) consumerLikeAdd(messages <-chan amqp.Delivery) {  
for d := range messages {  
// 参数解析。  
params := strings.Split(fmt.Sprintf("%s", d.Body), " ")  
userId, _ := strconv.ParseInt(params[0], 10, 64)  
videoId, _ := strconv.ParseInt(params[1], 10, 64)  
//最多尝试操作数据库的次数  
for i := 0; i < config.Attempts; i++ {  
flag := false //默认无问题  
//如果查询没有数据,用来生成该条点赞信息,存储在likeData中  
var likeData dao.Like  
//先查询是否有这条数据  
likeInfo, err := dao.GetLikeInfo(userId, videoId)  
//如果有问题,说明查询数据库失败,打印错误信息err:"get likeInfo failed"  
if err != nil {  
log.Printf(err.Error())  
flag = true //出现问题  
} else {  
if likeInfo == (dao.Like{}) { //没查到这条数据,则新建这条数据;  
likeData.UserId = userId //插入userId  
likeData.VideoId = videoId //插入videoId  
likeData.Cancel = config.IsLike //插入点赞cancel=0  
//如果有问题,说明插入数据库失败,打印错误信息err:"insert data fail"  
if err := dao.InsertLike(likeData); err != nil {  
log.Printf(err.Error())  
flag = true //出现问题  
}  
} else { //查到这条数据,更新即可;  
//如果有问题,说明插入数据库失败,打印错误信息err:"update data fail"  
if err := dao.UpdateLike(userId, videoId, config.IsLike); err != nil {  
log.Printf(err.Error())  
flag = true //出现问题  
}  
}  
//一遍流程下来正常执行了,那就打断结束,不再尝试  
if flag == false {  
break  
}  
}  
}  
}  
}  
  
//consumerLikeDel 赞关系删除的消费方式。  
func (l *LikeMQ) consumerLikeDel(messages <-chan amqp.Delivery) {  
for d := range messages {  
// 参数解析。  
params := strings.Split(fmt.Sprintf("%s", d.Body), " ")  
userId, _ := strconv.ParseInt(params[0], 10, 64)  
videoId, _ := strconv.ParseInt(params[1], 10, 64)  
//最多尝试操作数据库的次数  
for i := 0; i < config.Attempts; i++ {  
flag := false //默认无问题  
//取消赞行为,只有当前状态是点赞状态才会发起取消赞行为,所以如果查询到,必然是cancel==0(点赞)  
//先查询是否有这条数据  
likeInfo, err := dao.GetLikeInfo(userId, videoId)  
//如果有问题,说明查询数据库失败,返回错误信息err:"get likeInfo failed"  
if err != nil {  
log.Printf(err.Error())  
flag = true //出现问题  
} else {  
if likeInfo == (dao.Like{}) { //只有当前是点赞状态才能取消点赞这个行为  
// 所以如果查询不到数据则返回错误信息:"can't find data,this action invalid"  
log.Printf(errors.New("can't find data,this action invalid").Error())  
} else {  
//如果查询到数据,则更新为取消赞状态  
//如果有问题,说明插入数据库失败,打印错误信息err:"update data fail"  
if err := dao.UpdateLike(userId, videoId, config.Unlike); err != nil {  
log.Printf(err.Error())  
flag = true  
}  
}  
}  
//一遍流程下来正常执行了,那就打断结束,不再尝试  
if flag == false {  
break  
}  
}  
}  
}  
  
var RmqLikeAdd *LikeMQ  
var RmqLikeDel *LikeMQ  
  
// InitLikeRabbitMQ 初始化rabbitMQ连接。  
func InitLikeRabbitMQ() {  
RmqLikeAdd = NewLikeRabbitMQ("like_add")  
go RmqLikeAdd.Consumer()  
  
RmqLikeDel = NewLikeRabbitMQ("like_del")  
go RmqLikeDel.Consumer()  
}