消息队列简介
消息队列(MQ),指保存消息的一个容器,本质是个队列。但这个队列需要支持高吞吐,高并发,高可用。
消息队列-Kafla
使用场景
- 用在离线的情况下,比如日志信息,可以进行分析
- Metrics数据:对程序状态的采集比如搜索服务,直播服务,订单服务,支付服务等,可以采集耗时等
- 用户行为:点赞、评论、收藏、搜索
如何使用
创建集群 -> 新增topic -> 编写生产者逻辑-> 编写消费者逻辑
基本概念
上图第三个副本与第一个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进行分配
Kafka架构
而在集群的基础上,还有一个模块是ZooKeeper,这个模块其实是存储了集群的元数据信息,比如副本的分配信息等等,Controller计算好的方案都会放到这个地方
producer
如果发送一条消息,等到其成功后再发一条会太慢了。producer就是批量patch发送消息。但是如果消息量太大的话,带宽会不够用,所以producer会支持ZSTD等压缩方法。
broker
消息会有过期机制,所以需要切分
同时broker是顺序写以减少磁盘寻道的时间,用二分查找来读,用的也是基于sendfile的零拷贝。Consumer从Broker中读取数据,通过sendfile的方式,将磁盘读到os内核线冲区后,直接转到socket buffer进行网络发送Producer生产的数据持久化到broker,采用mmap文件映射,实现顺序的快速写入。
这些技术都可以减少时间
consumer
手动分配的缺点是,如果有一个consumer宕机了,其他的也会停,所以提供了自动分配的办法
青训营项目消息队列代码示例
需求分析
实现一个点赞功能的消息队列处理系统,使用 RabbitMQ 来处理点赞和取消点赞操作的消息。根据消息队列中的消息,它会执行相应的数据库操作,以确保数据的一致性和可靠性。这种方式可以有效地将点赞操作异步处理,提高系统的性能和可扩展性。
具体实现
-
NewLikeRabbitMQ函数:这个函数用于创建一个LikeMQ实例,初始化 RabbitMQ 连接并返回。它接受一个队列名作为参数,用于标识不同的点赞队列。 -
Publish函数:这个函数用于发布点赞消息到 RabbitMQ 队列。它创建一个队列(如果不存在),然后将消息发布到队列中。 -
Consumer函数:这个函数用于消费 RabbitMQ 队列中的消息。它创建一个队列(如果不存在),然后开始接收消息并根据队列名调用相应的消费函数。 -
consumerLikeAdd和consumerLikeDel函数:这两个函数分别用于处理点赞和取消点赞操作的消息。它们解析消息体,执行数据库操作,并支持重试逻辑。 -
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()
}