一、mq简介
1 - mq应用场景
- 什么是mq(message queue):消息队列是一种“先进先出”的数据结构
- mq应用场景
- 解耦:系统的耦合性越高,容错性就越低。以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障或者因为升级等原因暂时不可用,都会造成下单操作异常,影响用户使用体验
- 流量削峰:应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮。有了消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以大大提高系统的稳定性和用户体验
- 举例:业务系统正常时段的QPS如果是1000,流量最高峰是10000,为了应对流量高峰配置高性能的服务器显然不划算,这时可以使用消息队列对峰值流量削峰
- 数据分发:通过消息队列可以让数据在多个系统之间进行流通。数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据即可
- mq缺点
- 系统可用性降低:系统引入的外部依赖越多,越容易挂掉,本来你就是 A 系统调用 BCD 三个系统的接口就好了,人 ABCD 四个系统好好的,没啥问题,你偏加个 MQ 进来,万一MQ 挂了咋整?MQ 挂了,整套系统崩溃了
- 系统复杂性提高:硬生生加个 MQ 进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?
- 一致性问题:A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了
2 - mq技术选型
特性 | ActiveMq | RabbitMq | RocketMQ | Kafka |
---|
成熟度 | 成熟 | 成熟 | 比较成熟 | 成熟的日志领域 |
时效性 | | 微秒级 | 毫秒级 | 毫秒级 |
社区活跃度 | 低 | 高 | 高 | 高 |
单机吞吐量 | 万级,吞吐量比RocketMQ和Kafka要低了一个数量级 | 万级,吞吐量比RocketMQ和Kafka要低了一个数量级 | 10万级,RocketMQ也是可以支撑高吞吐的一种MQ | 10万级别,这是kafka最大的优点,就是吞吐量高。一般配合大数据类的系统来进行实时数据计算、日志采集等场景 |
topic数量对吞吐量的影响 | | | topic可以达到几百,几千个的级别,吞吐量会有较小幅度的下降这是RocketMQ的一大优势,在同等机器下,可以支撑大量的topic | topic从几十个到几百个的时候,吞吐量会大幅度下降所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源 |
可用性 | 高,基于主从架构实现高可用性 | 高,基于主从架构实现高可用性 | 非常高,分布式架构 | 非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
消息可靠性 | 有较低的概率丢失数据 | | 经过参数优化配置,可以做到0丢失 | 经过参数优化配置,消息可以做到0丢失 |
功能支持 | MQ领域的功能极其完备 | 基于erlang开发,所以并发能力很强,性能极其好,延时很低 | MQ功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 |
- 优缺点总结
- ActiveMq:非常成熟,功能强大,在业内大量的公司以及项目中都有应用偶尔会有较低概率丢失消息而且现在社区以及国内应用都越来越少,官方社区现维护越来越少,几个月才发布一个版本而且确实主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用
- RabbitMq:erlang语言开发,性能极其好,延时很低;吞吐量到万级,MQ功能比较完备而且开源提供的管理界面非常棒,用起来很好用社区相对比较活跃,几乎每个月都发布几个版本分在国内一些互联网公司近几年用rabbitmq也比较多一些但是问题也是显而易见的,RabbitMQ确实吞吐量会低一些,这是因为他做的实现机制比较重。而且erlang开发,国内有几个公司有实力做erlang源码级别的研究和定制?如果说你没这个实力的话,确实偶尔会有一些问题,你很难去看懂源码,你公司对这个东西的掌控很弱,基本职能依赖于开源社区的快速维护和修复bug。而且rabbitmq集群动态扩展会很麻烦,不过这个我觉得还好。其实主要是erlang语言本身带来的问题。很难读源码,很难定制和掌控
- RocketMQ:接口简单易用,而且毕竟在阿里大规模应用过,有阿里品牌保障日处理消息上百亿之多,可以做到大规模吞吐,性能也非常好,分布式扩展也很方便,社区维护还可以,可靠性和可用性都是ok的,还可以支撑大规模的topic数量,支持复杂MQ业务场景而且一个很大的优势在于,阿里出品都是java系的,我们可以自己阅读源码,定制自己公司的MQ,可以掌控社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准JMS规范走的有些系统要迁移需要修改大量代码还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用RocketMQ挺好的
- kafka:kafka的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展同时kafka最好是支撑较少的topic数量即可,保证其超高吞吐量而且kafka唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集
二、rocketMq安装
三、rocketmq基本概念
1 - rocketmq基本概念
- Producer:消息的发送者,例如:发信者
- Consumer:消息的接收者,例如:收信者
- Broker:暂存和传输消息,例如:邮局
- Name Server:管理Broker,例如:各个邮局的管理机构
- Topic:区分消息的种类;一个发送者可以发送消息给一个或者多个Topic;一个消息的接收者可以订阅一个或者多个Topic消息(理解Topic:把Broker理解成微信,Topic理解成公众号,可以订阅多个公众号的消息,也可以发送消息给多个公众号)
- Message Queue:相当于是Topic的分区;用于并行发送和接收消息
- 数据读取与存储流程:数据保存在Broker,数据获取先到Name Server中获取Broker配置信息,然后再到Broker中获取数据
2 - rocketmq消息类型
- 按照发送特点分
- 同步发送
- 同步发送,线程阻塞,投递completes阻塞结束
- 如果发送失败,会在默认的超时时间3秒内进行重试,最多重试2次
- 投递completes不代表投递成功,要check SendResult.sendStatus来判断是否投递成功
- SendResult里面有发送状态的枚举:SendStatus,同步的消息投递有一个状态返回值
- 注意事项:发送同步消息且Ack为SEND_OK,只代表该消息成功的写入了MQ当中,并不代表该消息成功的被Consumer消费了
- 这种可靠性同步地发送方式应用场景非常广泛,例如重要通知邮件、报名短信通知、营销短信系统等
- 异步发送
- 异步调用的话,当前线程一定要等待异步线程回调结束再关闭producer,因为是异步的,不会阻塞,提前关闭producer会导致未回调链接就断开了
- 异步消息不retry,投递失败回调onException()方法,只有同步消息才会retry,源码参考DefaultMQProducerImpl.class
- 异步发送一般用于链路耗时较长,对响应时间较为敏感的业务场景,例如,您视频上传后通知启动转码服务,转码完成后通知推送转码结果等
- 单向发送
- 消息不可靠,性能高,只负责往服务器发送一条消息,不会重试也不关心是否发送成功
- 此方式发送消息的过程耗时非常短,一般在微妙级别
发送方式 | 发送TPS | 发送结果反馈 | 是否有重试 | 可靠性 |
---|
同步发送 | 快 | 有 | 有 | 不丢失 |
异步发送 | 快 | 有 | 无 | 不丢失 |
单向发送 | 最快 | 无 | 无 | 可能丢失 |
- 按照使用功能特点划分
- 普通消息(订阅):普通消息是我们在业务开发中用到的最多的消息类型,生产者需要关注消息发送成功即可,消费者消费到消息即可,不需要保证消息的顺序,所以消息可以大规模并发地发送和消费,吞吐量很高,适合大部分场景
- 顺序消息:顺序消息分为分区顺序消息和全局顺序消息,全局顺序消息比较容易理解,也就是哪条信息先进入,哪条信息就会被先消费,符合我们FIFO,很多时候全局消息的实现代价很大,所以就出现了分区顺序消息
- 全局顺序:对于指定的个 Topic,所有消息按照严格的先先出(First In First Out,简称FIFO)的顺序进发布和消费
- 分区顺序:对于指定的个 Topic,所有消息根据 Sharding Key 进区块分区。 同个分区内的消息按照严格的 FIFO 顺序进发布和消费。Sharding Key 是顺序消息中来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念
- 延时消息:业务开发需求场景 -> 订单超时库存归还
- 延迟的基址时在服务端实现的,也就是Broker收到了消息,但是经过一段时间以后才发送;服务器按照1-N定义了如下级别“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”;若要发送定时消息,在应用层初始化Message消息对象之后,调用Message.setDelayTimeLevel(int level)方法来设置延迟级别,按照序列取响应的延迟级别,例如level=2,则延迟为5s
- 实现原理:
- a.发送消息的时候如果消息设置了DelayTimeLevel,那么该消息会被丢到ScheduleMessageService.SCHEDULE_TOPIC这个Topic里面
- b.根据DelayTimeLevel选择对应的queue
- c.再把真实的topic和queue信息封装起来,set到msg里面
- d.然后每个SCHEDULE_TOPIC_XXXX的每个DelayTimeLevelQueue,有定时任务去刷新,是否有待投递的消息
- e.每10s定时持久化发送进度
- 事务消息
- 事务消息:消息队列 RocketMQ 版提供类似 X/Open XA 的分布式事务功能,通过消息队列RocketMQ 事务消息能达到分布式事务的最终致
- 半事务消息:暂不能投递的消息,发送已经成功地将消息发送到了消息队列 RocketMQ 版服务端,但是服务端未收到产者对该消息的次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息
- 消息回查:由于络闪断、产者应重启等原因,导致某条事务消息的次确认丢失,消息队列RocketMQ 版服务端通过扫描发现某条消息期处于“半事务消息”时,需要主动向消息产者询问该消息的最终状态(Commit 或是 Rollback),该询问过程即消息回查
四、go使用rocketmq
1 - go发送普通消息
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() {
p, err := rocketmq.NewProducer(producer.WithNameServer([]string{"192.168.124.51:9876"}))
if err != nil {
panic("生成producer失败")
}
if err = p.Start(); err != nil {
panic("启动producer失败")
}
res, err := p.SendSync(context.Background(), primitive.NewMessage("imooc1", []byte("this is imooc1")))
if err != nil {
fmt.Printf("发送失败: %s\n", err)
} else {
fmt.Printf("发送成功: %s\n", res.String())
}
if err = p.Shutdown(); err != nil {
panic("关闭producer失败")
}
}
2 - pushconsumer消费消息
- order_srv/rocketmq_test/consumer/main.go
- consumer.WithGroupName:这个GroupName很重要,可以用来实现负载均衡,对于同一group的生产者,生产了相同的数据rocketmq会不会重复生产,对于消费者也是如此,对于同一group的消费者,也不会重复消费
package main
import (
"context"
"fmt"
"time"
"github.com/apache/rocketmq-client-go/v2"
"github.com/apache/rocketmq-client-go/v2/consumer"
"github.com/apache/rocketmq-client-go/v2/primitive"
)
func main() {
c, _ := rocketmq.NewPushConsumer(
consumer.WithNameServer([]string{"192.168.124.51:9876"}),
consumer.WithGroupName("mxshop"),
)
if err := c.Subscribe("imooc1", consumer.MessageSelector{}, func(ctx context.Context, msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error) {
for i := range msgs {
fmt.Printf("获取到值: %v \n", msgs[i])
}
return consumer.ConsumeSuccess, nil
}); err != nil {
fmt.Println("读取消息失败")
}
_ = c.Start()
time.Sleep(time.Hour)
_ = c.Shutdown()
}
3 - 发送延迟消息
- order_srv/rocketmq_test/delay/main.go:先运行order_srv/rocketmq_test/consumer/main.go,然后运行order_srv/rocketmq_test/delay/main.go,等待几秒之后就可以看到消息
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() {
p, err := rocketmq.NewProducer(producer.WithNameServer([]string{"192.168.124.51:9876"}))
if err != nil {
panic("生成producer失败")
}
if err = p.Start(); err != nil {
panic("启动producer失败")
}
msg := primitive.NewMessage("imooc1", []byte("this is delay message"))
msg.WithDelayTimeLevel(3)
res, err := p.SendSync(context.Background(), msg)
if err != nil {
fmt.Printf("发送失败: %s\n", err)
} else {
fmt.Printf("发送成功: %s\n", res.String())
}
if err = p.Shutdown(); err != nil {
panic("关闭producer失败")
}
}
4 - TransactionProducer事务消息
package main
import (
"context"
"fmt"
"time"
"github.com/apache/rocketmq-client-go/v2"
"github.com/apache/rocketmq-client-go/v2/primitive"
"github.com/apache/rocketmq-client-go/v2/producer"
)
type OrderListener struct{}
func (o *OrderListener) ExecuteLocalTransaction(msg *primitive.Message) primitive.LocalTransactionState {
fmt.Println("开始执行本地逻辑")
time.Sleep(time.Second * 3)
fmt.Println("执行本地逻辑成功")
return primitive.CommitMessageState
}
func (o *OrderListener) CheckLocalTransaction(msg *primitive.MessageExt) primitive.LocalTransactionState {
fmt.Println("rocketmq的消息回查")
time.Sleep(time.Second * 15)
return primitive.CommitMessageState
}
func main() {
p, err := rocketmq.NewTransactionProducer(
&OrderListener{},
producer.WithNameServer([]string{"192.168.124.51:9876"}),
)
if err != nil {
panic("生成producer失败")
}
if err = p.Start(); err != nil {
panic("启动producer失败")
}
res, err := p.SendMessageInTransaction(context.Background(), primitive.NewMessage("TransTopic", []byte("this is transaction message")))
if err != nil {
fmt.Printf("发送失败: %s\n", err)
} else {
fmt.Printf("发送成功: %s\n", res.String())
}
time.Sleep(time.Hour)
if err = p.Shutdown(); err != nil {
panic("关闭producer失败")
}
}