这是我参与「第五届青训营」笔记创作活动的第8天。
今天主要是应用RabbitMQ优化了大项目的聊天系统,解决了大项目聊天系统的诸多问题。
项目初期聊天业务接口的存在的问题及解决方案
前文提到,在项目初期,在收到GET请求/douyin/message/chat/时,采用的是直接去数据库查询相应的聊天列表的方案。
但是,大项目的前端聊天界面,每过一秒都会重新发送一条GET请求(具体路径为:/douyin/message/chat/)给我们的后端服务器,这就会导致我们频繁地从数据库中拉取相同的MessageList,造成消息重复问题。
图1 前端频繁发送相同的get请求给后端
图2 造成消息重复问题
针对此问题,我在前文中也提出了如下的解决方案:
- 首先实现RabbitMQ的Queue中聊天记录的初始化--将数据库的消息写入到MQ的Queue中去。将数据库的Message信息拉取出来,得到一个MessageList,将这个MessageList通过josn.Mashal()序列化,转成一个byte数组。然后将此byte数组转成String类型,以str类型Json格式的形式存到我们的RabbitMQ的Queue中去。
- RabbitMQ的Queue中聊天记录初始化的时候,我们为了确保只初始化一次,将Queue的MAX-Length设置为1,并采用drop-head的过期策略,这样使得我们在初始化的时候总能初始化最新的数据库中的聊天记录到Queue中去,让RabbitMQ的Queue中的消息始终只有一条,在消费的时候不会二次加载我们的聊天记录,也就从本质上解决了我们的消息重复问题。
- 在接收到路径为/douyin/message/chat/的GET请求时,我们对RabbitMQ的Queue中的消息进行消费,消费后得到一个string类型的Json串。然后使用json.Unmashal()进行反序列化,重新得到消息列表,返回给前端。
针对聊天记录问题具体实现逻辑如下:
在发送新消息的时候,我们不仅仅调用Dao层的方法将Message信息写入到数据库中,还要将Message信息写到RabbitMQ的消息队列中去,和上面的处理思路十分类似,我们针对CurrentMessage信息Declare的Queue的MAX-Length仍然为1,并采用drop-head的过期策略。在接收到路径为/douyin/message/chat/的GET请求时,我们对RabbitMQ的Queue中的消息进行消费,然后反序列化,存到一个MessageList中去,最后将该MessageList返回给前端即可。
使用RabbitMQ优化项目的具体代码实现
业务逻辑分析
在收到Get请求/douyin/message/chat/前,一定会来到好友列表页面,并收到如下图所示的GET请求。
那么,只要我们在服务端接收到GET/douyin/relation/friend/list/请求时,将数据库的聊天记录数据Publish到RabbitMQ的Queue中去,就实现了RabbitMQ的Queue中聊天记录的初始化。
在下一次接收到GET/douyin/message/chat/请求时,消费RabbitMQ的Queue中刚刚初始化的聊天记录,即可实现下图的效果。
注:
- 我们对该Queue在Declare时进行了参数的设置--MAX-Length设置为1并采用drop-head的溢出策略。
- MAX-Length设置为1,可以保证Queue中只有一条被初始化的聊天记录,避免聊天记录重复的问题。
- drop-head的溢出策略,可以保证在聊天记录的初始化执行多次导致消息达到MAX-Length而发生溢出时,将最新的聊天记录发送到Queue中去,同时将Queue中旧的消息丢弃。
具体代码实现
我们将消息归类成两种:MessageList和MessageCurrent。其中MessageList是数据库的messages表中所有的message组成的List,MessageCurrent是当前用户在当前时刻发送的message。
每个userId都关联两个Queue。比如userId为1的用户,可以消费message_current1和message_list1中的聊天记录。
代码实现思路如下,今天先分享MessageList的生产和消费的实现方案:
生产端:
- 首先调用Service层的GetAllFriendsMessageList函数从数据库拉取到和该userId相关的所有Message,得到allFriendsMessageList。
- 对刚刚获取到的allFriendsMessageList进行序列化得到marshal对象。
- 将marshal转成string类型,为将该消息生产到Queue中做准备。
- 将该消息Publish到RabbitMQ的该用户对应的message_list Queue中。
//首先调用Service层的GetAllFriendsMessageList函数
//从数据库拉取到和该userId相关的所有Message,得到allFriendsMessageList
allFriendsMessageList, _ := ralation.GetAllFriendsMessageList(myId.(int))
//对刚刚获取到的allFriendsMessageList进行序列化得到marshal对象
marshal, _ := json.Marshal(allFriendsMessageList)
//将marshal转成string类型,为将该消息生产到Queue中做准备
strJson := string(marshal)
//将该消息Publish到RabbitMQ的该用户对应的message_list Queue中
err = mq.PublishMessageListToMQ(strJson, myId.(int))
func PublishMessageListToMQ(strJsonMessageList string, rabbitMQQueueId int) error {
strRabbitMQQueueId := strconv.Itoa(rabbitMQQueueId)
conn, err := amqp.Dial("amqp://admin:Qd20010701.@10.211.55.4:5672/")
if err != nil {
return err
}
ch, err := conn.Channel()
if err != nil {
return err
}
argumentsMap := map[string]interface{}{}
argumentsMap["x-max-length"] = 1
argumentsMap["x-overflow"] = "drop-head"
//Declare以userId为后缀的message_list队列
q, err := ch.QueueDeclare(
"message_list"+strRabbitMQQueueId, // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
argumentsMap, // arguments
)
if err != nil {
return err
}
err = ch.Publish(
"", // exchange
q.Name, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(strJsonMessageList),
})
ch.Close()
conn.Close()
return err
}
消费端:
只需要调用GetRabbitMQMessageList(userId)即可对该userId对应的message_list队列中的消息进行消费,得到allMessageList。
func GetRabbitMQMessageList(userId int) (respMessageList []model.RespMessage, err error) {
conn, _ := amqp.Dial("amqp://admin:Qd20010701.@10.211.55.4:5672/")
strUserId := strconv.Itoa(userId)
ch, _ := conn.Channel()
argumentsMap := map[string]interface{}{}
argumentsMap["x-max-length"] = 1
argumentsMap["x-overflow"] = "drop-head"
q, _ := ch.QueueDeclare(
"message_list"+strUserId, // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
argumentsMap, // arguments
)
msgs, _ := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
allMessageList := []model.RespMessage{}
messageList := []model.RespMessage{}
//message := model.RespMessage{}
go func() {
for d := range msgs {
//将消费得到的MQ中的string类型的json串反序列化,写入messageList对象中
json.Unmarshal(d.Body, &messageList)
//将messageList追加到allMessageList中
allMessageList = append(allMessageList, messageList...)
}
}()
err = ch.Close()
if err != nil {
g.Logger.Infof("ch.Close()时发生了错误!")
}
err = conn.Close()
if err != nil {
g.Logger.Infof("conn.Close()时发生了错误!")
}
return allMessageList, err
}
总结
今天分享了在大项目中引入RabbitMQ解决聊天记录重复问题的代码实现方案。对RabbitMQ的应用场景有了进一步的理解。