文章目录
前言
RocketMQ 是一种快速、低延迟、可靠、可扩展和分布式的消息中间件,特别为大规模分布式系统设计。它是一种消息解决方案,为系统架构中各种应用、微服务和其他组件提供高效可靠的通信。
RocketMQ 提供了消息持久性、事务消息、发布/订阅消息、消息过滤和负载平衡等功能。它支持同步和异步消息,并能处理高消息吞吐量和低延迟的消息。
RocketMQ 还提供了灵活可扩展的架构,可轻松集成到各种应用中,包括电子商务、金融、物联网和游戏应用。它的可扩展性和高性能使其成为需要可靠消息功能的大规模分布式系统中的一个流行选择。
总之,RocketMQ 是一个强大的消息中间件,为大规模分布式系统提供高效、可靠和可扩展的消息。
一、MQ使用场景
1. 什么是MQ
MQ (Message Queue) 消息队列是一种软件体系结构模式,它允许不同的应用程序或系统之间通过一个中间代理来进行消息传递。这种代理叫做消息队列服务器,通常简称为 MQ 服务器。
消息队列中的消息可以在生产者和消费者之间传递,生产者向队列中投递消息,消费者从队列中读取消息。这种模式在分布式系统、异步任务处理、解耦任务之间的依赖关系等方面都有很好的应用。
2. 应用场景
其应用场景主要包含以下3个方面
(1)应用解耦
系统的耦合性越高,容错性就越低。以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障或者因为升级等原因暂时不可用,都会造成下单操作异常,影响用户使用体验。
使用消息队列解耦合,系统的耦合性就会提高了。比如物流系统发生故障,需要几分钟才能来修复,在这段时间内,物流系统要处理的数据被缓存到消息队列中用户的下单操作正常完成,当物流系统回复后
(2)流量削峰
应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮。
有了消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以大大提到系统的稳定性和用户体验。
一般情况,为了保证系统的稳定性,如果系统负载超过阈值,就会阻止用户请求,这会影响用户体验,而如果使用消息队列将请求缓存起来,等待系统处理完毕后通知用户下单完毕,这样总不能下单体验要好。
处于经济考量目的:
业务系统正常时段的QPS如果是1000,流量最高峰是10000,为了应对流量高峰配置高性能的服务器显然不划算,这时可以使用消息队列对峰值流量削峰
(3)数据分发
通过消息队列可以让数据在多个系统更加之间进行流通。
数据的产生方不需要关心谁来使用数据,将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据即可。
3. MQ的优点和缺点
优点:解耦、削峰、数据分发
缺点包含以下几点:
- 系统可用性降低
系统引入的外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响。如何保证MQ的高可用? - 系统复杂度提高
MQ的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过MQ进行异步调用。
如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性? - 一致性问题
A系统处理完业务,通过MQ给B、C、D三个系统发消息数据,如果B系统、C系统处理成功,D系统处理失败。
如何保证消息数据处理的一致性?
二、MQ技术选型
结论:
- 中小型软件公司,建议选RabbitMQ.一方面,erlang语言天生具备高并发的特性,而且他的管理界面用起来十分方便。正所谓,成也萧何,败也萧何!他的弊端也在这里,虽然RabbitMQ是开源的,然而国内有几个能定制化开发erlang的程序员呢?所幸,RabbitMQ的社区十分活跃,可以解决开发过程中遇到的bug,这点对于中小型公司来说十分重要。不考虑rocketmq和kafka的原因是,一方面中小型软件公司不如互联网公司,数据量没那么大,选消息中间件,应首选功能比较完备的,所以kafka排除。但是rocketmq已经交给apache管理,所以rocketmq的未来发展趋势看好。
- 大型软件公司,根据具体使用在rocketMq和kafka之间二选一。一方面,大型软件公司,具备足够的资金搭建分布式环境,也具备足够大的数据量。针对rocketMQ,大型软件公司也可以抽出人手对rocketMQ进行定制化开发,毕竟国内有能力改JAVA源码的人,还是相当多的。至于kafka,根据业务场景选择,如果有日志功能,肯定是首选kafka了,具体该选哪个,看使用场景。
这里我选RocketMQ 原因有两点:
- 延迟消息简单高效
-
- 死信队列
- 完善的事务消息功能
三、RocketMQ 安装和配置
1. 下载
2. 安装
下载解压后
进入conf 编辑配置文件 把127.0.0.1换成自己服务器(虚拟机)的IP
docker-compose up
3. rocketmq-consle发送消息
在查看的时候起始日期尽量提前 因为服务器(虚拟机)的日期可能和当前时间不匹配
四、RocketMQ 基本概念
- Producer: 消息的发送者;举例:发信者
- Consumer: 消息接收者;举例:收信者
- Broker: 暂存和传输消息;举例:邮局
- NameServer: 管理Broker;举例:各个邮局的管理机构
- Topic: 区分消息的种类;一个发送者可以发送消息给一个或者多个Topic;一个消息的接收者可以订阅一个或者多个Topic消息
- Message Queue: 相当于是Topic的分区;用于并行发送和接收消息
五、RocketMQ的消息类型
按照发送的特点分:
1. 同步发送
- 同步发送,线程阻塞,投递
completes
阻塞结束 - 如果发送失败,会在默认的超时时间
3秒
内进行重试,最多重试2次 - 投递
completes
不代表投递成功,要check SendResult.sendStatus
来判断是否投递成功 SendResult
里面有发送状态的枚举:SendStatus,同步的消息投递有一个状态返回值的public enum SendStatus { SEND_OK, FLUSH_DISK_TIMEOUT, FLUSH_SLAVE_TIMEOUT, SLAVE_NOT_AVAILABLE, }
- retry的实现原理:只有
ack
的SendStatus=SEND_OK
才会停止retry
注意事项:
发送同步消息且Ack
为SEND_OK
,只代表该消息成功的写入了MQ
当中,并不代表该消息成功的被Consumer
消费了
2. 异步发送
- 异步调用的话,当前线程一定要等待异步线程回调结束再关闭
produce
r啊,因为是异步的,不会阻塞,提前关闭producer
会导致未回调链接就断开了 - 异步消息不
retry
,投递失败回调onException()
方法,只有同步消息才会retry
,源码参考DefaultMQProducerlmpl.class
- 异步发送一般用于链路耗时较长,对RT响应时间较为敏感的业务场景,例如用户视频上传后通知启动转码服务,转码完成后通知推送转码结果等。
3. 单项发送
- 消息不可靠,性能高,只负责往服务器发送一条消息,不会重试也不关心是否发送成功
- 此方式发送消息的过程耗时非常短,一般在微秒级别
下表概括了三者的特点和主要区别。
按照使用功能特点分:
1. 普通消息(订阅)
普通消息是我们在业务开发中用到的最多的消息类型,生产者需要关注消息发送成功即可,消费者消费到消息即可,不需要保证消息的顺序,所以消息可以大规模并发地发送和消费,吞吐量很高,适合大部分场景。
2. 顺序消息
顺序消息分为分区顺序消息和全局顺序消息,全局顺序消息比较容易理解,也就是哪条消息先进入,哪条消息就会先被消费,符合我们的FIFO,很多时候全局消息的实现代价很大,所以就出现了分区顺序消息。分区顺序消息的概念可以如下图所示:
我们通过对消息的key
,进行hash
,相同hash
的消息会被分配到同一个分区里面,当然如果要做全局顺序消息,我们的分区只需要一个即可,所以全局顺序消息的代价是比较大的。
3. 延时消息–订单超时库存归还
延迟的机制是在服务端实现的,也就是Broker
收到了消息,但是经过一段时间以后才发送
服务器按照1-N定义了如下级别:“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h2h”;若要发送定时消息,在应用层初始化Message消息对象之后,调用Message.setDelayTimeLeve(intlevel)
方法来设置延迟级别,按照序列取相应的延迟级别,例如level=2
,则延迟为5s
msg.setDelayTimeLevel(2);
SendResult sendResult = producer.send(msg );
实现原理:
- 发送消息的时候如果消息设置了
DelayTimeLevel
,那么该消息会被丢到ScheduleMessageService.SCHEDULE_TOPIC
这个Topic
里面 - 根据
DelayTimeLevel
选择对应的queue
- 再把真实的
topic
和queue
信息封装起来,set
到msg
里面 - 然后每个
SCHEDULE_TOPIC_XXXX
的每个DelayTimeLevelQueue
,有定时任务去刷新,是否有待投递的消息 - 每
10s
定时持久化发送进度
4. 事务消息
正是因为有完善的事务消息才会用到rocketMQ(重点)
上一篇文章也详细分析过事务消息 这里就不详细分析了
消息队列RocketMQ版提供的分布式事务消息适用于所有对数据最终一致性有强需求的场景。本文介绍消息队列RocketMQ版事务消息的概念、优势、典型场景、交互流程以及使用过程中的注意事项。
概念介绍
- 事务消息: 消息队列RocketMQ版提供类似X或Open XA的分布式事务功能,通过消息队列RocketMQ版事务消息能达到分布式事务的最终一致。
- 半事务消息: 暂不能投递的消息,发送方已经成功地将消息发送到了消息队列RocketMQ版服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递"状态,处于该种状态下的消息即半事务消息。
- 消息回查: 由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,消息队列RocketMQ版服务端通过扫描发现某条消息长期处于“半事务消息"时,需要主动向消息生产者询问该消息的最终状态(Commit或是Rollback),该询问过程即消息回查。
分布式事务消息的优势
消息队列RocketMQ版分布式事务消息不仅可以实现应用之间的解耦,又能保证数据的最终一致性。同时,传统的大事务可以被拆分为小事务,不仅能提升效率,还不会因为某一个关联应用的不可用导致整体回滚,从而最大限度保证核心系统的可用性。在极端情况下,如果关联的某一个应用始终无法处理成功,也只需对当前应用进行补偿或数据订正处理,而无需对整体业务进行回滚。
六、go发送消息
需要关闭防火墙
1. 发送普通消息
package main
import (
"context"
"fmt"
"github.com/apache/rocketmq-client-go/v2"
"github.com/apache/rocketmq-client-go/v2/primitive"
"github.com/apache/rocketmq-client-go/v2/producer"
)
func main() {
//连接recketmq
p, err := rocketmq.NewProducer(producer.WithNameServer([]string{"192.168.10.130:9876"}))
if err != nil {
fmt.Println("生成producer失败:", err)
}
//启动
err = p.Start()
if err != nil {
fmt.Println("启动producer错误:", err)
}
//实例化消息
msg := &primitive.Message{
Topic: "jzin",
Body: []byte("this is jzin"),
}
//同步发送
res, err := p.SendSync(context.Background(), msg)
if err != nil {
fmt.Printf("send message error: %s\n", err)
} else {
fmt.Printf("send message success: result=%s\n", res.String())
}
//关闭连接
err = p.Shutdown()
if err != nil {
fmt.Printf("shutdown producer error: %s", err.Error())
}
}
2. 消费消息
package main
import (
"context"
"fmt"
"github.com/apache/rocketmq-client-go/v2"
"github.com/apache/rocketmq-client-go/v2/consumer"
"github.com/apache/rocketmq-client-go/v2/primitive"
"os"
"time"
)
func main() {
//启动recketmq并设置负载均衡的Group
c, _ := rocketmq.NewPushConsumer(
consumer.WithNameServer([]string{"192.168.10.130:9876"}),
consumer.WithGroupName("jzins"),
)
//订阅消息
if err := c.Subscribe("jzin", consumer.MessageSelector{}, func(ctx context.Context, msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error) {
for i := range msgs {
fmt.Printf("subscribe callback: %v \n", msgs[i])
}
return consumer.ConsumeSuccess, nil
}); err != nil {
fmt.Println(err.Error())
}
//启动
err := c.Start()
if err != nil {
fmt.Println(err.Error())
os.Exit(-1)
}
//阻塞主线程
time.Sleep(time.Hour)
//关闭连接
err = c.Shutdown()
if err != nil {
fmt.Printf("shutdown Consumer error: %s", err.Error())
}
}
3. 发送延时消息
为什么要用延时消息而不是自己写:
支付的时候,淘宝,12306 ,购票,超时归还–定时执行逻辑
我可以去写一个轮询,轮询的问题:1.多久执行一次轮询30分钟
在12:00执行过一次,下一次执行就是在12:30的时候但是12:01的时候下了单,12:31就应该超时13:00时候才能超时
那我1分钟执行一次啊,比如我的订单量没有这么大,1分钟执行一次,其中29次查询都是无用,而且你还还会轮询mysql
rocketmq的延迟消息,1.时间一到就执行,2.消息中包含了订单编号,你只查询这种订单编号
只需要在msg
里设置WithDelayTimeLevel
就行了:
msg.WithDelayTimeLevel(3)
4. 发送事务消息
package main
import (
"context"
"fmt"
"github.com/apache/rocketmq-client-go/v2"
"github.com/apache/rocketmq-client-go/v2/primitive"
"github.com/apache/rocketmq-client-go/v2/producer"
"go.uber.org/zap"
"time"
)
type OrderListener struct {
ID int32
Detail string
}
func (o *OrderListener) ExecuteLocalTransaction(msg *primitive.Message) primitive.LocalTransactionState {
//执行逻辑并返回状态-自己决定
return primitive.CommitMessageState
}
func (o *OrderListener) CheckLocalTransaction(msg *primitive.MessageExt) primitive.LocalTransactionState {
//执行回查逻辑并返回状态-自己决定
return primitive.RollbackMessageState
}
func main() {
p, err := rocketmq.NewTransactionProducer(
&OrderListener{},
producer.WithNameServer([]string{"192.168.10.130:9876"}),
)
if err != nil {
zap.S().Error("生成producer失败:%s", err.Error())
}
//启动
if err = p.Start(); err != nil {
zap.S().Error("启动producer失败:%s", err.Error())
}
//发送半消息
res, err := p.SendMessageInTransaction(context.Background(), primitive.NewMessage("order_info", []byte("this is tranJzin")))
if err != nil {
fmt.Printf("发送失败: %s\n", err)
} else {
fmt.Printf("发送成功: %s\n", res.String())
}
//if res.State == primitive.CommitMessageState {
// fmt.Printf("发送失败: %s\n", err)
//}
//阻塞主线程
time.Sleep(time.Hour)
if err = p.Shutdown(); err != nil {
fmt.Println("关闭producer失败")
}
}