RocketMQ相关总结

RocketMQ相关总结

概念

所谓消息中间件,其实就是一种系统,他自己也是独立部署的,然后让其他系统之间通过发消息和收消息,来进行异步的调用,而不仅仅局限于同步调用。

作用

异步化提升性能

如果存在多个业务,那么多个业务系统之间通信都是进行接口调用的,然后系统A收到请求之火,立马去调用系统B,直到系统B返回给系统A之后,才能返回结果给用户,这种模式就是所谓的"同步调用"。

在两个系统中间,加入一个MQ,然后系统A将消息发送到MQ中,然后就直接返回给用户,系统B则是在适当的时机的时候将消息从MQ中获取到,进行处理,这种情况下,系统A与系统B是有实现通信,系统A发送给MQ,MQ被系统B获取,但是这时候是异步调用的,系统A与系统B的步调是不同的。

这种情况下,用户的体验就会直线上线,原本整个流程需要200ms,结果因为不需要执行系统B,可能就需要20ms就可以得到返回结果。

降低系统耦合

由于MQ的加入,原本两个系统是耦合在一起的,两个系统都要同时进行更改,或者同时不变,而且如果系统B发生了故障,那么就会影响至系统A发生故障,然后处理异常返回给用户,处理系统B对应的异常需要额外的精力和实现,很麻烦,现在就直接返回给用户,就算系统B发生故障,也不会影响到系统A,两个系统之间的耦合性大大降低。

流量削峰

假设系统A可以承载1w的QPS,然后系统B进行数据库的操作,但是数据库只能承载6k的QPS,那么加入MQ之后,系统A以1w的QPS来发送消息至MQ,MQ是可以抗几万的QPS,然后系统B从MQ中按6k的QPS获取消息,写入到数据库中,MQ中一定会积压一些消息,但是MQ一般都是基于磁盘来存储消息,所以适当积压是可以的,之后高峰期过后,系统A可能就恢复到1k的QPS,此时系统B还是以6k的QPS获取消息写入数据库,那么MQ里积压的消息慢慢就会被消化掉了

如果都是同步的情况下,高QPS可能会导致数据库承受不住QPS导致瞬间压垮。

所以MQ进行流量削峰的效果就是,系统A发送过来的1w请求的流量洪峰,被MQ给行下来,存储到自己本地磁盘,之后系统B来慢慢消化获取消息。

以上就是MQ的作用。

技术选型

RabbitMQ

优点:可以保证数据不丢失,也可以保证高可用性,支持部分高级功能,比如死信队列,消息重试之类的。

缺点:吞吐量较低,每秒几万,集群扩展较麻烦,开发语言为erlang,不好进行阅读和修改

kafka

优点:性能非常高,吞吐量也非常高,达到十几万QPS,可用性也高,支持集群部署。

缺点:数据可能会丢失,功能单一简单

一般作为日志的采集,比如大数据团队的日志,所以数据适当丢失是没有关系的。

RocketMQ

优点:可以保证数据不丢失,高可用性,支持各种高级功能性能也很高,吞吐量也很高,可达10wQPS,支持大规模的集群部署,基于Java开发,容易阅读和修改

缺点:官方文档简单,阿里企业开源,会一定的停用风险。

RocketMQ的架构原理和使用方式

如何集群化部署支持高并发

假设每台机器能抗10w并发,那么就可以将几十万甚至几百万的高并发分散到多台机器上,并且让每台机器的QPS不超过MQ的10w并发就可以了

如何存储海量消息

因为MQ接收到大量的消息后,并不是立马被所有的消费者获取后进行消费的,所以一般MQ都得把消息放在自己的本地磁盘存储起来,等待消费者进行消费处理。

既然入磁,MQ存储的消息可能会很大,几百万条,几亿条,那么一台机器上肯定没办法存储的,所以就需要把消息分散发送给多态不同的机器进行存储,比如1万条消息分发给10台机器,每台机器存储1000条消息

每台机器上部署的RocketMQ进程被称为Broker,每个Broker都会收到不同的消息,然后把这批消息存储在自己的磁盘文件中。

所以本质上RocketMQ存储海量消息的机制就是分布式存储,所谓分布式存储,就是将数据分散在多台机器上存储,每台机器存储一部分消息,多台机器就可以存储海量消息了。

高可用保障

如何解决Broker宕机的问题,解决思路是Broker主从架构以及多副本策略

简单来说,Broker是有Master和Slave两种,也就是主从,Master Broker收到消息后同步给Slave Broker,这样Slave Broker就会有一模一样的一份副本数据,多个Slave副本就可以更加保障MQ的可用性,防止单个Slave也挂掉。

数据路由:怎么直到访问哪个Broker

有一个NameServer的概念,他也是独立部署在几台机器上,然后所有的Broker都会把自己注册到NameServer上,那么NameServer就直到集群里有哪些Broker了。

对于系统而言,如果要发消息到Broker,就会去NameServer去获取路由信息,就是集群里有哪些Broker等信息,如果系统要从Broker获取信息,也会去NameServer获取路由信息,去对应的Broker获取消息。

原理图大概如下:

在这里插入图片描述

NameServer

集群部署

由于NameServer要管理Broker的信息,系统都要从NameServer中获取路由信息,如果只部署一台,如果挂掉了,那么就无法路由到对应的Broker中了,那么Broker也无法进行注册了。

所以NameServer集群的目的是为了高可用性。

Broker是注册到哪个NameServer

答案是每个Broker启动都得向所有的NameServer进行注册,因为每个NameServer都要有完整的Broker信息,要不然宕机的时候,切换到其他NameServer会导致数据丢失,找不到部分Broker的。

系统如何从NameServer获取Broker信息

RocketMQ中的生产者和消费者是主动去NameServer拉取Broker信息的,通过这些信息,每个系统就知道发送消息或者获取消息从哪个Broker上进行,起到了一个把消息路由到一个Broker的效果,所以这种信息叫做路由信息。

Broker宕机,NameServer如何感知

Broker和NameServer之间有心跳机制,Broker会每隔30s给所有的NameServer发送心跳,告诉每个NameServer自己还活着,每次NameServer收到一个Broker的心跳,就可以更新一下他的最近一次心跳时间

然后NameServer会每隔10s运行一个任务,去检查一下各个Broker的最近一次心跳时间,如果某个Broker超过120s都没发送心跳了,那么就认为这个Broker已经挂掉了。

作为生产者或消费者如何感知Broker宕机,首先,可以考虑不发送消息到这台Broker中,改成其他的Broker,其次,如果必须发送给这台Broker,那么他还有Slave机器,可以继续使用,那么可以考虑过一会与Slave进行通信。

而且过一会,NameServer获取到最新的路由信息,会知道这个Broker已经宕机了。

Broker

Master如何同步到Slave

这里注意一下,所有的Slave也会向所有的NameServer进行注册,也会30s发送心跳。

RocketMQ自身的Master-Slave模式采取的是Pull模式拉取信息,也就是Slave Broker不停的发送请求到Master Broker去拉取信息

RocketMQ实现读写分离了吗

原则上不算是完全的读写分离,生产者发送消息是往Master进行发送,而消费者获取消息则有可能是Master有可能是Slave,要根据Master Broker的负载情况和Slave Broker的同步情况判断。

如果Master Broker负载很重,已经要抗10w写并发,那么此时Master Broker就会建议你从Slave Broker中拉取消息,还有就是Slave同步的比较慢,100w数据差了4w,而消费者可能都获取完96w了,那么下次还是只能从Master Broker去拉取消息,因为同步太慢,没法获取更新的消息了。

Master或者Slave挂掉了有什么影响

1.Master Broker宕机

这个时候Slave Broker有一样的数据在的,只不过可能会有部分数据没来得及同步过来,而且不能自动切换成Master Broker。

4.5版本前,一旦Master故障,那么就需要手动做一些运维操作,将Slave重新修改参数配置,重启机器调整为Master Broker,所以这种Master-Slave模式不是彻底的高可用模式,没法自动切换主从。

4.5版本后,引入了新的机制,叫做Dledger。

这个机制是基于Raft协议实现,简单来说把Dledger融入RocketMQ之后,可以让一个Master Broker对应多个Slave Broker,也就是一份数据有多份备份,一旦Master宕机了,那么就可以在多个Slave中,通过Dledger技术和Raft协议算法选举处leader,然后直接将一个Slave Broker选举成新的Master Broker,整个过程也许只要10s或者几十秒就可以自动完成。

2.Slave Broker宕机

会有一点影响,但是不大,因为写入全部发送到Master Broker中,然后获取也是可以走Master Broker的,所以整体影响不大,只不过会导致读写压力都集中在Master Broker上。

所以整体流程变为:
在这里插入图片描述

设计RokectMQ高可用架构

NameServer集群化部署

首先将NameServer进行集群化部署,建议3个或3个以上,这样即使有两台同时宕机,只要有一个NameServer还在运行,就可以保证MQ的路由信息是可用的。

NameServer里任何一台机器都是独立运行的,跟其他的机器没有任何通信,且每台NameServer都有完整的集群路由信息,包括Broker节点信息,我们的数据信息等等

基于Dledger的Broker主从架构部署

因为4.5版本后就可以使用Dledger技术自动实现Master-Slave切换,避免了无法自动切换,需要手动进行修改配置,重启这种非常麻烦的操作。

Dledger技术是至少需要一个Master对应两个Slave,由三个Broker组成一个Group,一旦Master宕机,就可以从剩下的两个Slave中选举出新的Master对外提供服务。

Broker与NameServer通信

每个Broker会跟所有的NameServer建立一个TCP长连接,然后定时通过TCP长连接发送心跳请求过去

那么NameServer就是通过这个建立好的长连接,定时检查Broker有没有120s都没发送心跳的,来判断Broker是否宕机。

使用MQ的系统集群部署

除了MQ系统以外,其他的系统,也就是生产者系统和消费者系统也都需要进行集群部署。
如果一个系统部署在一个机器上,那么作为生产者向MQ发送消息,一旦这个生产者系统挂掉,整个流程就断开了,不能保证高可用性。

Topic

topic是MQ中的核心数据模型,是一个数据集合的意思,不同类型的数据放到不同的Topic中。
由于Topic的数据量可能会很大,达到几百万,甚至几千万的数据量,而且这还是一个Topic,所以为了存储大量的数据,Topic要进行分布式存储,将Topic创建的时候指定他里面的数据分散存储在多台Broker机器上,

tips:每个Broker在定时发送心跳给NameServer的时候,都会告诉NameServer自己当前的数据情况,包括Topic的哪些数据在自己这里,这些都属于路由信息的一部分。

生产者和消费者如何发送和获取消息

无论是生产者还是消费者,在发送和获取消息的时候,都要从NameServer中拉取最新的路由信息,那么就要和NameServer建立一个TCP长连接,包括集群里有哪些Broker,集群里有哪些Topic,每个Topic都存储在哪些Broker上

然后系统通过路由信息找到自己要投递或获取消息的Topic分布在哪几台Broker上,此时可以根据负载均衡算法,从里面选择一台Broker,比如round robine轮询算法,hash算法等等。

选择一个Broker之后,就可以跟哪个Broker也建立一个TCP长连接,然后通过长连接向Broker发送或获取消息即可。

生产者是一定往Master发送消息,消费者则都有可能,根据之前说过的情况进行判断。

部署RocketMQ集群

官方文档为:https://github.com/apache/rocketmq/tree/master/docs/cn

基本配置

1.构建Dledger

git clone https://github.com/openmessaging/openmessaging-storage-dledger.git

cd openmessaging-storage-dledger

mvn clean install -DskipTests

2.构建RocketMQ

git clone https://github.com/apache/rocketmq.git

cd rocketmq

git checkout -b store_with_dledger origin/store_with_dledger

mvn -Prelease-all -DskipTests clean install -U

3.JAVA_HOME配置

cd distribution/target/apache-rocketmq

之后里面需要编辑三个文件,分别是bin/runserver.sh,bin/runbroker.sh,bin/tools.sh

删除掉JAVA_HOME的后两行,并且将JAVA_HOME第一行的值修改为自己JDK的主目录

快速部署尝试

如果要在一台机器进行尝试的话

sh bin/dledger/fast-try.sh start、

步骤成功的话就可以进行

sh bin/mqadmin clusterList -n 127.0.0.1:9876

来查看RocketMQ集群的状态

你会看到三行记录,说是一个RaftCluster,Broker名称叫做RaftNode00,然后BID是0,1,2或者其他,其中BID为0的就是Master Broker,剩下的就是Slave Broker。

如果想验证自动切换节点,可以使用lsof -i 端口号,来找到对应的PID,然后kill -9 PID来杀死这个进程,然后等待一段时间,再次查看集群状态,就会发现BID为0的变成了另外一个Broker。

NameServer的部署

将每台机器的基本配置完成后,进行如下命令

nohup sh mqnamesrv &

默认端口是9876,所以三台机器上都启动了NameServer,那么他们的端口号都是9876,我们这时就完成了NameServer集群的部署

一组Broker的部署

还是将基本配置完成后,执行

nohup sh bin/mqbroker -c conf/dledger/broker-n0.conf &

第一个Broker的配置文件时broker-n0.conf,第二个Broker的配置文件时broker-n1.conf,第三个Broker的配置文件时broker-n2.conf。

里面的配置如下:

# 这个是集群的名称,整个broker集群都可以用这个名称
brokerClusterName=RaftCluster

#这是Broker的名称,一个组的Broker,也就是三个一组的话,三个Broker的名称必须是一样的,如果有另外一组Broker,可以起别的名字
brokerName=RaftNode00

# 这个是你的Broker监听的端口号,如果每台机器就部署一个Broker,那么可以不用修改
listenPort=30911

# 这里是配置NameServer的地址,如果有多个NameServer,那就写入多个NameServer
namesrvAddr=127.0.0.1:9876

# 下面是存放Broker数据的地方,可以换成别的目录
storePathRootDir=/tmp/rmqstore/node00
storePathCommitLog=/tmp/rmqstore/node00/commitlog

# 是否启用DLeger技术,那肯定启用啊
enableDLegerCommitLog=true

# 这个一般建议和Broker名字保持一致,一个Group的名字
dLegerGroup=RaftNode00

# 这个很关键,对于每一组的Broker,得保证他们的这个配置是一样的,要写出来一个组里有多少个Broker,比如三台机器部署了Broker,那么在这里就得写入他们三个的ip地址和监听的端口号
dLegerPeers=n0-127.0.0.1:40911;n1-127.0.0.1:40912;n2-127.0.0.1:40913

# 这个代表一个Broker在组里的id,一般就是n0,n1之类的,这个必须与上面的dLegerPeers中的n0,n1,n2相匹配
dLegerSelfId=n0

# 这个是发送消息的线程数量,一般建议配置跟CPU核数一样
sendMessageThreadPoolNums=16

按照上面的配置好一组Broker之后,启动Broker即可,然后可以用以下命令查看Broker集群状态:
sh bin/mqadmin clusterList -n 127.0.0.1:9876、

之后编写生产者和消费者的代码使用即可。

可视化管理工具

拉取git
git clone https://github.com/apache/rocketmq-externals.git

进入rocketmq-console目录
cd rocketmq-externals/rocketmq-console

打包
mvn package -DskipTests

然后进入target目录下,可以看到一个jar包,接着执行下面的命令启动工作台:
java -jar rocketmq-console-ng-1.0.1.jar --server.port=8080 --rocketmq.config.namesrvAddr=127.0.0.1:9876

这里务必要在启动的时候设置好NameServer的地址,如果有多个地址可以用分号隔开,接着就会看到工作台启动了,然后就通过浏览器访问那台机器的8080端口就可以了,就可以看到精美的工作台界面。

由于是支持中文的,所以用中文就可以了

Rocket集群参数配置

OS内核参数的配置

1.vm.overcommit_memory
这个参数有三个值可以选择:0,1,2

0, 表示内核将检查是否有足够的可用内存供应用进程使用;如果有足够的可用内存,内存申请允许;否则,内存申请失败,并把错误返回给应用进程。
1, 表示内核允许分配所有的物理内存,而不管当前的内存状态如何。
2, 表示内核允许分配超过所有物理内存和交换空间总和的内存

所以当配置0的时候,是可能会在申请内存的时候被拒绝从而返回异常错误的,因此一般情况下设置1就可以

可以用如下命令修改:echo ‘vm.overcommit_memory=1’ >> /etc/sysctl.conf。

2.vm.max_map_count

表示影响中间件系统可以开启的线程数量,默认是65536,如果在大量的线程请求的情况下需要调大一些,比如kafka集群进行大数据收集的时候,可以调大10被,保证有足够的线程数。

可以用如下命令修改:echo ‘vm.max_map_count=655360’ >> /etc/sysctl.conf。

3.vm.swappiness

这个参数是设置swap区域的,就是将不太活跃的区域调整为睡眠状态,给活跃的进程使用。

0~100,100就是尽量把进程放到swap区域中。

一般情况下生产环境建议把这个参数调小一些,比如10,尽量用物理内存,别放到swap区域中

可以用如下命令修改:echo ‘vm.swappiness=10’ >> /etc/sysctl.conf。

4.ulimit

表示linux上最大文件链接数,默认是1024,但是一般肯定不够的,大量频繁的读写磁盘文件的时候都与这个参数有关,如果超过最大值,就:会报错:too many openfiles

因此通常建议用如下命令修改这个值:echo ‘ulimit -n 1000000’ >> /etc/profile。



以上就是需要调整的OS参数,无非是围绕磁盘IO,网络通信,内存管理,线程数量有关。

JVM参数调整

在rocketmq/distribution/target/apache-rocketmq/bin目录下,就有对应的启动脚本,比如mqbroker是用来启动Broker的,mqnamesvr是用来启动NameServer的。

里面runserver.sh和runbroker.sh里有相关JVM的配置参数

JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g"

这个就是设置堆大小的和新生代的大小。

JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"

这是使用的垃圾回收器,还有每个Region的大小,-XX:G1ReservePercent=25 这个是给老年代预留的空闲内存,避免老年代内存满了,-XX:InitiatingHeapOccupancyPercent=30是当堆使用率30%时候自动启动并发垃圾回收,尝试回收一些

JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:/dev/shm/mq_gc_%p.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m

这些是打开gc日志的,然后还有设置相关的日志文件大小,最多保留多少这样

-XX:-OmitStackTraceInFastThrow:这个参数是说,有时候JVM会抛弃一些异常堆栈信息,因此这个参数设置之后,就是禁用这个特性,要把完整的异常堆栈信息打印出来

-XX:+AlwaysPreTouch:这个参数的意思是我们刚开始指定JVM用多少内存,不会真正分配给他,会在实际需要使用的时候再分配给他

-XX:MaxDirectMemorySize=15g:这是说RocketMQ里大量用了NIO中的direct buffer,这里限定了direct buffer最多申请多少,如果你机器内存比较大,可以适当调大这个值,如果有朋友不了解direct buffer是什么,可以自己查阅一些资料。

-XX:-UseLargePages -XX:-UseBiasedLocking:这两个参数的意思是禁用大内存页和偏向锁,这两个参数对应的概念每个要说清楚都得一篇文章,所以这里大家直接知道人家禁用了两个特性即可。

基本上启用的打印和行为参数不用动,我们需要根据自己的机器调整参数大小,然后禁用一些对运行不利的参数,启用一些优化的参数即可。

RocketMQ核心参数

在下面的目录里有dledger的示例配置文件:rocketmq/distribution/target/apache-rocketmq/conf/dledger

sendMessageThreadPoolNums=16,这个参数就是RocketMQ内部用来发送消息的线程池的线程数量,可以根据核数来适当增加

RocketMQ压测

对于压测来说,并不是机器能抗的最高负载就是我们允许的最大负载,因为在最大负载下,可能压测的时候,CPU,内存,IO负载都极高,很有可能要宕机了,这时候在真正的生产环境下能放心的允许达到这个最大值吗?显然是不行的,因此我们要测出来是一个最合适的最高负载,也就是CPU,内存,IO负载都是可以接受成都下的最大负载量,即在RocketMQ的TPS核机器的资源使用率和负载之间取得一个平衡。

RocketMQ和TPS和消息延时

让两个生产者和消费者不停地往RocketMQ集群发送消息,每个生产者和消费者开启80个线程同时发送和写入,每条数据大小是500字节,然后通过工作台,可以发现从生产到存储,再到消费,基本上时间跨度不大于1s,这个性能是可以接受的,同时,Master Broker 的TPS(每秒处理消息的数量),可以稳定在7w左右。

CPU负载情况

通过top和uptime来查看Broker机器上的CPU负载。

比如top命令可以看到cpu load和cpu使用率,带条cpu负载情况

例如load average:12.03,12.05,12.08

表示cpu在1分钟。5分钟,15分钟内的cpu负载情况,12表示有12个核在使用中,如果是24核的机器,就表示还有12个核是空闲的,那么就表示cput负载比较良好,没有达到极限

内存使用率

使用free命令可以查看内存的使用率,如果内存是48G的,然后显示仅仅使用了一部分,那么剩下的内存很大一部分都是空闲的,或者是被RocketMQ用来进行磁盘数据缓存了

JVM GC频率

这个就不多说了,新生代几十秒一次正常,老年代尽量不进行垃圾回收,或者很偶尔进行回收。

磁盘IO负载

可以使用top命令查看IO等待占用CPU时间的百分比,
Cpu(s): 0.3% us, 0.3% sy, 0.0% ni, 76.7% id, 13.2% wa, 0.0% hi, 0.0% si。

这里13.2wa就是说磁盘IO等待在CPU执行时间中的百分比。

所以比例太高,说明大部分都在等待IO,说明IO负载很大,压测的时候如果在40以上,说明比较高了,需要调整来降低百分比。

网卡流量

可以用以下命令查看
sar -n DEV 1 2

通过这个命令可以看到每秒钟读写数据量,在千兆网卡下,理论上限是128M每秒,但是实际上每秒100M左右,所以如果500字节左右的大小下,2台80线程的机器,大概每秒是80多将近90M的数据量,几乎达到了网卡极限了



所以在这个范围内,网卡流量已经达到极限值的时候,基本上无法再提升TPS了,其他的基本在正常范围内,那么这个RocketMQ一个比较靠谱的TPS就是7w左右。



RocketMQ简单示例

生产者实例

public class RocketMQProducer {
    // 这是RocketMQ的生产者类,用这个发送消息
    private static DefaultMQProducer producer;

    static {
        // 构建一个Producer实例对象
        producer = new DefaultMQProducer("order_producer_group");
        // 设置NameServer的地址,拉取到路由信息
        producer.setNamesrvAddr("localhost:9876");
        try {
            // 启动Producer
            producer.start();
        } catch (MQClientException e) {
            e.printStackTrace();
        }
    }

    public static void send(String topic, String message) throws Exception {
        // 构建一条消息对象
        Message msg = new Message(
                topic, // 发送到指定的topic
                "", // 这时消息的Tag
                message.getBytes(RemotingHelper.DEFAULT_CHARSET) // 这是消息,转换成字节
        );

        // 用producer来发送消息
        SendResult sendResult = producer.send(msg);

        System.out.printf("%s%n", sendResult);
    }
}

消费者实例

public class RocketMQConsumer {

    public static void start() {
        // 创建多线程
        new Thread() {
            @Override
            public void run() {
                try {
                    // 创建消费者实例对象
                    // 里面就是消费者分组,订单就是order_group等等,自定义分组名
                    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("credit_group");
                    // 消费者也要拉取路由信息
                    consumer.setNamesrvAddr("localhost:9876");
                    // 选择订阅的topic消息
                    consumer.subscribe("TopicOrderPaySuccess", "*");
                    // 注册消息监听器来拉取到订单消息
                    // 拉取到的情况下就会回调这个方法给你处理
                    consumer.registerMessageListener(new MessageListenerConcurrently() {
                        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                            // 这里对获取到的订单消息进行处理,增加积分,发送优惠卷,通知发货,通知短信等等
                            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                        }
                    });
                    // 启动消费者实例
                    consumer.start();
                    System.out.printf("Consumer Started.%n");
                    // 每秒不停的消费
                    while (true) {
                        Thread.sleep(1000);
                    }
                } catch (MQClientException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
}


以上就是生产者和消费者使用的简单实例,那么原来订单系统的流程图就变为:

在这里插入图片描述

发送消息

同步发送消息:就是示例的代码,就是同步发送消息,要等MQ返回SendResult,之后才会继续执行。

异步发送消息:就是不会等待MQ返回结果,直接往下继续走,当MQ返回结果的时候,会回调你的SendCallBack里的函数。

// 设置异步发送失败的时候重试次数为0
producer.setRetryTimesWhenSendAsyncFailed(0);

SendCallback sendCallback = new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
        // 这里会回调MQ返回成功
    }

    @Override
    public void onException(Throwable throwable) {
        // 这里会回调MQ返回失败
    }
};
producer.send(msg, sendCallback);

发送单向消息:就是发送一个消息给MQ,然后就直接往下走,也不会关注MQ有没有返回结果给你,无论成功或失败都无关。

producer.sendOneway(msg);

消费消息

Push消费模式:之前的实例就是Push,可以看到Consumer的类名是DefaultPushConsumer
所谓Push消费模式,就是Broker会主动把消息发送给你的消费者,而你的消费者是被动的接收Broker推送过来的消息,然后进行处理

Pull消费模式:使用的Consumer的类名是DefaultPullConsumer,也就是Broker不会主动推送消息给Consumer,而是消费者主动发送请求到Broker去拉取消息过来。

DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("credit_group");
consumer.start();
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicOrderPaySuccess");

for (MessageQueue mq : mqs) {
    System.out.printf("Consume from the queue: %s%n", mq);
    while (true) {
        // 从MQ中拉取结果
        PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, 1, 12);
        List<MessageExt> msgFoundList = pullResult.getMsgFoundList();
        // 在这里进行一些处理
    }
}

解决订单系统的问题

下单核心流程环节太多,性能较差

由于MQ的引入,使得订单系统只需要更改数据库订单的状态和减少库存就可以了,然后将消息发送到MQ,之后直接返回给用户,剩下的就让其他的系统从MQ中取得消息进行消费处理就可以,之前可能整个流程为1s,现在只需要100~200ms即可,性能提升了不少

跟第三方系统耦合在一起,性能存在抖动的风险

之前的系统是与第三方系统耦合在一起的,比如第三方短信系统,第三方物流系统,那么如果这两个第三方系统存在抖动的情况,平时100ms,但有时候会出现1s的情况,那么整个订单流程就会变得更长,那么用户所需要等待的时间也就更长,但是现在由于MQ的引入,与第三方系统进行了解耦,不管他们是否发生抖动,与用户系统流程不发生直接关系,因为订单系统只是将订单支付消息发送到MQ即可,就算发生抖动,也是其他系统在消费消息处理的过程变得长一些,但这些是后台处理的过程,用户本身是无法感知的,所以与第三方系统也进行了解耦。

大数据或其他团队要获取订单数据,存在不规范直接查询订单数据库的问题

1.避免其他团队不规范的直接从订单数据库中获取数据,那么完全可以将订单数据推送到MQ中,然后从MQ中获取订单数据,然后落地到自己的存储中

2.由于需要的数据很可能是所有相关的数据,那么单个消息几乎不能满足要求,如果将所有的增删改操作都进行推送消息到MQ,那么会增加没必要的负担,而且与自己系统没有关系。

3.所以需要一个可以实时监听数据变化的系统,比如阿里开源的Canal,Linkedin开源的Databus,都是属于MySQL Binlog同步系统,这种系统可以监听MySQL的binlog,然后将binlog同步发送给你的系统,那么我们完全可以将binlog直接发送到MQ中

然后其他团队的同步系统从RocketMQ中获取到MySQL Binlog,也就是增删改操作,接着把增删改操作还原到自己的存储中即可。

在这里插入图片描述

解决秒杀活动下的高并发请求

解决高并发请求下的商品详情页

由于在商品秒杀前,会有大量的用户访问商品详情页,并进行刷新等待着商品倒计时,那么无疑商品详情页会被大量的用户持续访问,访问量是最高的,那么为了解决这个问题,要采用的就是页面数据静态化和多级缓存的方案。

首先许多数据如果从数据库进行访问,那么数据库必然会宕机,而且大量的数据是重复且没有必要去访问数据库的,比如标题,副标题,价格,图片,商品详情说明等等,那么就可以将这些数据提前从数据库中取出后进行组装成一份静态数据放在别的地方,然后使用的时候去取,而不是每次都是访问数据库。

多级缓存要使用的是CDN + Nginx + Redis的多级缓存架构。

CDN可以理解为,用户访问数据的时候从离自己最近的节点进行访问,比如将刚刚取出的静态数据放在广州的CDN上,那么广州的用户在发送请求的时候就会从就近的CDN上加载,并不用每次都发送到公司的机房中

如果CDN中的缓存过期了,那么就要从公司的机房中取数据,这时候可以使用Nginx去进行缓存,Nginx可以基于Lua脚本实现本地缓存,可以把数据放到Nginx中进行缓存,那么请求发送过来之后,直接从Nginx中加载缓存数据,不需要转发到商品系统上。

如果Nginx也过期了或者没缓存秒杀商品的数据的话,也需要二级缓存Redis集群了,Nginx中的Lua脚本发送请求到Redis集群中去加载我们提前放进去的秒杀商品数据。

如果都过期了,或者都没有加载到,那么最后是访问商品系统去到数据库中加载商品页数据,然后再缓存到我们的各级缓存中即可。

这样一套的方案,CDN就分摊掉了大量的请求,即使到达了我们的后台系统,也会可以使用Nginx + Redis方案来返回商品数据,且单机轻松可以抗住10w+的并发量。

在这里插入图片描述

秒杀系统下单时的高并发请求

在详情页之后,订单系统的秒杀系统而言,有以下重点:

1.在前端/客户端设置秒杀答题或者验证,错开大量人下单的时间,也阻止作弊器进行刷单

所以在秒杀的情况下,弹出框进行验证,因为每个人答题或者验证速度不同,就可以有效地错开发送请求地时间。

2.独立出来一套秒杀系统,专门负责处理秒杀请求

如果所有的订单都用一套系统的话,那么秒杀下单请求和不参与秒杀的请求会被共同由订单系统承载,那么秒杀下单请求会耗尽订单系统的资源,或者导致系统不稳定,以至于普通下单也会出现问题,而且普通下单也会占用秒杀请求的资源,所以一般会对订单系统部署两个集群,一个是秒杀订单系统集群,一个是普通订单集群。

3.优先基于Redis进行高并发的库存扣减,一旦库存扣完则秒杀结束

通常在秒杀情况下,如果去访问库存数据库去减少库存,那么会瞬间导致压力过大,所以一般会将每个秒杀商品的库存提前写入到Redis中,然后直接对Redis中的库存进行扣减,Redis是可以轻松用单机抗每秒几w高并发的,接着当Rdis库存减少到0的时候,就会发现库存已经没了,就无法抢购到商品了。

4.秒杀结束后,Nginx过滤掉无效的请求,大幅度消减转发到后端的流量

在商品库存减完之后,可以在zookeeper中写入一个秒杀完毕的标志位,然后zookeeper会反向通知Nginx中我们自己写的Lua脚本,通过Lua脚本后续在请求过来的时候直接过滤掉,不要向后进行转发。那么50w个人抢1w个商品,那么49w个请求是会被Nginx拦截掉,然后过滤掉无效请求。

5.瞬间生成的大量下单请求直接进入RocketMQ进行削峰,订单系统慢慢拉取消息完成下单操作

因为RocketMQ轻松可以抗下上w的请求,所以秒杀成功时,向MQ中发送一个消息,然后订单系统从MQ中消费秒杀成功的消息,进行常规性的流程处理即可。也就是最多可能会延迟几十秒。

在这里插入图片描述

精益求精

生产者是如何发送消息

MessageQueue

所谓MessageQueue就是这个Topic对应的队列数量,比如有一个Topic,为这个Topic创建了4个MessageQueue,然后有两组Broker,根据写入MessageQueue的策略,每个Broker会有不同的MessageQueue,且每个MessageQueue的数据量也会有所不同,但是大致可以认为是平均的,那么每组Broker就会放两个MessageQueue。

那么对于生产者来说,如果按照平均分发消息到MessageQueue,且MessageQueue也平均分配放到Broker,那么就会增加整体抗住的并发量,比如每个Broker能抗住7w,然后生产者发送消息给4个MessageQueue,发送了20条,每个MessageQueue平均是5条,每个Broker平均是两个MessageQueue,那么2个Broker就可以抗住14w的并发量。

如果Master Broker挂掉怎么办

当Master Broker挂掉,那么根据Dledger技术会自动切换新的Master Broker,但是切换的时间内,还是按照之前的策略均匀分配发送的话,挂掉的Master Broker就会访问失败。

所以通常来说,建议开启sendLatencyFaultEnable,这个开关的作用就是,如果某次访问一个Broker发现网络延迟有500ms,而且无法访问,那么就会自动回避访问这个Broker一段时间,比如3s内不访问了,那么就会减少发送到这个故障Broker的次数,直到下一个Master Broker选举出来后就可以正常访问了。

Broker持久化存储消息

Broker的数据存储实际上是RocketMQ中最重要的一个环节,它决定了生产者消息写入和消费者或者消息的吞吐量,也决定了消息是否能丢失的要素。

CommitLog

Broker接收到消息之后,会把消息写入到磁盘日志文件CommitLog中,CommitLog是很多磁盘文件,每个文件限定最多1GB,Broker收到消息之后直接顺序追加写入这个文件的末尾,写满了就会创建一个新的CommitLog文件。

CommitLog文件是先写入到OS的PageCache内存缓存中,然后由OS的后台线程进行异步化将缓存中的数据刷入到底层的磁盘文件,而且由于是顺序写的特性,所以导致写入的吞吐量非常高,基本上与直接写入内存里是差不多的。

上面的异步刷入磁盘是写入到OS PageCache中就直接返回ACK给生产者了,但是如果这个Broker宕机了,那么缓存中数据就会丢失了,所以这种模式是高吞吐量,但是有丢失数据的风险。
还有一种就是同步刷盘。每次Broker收到一条消息,都要将消息强制刷入地层的物理磁盘文件中,才会返回ACK给生产者,那么除非磁盘坏了,其他情况数据几乎不会丢失,但是代价就是每次都是刷入磁盘,性能肯定慢一些,吞吐量就会降低。

ConsumeQueue

在Broker的磁盘文件下,有下面格式的系列文件:
$HOME/store/consumequeue/{topic}/{queueId}/{fileName}
其中topic就是Topic主题,queueId就是MessageQueue对应的id,fileName就是MessageQueue对应ConsumeQueue。

每个MessageQueue有很多个ConsumeQueue文件,ConsumeQueue文件里存储的是一条消息在CommitLog文件中的offset偏移量,也就是CommitLog中的物理地址位置。

ConsumeQueue文件也是基于OS Cache的,因为ConsumeQueue会被大量的消费者发送请求读取CommitLog的物理offset偏移量,所以读取是非常频繁的,而且ConsumeQueue保存的并不是数据本身,而是所在的offset偏移量,所以大小不会很大,30w消息的offset只有5.72MB,所以实际上ConsumeQueue文件是不占用多少磁盘空间的,几乎完全可以被os缓存在内存cache里,所以消费者拉取消息的时候,第一步大量的频繁读取ConsumeQueue文件,几乎可以说是跟读取内存的数据性能是一样的,通过这个来保证消费者的高吞吐。

Dledger

首先第一步,Dledger来管理CommitLog,然后Broker还是可以基于Dledger管理的CommitLog去构建出来机器上的各个ConsumeQueue磁盘文件。

Raft协议选举Leader

Dledger是基于Raft协议选举出Leader Broker的。需要发起一轮一轮的投票,简单来说,每个Broker在启动的时候,都会给自己投票,并把投票发送给其他的Broker,那么都选自己肯定是不行的,所以第一轮选举是失败的,那么接下来会将所有的Broker进入随机时间的休眠,比如Broker1休眠100ms,Broker2休眠200ms,Broker3休眠300ms,那么先唤醒的Broker1就会给自己投票并发送给其他Broker,当其他的Broker唤醒后发现已经有发送过来的选票,那么就会尊重Broker1的选择,把票投给Broker1,所以当一般以上的票选举的时候,就可以被选出Leader Broker作为Master Broker。

Raft协议进行多副本同步

首先Master Broker收到一条消息后,会把消息标记为uncommitted状态,然后通过自己的DledgerServer组件把这个调戏发送给其他Broker的DledgerServer,之后收到uncommitted消息之后,必须返回一个ack给Master Broker的DledgerServer,如果超过半数的Follower Broker返回ack之后,就会将消息标记为committed状态,然后Master Broker就会发送committed消息发送给Follower Broker机器的DledgerServer,让他们也把消息标记为committed状态。

Leader Broker崩溃的情况

如果Leader Broker崩溃的时候,就会根据Raft协议的算法重新选举出Leader Broker,并且会把uncommitted的数据进行一些恢复性的操作,使数据恢复成committed状态。

消费者如何获取消息处理

消费组

同样的消息,可能有很多个系统都要获取这个消息并处理,所以讲每个系统分为一个组来进行消费,这就是消费组。

一条消息,每个消费组都会获取到这条消息,根据消费模式的区别,有集群消费模式和广播消费模式,一般用于集群消费模式即可,默认也是集群消费模式,可通过以下设置改为广播消费模式:
comsumer.setMessageModel(MessageModel.BROADCASTING);

集群消费模式就是一个消费组有多台机器,那么只会交给一台机器获取这条消息。
广播消费模式就是一个消费组有多台机器,那么就会给所有的机器获取这条消息。

MessageQueue与消费者的关系

可以简单的理解为,Borker会均匀地将MessageQueue分配给消费组的多台机器来消费,如果消费组里有机器宕机了,那么就会把宕机的机器所分配的MessageQueue均匀分配给其他的Broker,如果加机器了,那么就会将其他Broker上的MessageQueue分配给新的机器,总是就要保持负载均衡的原则。

Push模式

Push模式本质地层也是基于消费者主动拉取的模式来实现的,当消费者发送请求到Broker去拉取消息的时候,如果有新的消息可以消费,那么久立马返回一批消息到消息机器去处理,处理完之后立刻发送请求到Broker机器去拉取下一批消息,所以消息处理的时效性非常好,就像Broker一直推送消息到消费机器一样。
另外,Push模式下有一个请求挂起和长轮询的机制,当你的请求发送到Broker,结果没有新的消息给你处理,那么就会让请求线程挂起,默认是挂15s,这个期间也会有后台线程每隔一会就去检查一下是否有新的消息给你,如果有新的消息,就会唤醒挂起的线程,然后把消息返回给你。

Pull模式的代码写起来更复杂和繁琐,所以一般都是用Push来实现的。

Broker是如何将消息取出后返回给消费者的

一个消费者发送拉取请求到Broker,拉取指定MessageQueue中的消息,如果之前没拉取过,那么就会从MessageQueue的第一条消息开始拉取,那么Broker就会找到MessageQueue对应的ConsumeQueue文件,然后根据ConsumeQueue读取对应位置的消息在CommitLog中的物理offset偏移量,然后到CommitLog中根据offset读取消息数据,返回给消费者机器。

消费者如何处理消息

消费者拉取到一批消息之后,就会将这批消息回调我们的注册的一个函数,处理完之后就会提交目前一个消费进度到Broker,然后Broker就会存储我们的消费进度,下次再次拉取这个ConsumeQueue的消息,就可以从Broker记录的消费位置开始继续拉取,不用重新拉取。

消费者是根据什么策略选择Master还是Slave

第一步读取ConsumeQueue文件,这个之前写过,是存在os cache中,性能是很高的
第二步去找CommitLog对应的offset偏移量,但是CommitLog文件是存放数据本身的,所以大小是很大的,所以几乎不可能都存在os cache中,而且os cache主要是提升文件的写入性能,而且还有要给JVM提供额外的内存大小,所以大部分数据都是在磁盘上的。
如果你读取的数据是刚刚写入CommitLog的数据,那么大概率他们还停留在os cache中,那么就可以直接从os cache中读取CommitLog数据,那么性能就是很高的。如果你读取的是很早之前就写入的数据,那么可能os cache中已经被新的数据替代了,这时候就只能从磁盘的文件读取,性能会差一些。

所以如果消费者机器一直在快速的拉取和消费处理,紧紧的跟进生产者写入Broker的消息速率,那么几乎都是在os cache中消费取得的,但是如果Broker的负载很高,导致拉取消息的速度很慢,或者消费者处理消息的速度很慢,跟不上生产者写入的速度,比如生产者已经写了10w条,但是你才拉取2w条数据,那么可能最新的5w条都在os cache中,那剩下的3w条你还没有拉取的消息就大概率已经刷入磁盘了,那么相当于每次都从磁盘里读取数据了。

根据上面的铺垫,当消费者与生产者的差距很大的时候,那么Master Broker会判断出此时消费者大概率会从磁盘里加载出来消息,会认为出现这种情况,很可能是因为自己作为Master Broker 的负载太高了,导致没办法及时把消息给你,所以消费者落后进度太多,那么本次读取还是从Master Broker,但是下次就会让消费者从Slave Broker去读取消息了。




整体的流程图为:

在这里插入图片描述

RocketMQ如何基于Netty扩展出高性能网络通信架构

1.Broker有一个Reactor主线程,用来监听网络端口的,然后生产者,消费者与Broker建立TCP长连接,那么Reactor线程就会监听到建立的长连接。

之后生产者,消费者与Broker里会有一个SocketChannel来代表他们建立好的长连接,然后通过SocketChannel去发送和消费消息。

2.Broker里有一个Reactor线程池里的线程来监听SocketChannel,多个SocketChannel就会有多个线程来进行监听,这个线程池默认是3个线程。

3.在处理消息之前,要进行一些准备工作和预处理,比如SSL加密验证,编码解码,连接空闲检查,网络连接管理等等,Broker里有一个Worker线程池,默认有8个线程,Reactor线程收到这个请求会交给Wroker线程池的一个线程进行处理,完成上面的准备工作。

4.之后类似写入CommitLog,还有ConsumeQueue之类的事情需要处理,这些就是业务处理逻辑,那么预处理之后就要将请求转交给业务线程池进行处理,对于处理发送消息,就会转交给SendMessage线程池,这个也可以配置线程数量,配置越多,吞吐量自然就会更高,当然也要考虑机器的性能。

所以综上所述一旦建立好连接,大量的长连接均匀地分配给Reactor线程池的多个线程,就可以多线程并发监听不同连接的请求,有效提高处理能力,接着对请求都是基于Worker线程池预处理,此时Reactor还是可以继续监听和接收大量连接的请求,最终读写磁盘文件之类的操作交给业务线程池来处理,当并发的执行读写操作的时候,不影响其他线程池同时接收请求,预处理请求。

在这里插入图片描述

基于mmap内存映射实现磁盘文件的高性能读写

传统文件IO操作都会有多次数据拷贝的问题,读取就是要从磁盘文件将数据拷贝到内核IO缓冲区,然后再从内核IO缓冲区将数据拷贝到用户进程私有空间,那么对磁盘读写性能是有影响的。

mmap技术就是基于JDK NIO包下的MappedByteBuffer的map()函数,先将一个磁盘文件(比如一个CommitLog文件,或者是一个CosumeQueue文件)映射到内存来,映射的时候,并没有数据拷贝操作,只不过把物理上的磁盘文件的一些地址和用户进程私有空间的一些虚拟内存地址进行了一个映射。

这个mmap技术在进行映射的时候,一般有大小限制,在1.5GB到2GB,所以CommitLog单个文件在1GB,不会太大,那么在进行文件读写的时候,很方便的进行内存映射了。

然后就是PageCache,在这里对应于虚拟内存,比如要写入消息到CommitLog文件,先把这个文件通过MappedByteBuffer的map()函数映射到其地址到你的虚拟内存地址,接着就可以对这个MappedByteBuffer执行写入操作,写入时候会直接进入Page Cache,然后由os线程异步刷入磁盘中,那么整个过程只有虚拟内存拷贝到磁盘文件而已,性能要比两次拷贝要高。

如果是读数据的情况,如果在PageCache,就直接从Page Cache中读取数据,如果没有,就要从磁盘文件里加载数据到PageCache中,而且加载到PageCache的时候,还会将加载的数据块临近的其他数据块也一起加载到PageCache里去。

内存预映射机制:Broker会针对磁盘上的各种总CommitLog,ConsumeQueue文件预先分配好MappedFile,也就是提前对一些可能要读写的磁盘文件,那么之后读写文件的时候,就可以直接执行了。

文件预热:在提前对一些文件完成映射之后,因为映射不会直接将数据加载到内存里,那么在读取数据的时候可能会频繁的从磁盘里加载数据到内存中,所以执行完map()函数之后,会进行madvise系统调用,提前尽可能多的把磁盘文件加载到内存里去。

通过上述优化才真正实现读写的时候尽可能都是进入PageCache,保证高性能的读写操作。

在这里插入图片描述

消息丢失

消息丢失的原因

1.发送消息的时候可能会因为网络发生抖动,或者网络异常而没有发送到MQ中。

2.MQ本身是收到消息了,但是他的网络通信模块的代码出现了异常,可能是内部网络通信的bug,或者在写入到Page Cache的时候,机器宕机了,就算最后写入到了磁盘,但是磁盘发生故障,而且还没有备份,那么上面的数据还是会丢失。

3.默认情况下,MQ的消费者有可能自动提交已经消费的offset,但是此时消息还没有处理完,结果消费者系统宕机了,然后再次重启后从下一个offset位置进行消费,那么之前的消息就丢失了。

在这里插入图片描述

事务消息机制

解决方法:RocketMQ有一个非常强力的功能,就是事务消息的功能,首先就是先对MQ发送一条half消息,这个half消息就是要发送的消息,比如订单支付成功消息,只不过现在是half状态,对于消费者系统是不可见的。

为什么要先发送一条half消息,因为如果对数据库进行操作完成后,发现发送消息失败,那么消费者就无法从MQ中消费数据,那么就会丢失后续的处理,所以要先发送half消息,确保发送消息是成功的,然后将本地事务完成之后,发送一个commit请求给MQ,要求MQ将之前发送的half消息进行commit操作,让消费者可以看见这个消息。

1.如果half消息发送失败

那么就说明现在与MQ无法通信,要么MQ自己挂掉了,要么网络通信有问题,总之是无法通信的状态,那么生产者系统就要执行一系列的回滚操作了,比如让订单状态改成关闭交易,然后通知支付系统进行退款,因为你订单系统是正常的,但是其他系统无法从MQ中获取到消息,所以只能关闭此订单。

2.half消息发送成功,生产者系统的本地事务执行失败

half消息虽然成功,但是生产者本身在更新数据库的时候失败了,那么这个时候就直接让订单系统发送一个rollback请求给MQ,让MQ把之前发送的half消息给删除掉,然后自己在做一些回滚操作,退款之类的。

3.发送half消息成功,但是没有收到MQ响应,或者rollback,commit发送失败

如果没有收到MQ的响应,可能因为返回响应过程中发生网络异常,然后订单系统以为发送half消息失败,执行退款流程,但是MQ已经保存了half消息。

或者rollback和commit发送的时候也因为网络问题,发送失败,MQ没有收到,没有进一步处理这条half消息。

RocketMQ有一个补偿流程,会去扫描自己处于half状态的消息,如果一直没有对这个消息执行rollback或者commit消息,超过一段时间,就会回调你的订单系统的一个接口,那我们可以用这个接口去查下数据库,查看当前订单状态,然后发送rollback或者commit给MQ,将half消息删除或改成commit状态。

事务消息实现原理

当我们写入消息到MQ的时候,都会把MQ写入CommitLog中,然后将消费索引写入MessageQueue对应的ConsumeQueue文件中,所以当生产者与消费者都发送和消费同一个Topic的时候,就可以看见了,那么half消息是为什么看不见。

half消息是写入到MQ自己内部的"RMQ_SYSTRANS_HALF_TOPIC"这个Topic对应的ConsumeQueue里去了,那么消费者自然是无法从订阅的Topic看到这条half消息了。

所以一旦生产者收到half消息写入成功的响应,那么half消息就已经在RocketMQ内部了,如果一直没有rollback或commit,那么就会去扫描"RMQ_SYSTRANS_HALF_TOPIC"中的half消息,如果超过一定时间,就会回调之前系统的接口,判断half消息要rollback还是commit。

如果执行rollback,并不是物理上进行删除,而是用一个OP操作来标记half消息的状态,RocketMQ有一个OP_TOPIC,此时可以写一条rollback OP记录到这个Topic里,标记某个half消息是rollback了

回调生产者系统的接口去判断half的状态,最多回调15次,如果15次还是没有告知的话,就会自动标记成rollback。

如果执行commit状态,那么RocketMQ就会在OP_TOPIC里写一条记录,标记half消息已经是commit状态,接着把"RMQ_SYSTRANS_HALF_TOPIC"中的half消息写入到真正发送的Topic中的ConsumeQueue里去,那么消费者系统就可以看到并消费了。

在这里插入图片描述

可否不用事务消息机制,来保证消息不丢失,如果采用同步发消息 + 反复重试多次的方案可不可以,可以,但是效率和性能比较低:

1.如果数据库先进行更新,然后发送到MQ,更新后宕机了,那么MQ中还没有数据,怎么办

2.如果上面可以用mysql innodb事务来限制,但是如果中间还有redis更新,elasticsearch更新,那么这两个也无法进行回滚

3.同步等待响应后,进行重试的时间过长,用户体验很差。

所以,综上所述,使用事务消息机制的性能是要高的多,且代价也小。

代码案例实现

1.发送half消息

在这里插入图片描述

2.如果half消息发送失败,或者没有收到消息在这里插入图片描述
3.half消息成功后执行本地事务和MQ回调函数,检查本地事务状态

在这里插入图片描述

Broker配置

  1. 将异步刷盘调整为同步刷盘

异步刷盘:消息写入到Page Cache之后就直接返回,然后由OS线程异步将Page Cache的消息刷入到磁盘中

同步刷盘:每条写入MQ的消息,都要同步到磁盘后才会返回ack。

调整参数为,将flushDiskType配置设置为SYNC_FLUSH,同步刷盘,反之默认值是ASYNC_FLUSH异步刷盘。

同步刷盘后,只要返回写入成功,那么消息必然是在磁盘内

2.主从架构避免磁盘故障

由于主从架构,Master Broker写入成功后,那就一定会通过Raft协议同步给其他的Broker机器,那么在Master Broker磁盘损坏之后,Slave Broker中也有相应的数据副本,所以可以避免因磁盘故障导致的数据丢失。

消费者处理消息

对于kafka不同,RocketMQ在处理消息的时候,会对一批消息处理完之后才会提交offset,也就是手动提交offset,也就是在注册监听器的地方,如下图:

在这里插入图片描述
这个地方不要使用多线程异步进行,那样的话是会异步处理,可能还没处理完其他线程就提交offset了。

总结

消息零丢失方案的优势与劣势

优势自然不用说,就是可以保证消息不会丢失,对于重要的订单系统,支付系统,财政系统相关的重要数据,可以保证数据不丢失,防止对系统造成损失。

劣势:由于要保证消息不会丢失,发送消息的时候要进行两次的通信,而且还要进行回调检查等,性能肯定是要比直接简单发送消息要慢,Broker要每次都同步到磁盘,与直接写入Page Cache的性能无法相比的,之后原本子线程可以处理完这批消息之后,就直接返回成功,然后接着处理下一批,但是现在只能一批一批的处理,那么吞吐量肯定会降低,原本吞吐量在7w,结果进行配置后可能只有7k的吞吐量。

所以一般来说,只有在重要的核心链路系统和资金相关的系统上进行消息零丢失,其他的可以接受数据丢失的情况就要进行非零丢失配置,或者退化成使用同步 + 重试的机制,来提高吞吐量。

消息重复消费

发生原因

1.发送消息可能比较慢,导致订单系统和支付系统之间的请求出现了超时,那么支付系统重试调用订单系统,可能就会再次发送消息

2.发送消息比较慢,导致没有收到MQ给你的响应,然后再次重试发送消息,但是此时MQ中已经有这条消息了

3.消费者处理完消息之后,还没有提交offset到Broker,这时候进行重启部署,或者宕机,导致Broker并不知道你已经处理完这条消息,还会从这个offset开始给你发,就会导致发送了两次消息。

解决方法

解决重复消息的关键就在于保持数据的幂等性,即同一个消息只能处理一次。

1.生产者
第一个是业务判断法,也就是发送一条消息前,去当前MQ里查询是否有这条消息,如果有,那么就不再次发送这条消息。比如发送订单id=100的订单支付成功消息,那么就可以去MQ里查询订单id=100的订单支付成功消息存不存在。

第二个是状态判断法

可以使用Redis缓存来存储你是否发送过这条消息,通过订单id对应的状态,判断是否要发送,然后再发送过后将状态改成已发送。
但是Redis在极端条件情况下还是会重复发送,比如已经发送给MQ了,还没来得及改Redis,系统崩溃了,重启,然后发现Redis里还没发送,就再次进行发送。

但是其实不管是哪种方案,在发送消息的时候进行处理的性能都不是很好,影响接口的性能,所以建议发送消息阶段默许可以重复发送消息,可以在消费消息的时候进行处理

2.消费者

消费者对于生产者端处理消息重复就简单多了,比如消费者拿到订单消息,要对数据库进行变更,或者插入一条数据,那么就可以去数据库中查询是否存在,或者通过唯一键约束来控制数据的唯一性,也可以在这里通过Redis进行状态判断,但极端情况还是会丢失,但是可能性很小。这样就可以高效的控制消息不被重复消费,不会影响写入MQ的性能。

重试队列和死信队列

重试队列

如果消费者系统的数据库无法访问,导致这一批消息无法被处理,如果没处理成功,就会丢失消息,这时候就要用到重试队列。在消费者消费消息时,如果发生异常,可以返回一个RECONSUME_LATER的状态,让Broker过段时间再给我这批消息重试。代码如下:

在这里插入图片描述

RocketMQ接收到你的RECONSUME_LATER状态之后,针对你的消费组,将消息放到你这个消费组的重试队列中。

比如你的消费组的名称是“VoucherConsumerGroup”,意思是优惠券系统的消费组,那么他会有一个“%RETRY%VoucherConsumerGroup”这个名字的重试队列。

如果过段时间处理,又返回了RECONSUME_LATER,那么还会再过一段时间让我们处理,最多16次,每次重试时间间隔是不一样的,配置如下:
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
第次一1s,第二次5s,第三次10s,以此类推。

死信队列

如果重试队列15次还没有处理完,那么就会用到死信队列了,顾名思义,就是死掉的消息,死信队列的名字是“%DLQ%VoucherConsumerGroup”,我们其实在RocketMQ的管理后台上都是可以看到的。

如果数据库好使了,还想将之前的数据进行消费,那么可以专门开一个后台线程,订阅“%DLQ%VoucherConsumerGroup”这个死信队列,对死信队列中的消息,还是一直不停的重试。

消息乱序

发生原因

由于在写入消息的时候,是把消息给特定Topic的多个MessageQueue,那么在消费者系统端,多个消费者系统在获取MessageQueue的ConsumeQueue时候,是负责一部分MessageQueue的,导致存入的是insert xxx,update xxx,然后两台消费者拿到insert xxx和update xxx,后一个先执行了update xxx发现没有这条数据,报错,然后前者执行了insert xxx,那么最后的数据就不是我们想要的数据结果了。

解决方法

让一个订单的binlog进入到一个MessageQueue,比如一个订单id为101的订单,有两个binlog,那么就要用这个订单id对MessageQueue的数量进行取模,让两个binlog进入到一个MessageQueue中。

当然,Canal作为获取binlog的中间件,也要按照binlog的顺序获取到,并按顺序发送到同一个MessageQueue

因为一个MessageQueue只能交给一个Consumer处理,所以就可以保证消息的顺序性。

但是这里要注意的是,如果消息处理失败要怎么办,并不想能之前一样 返回RECONSUME_LATER状态,比如insert失败了,然后返回给重试队列,然后再执行update,发现成功了,那么结果还是会出现消息乱序的现象,所以这时候要返回SUSPEND_CURRENT_QUEUE_A_MOMENT状态,过段时间再处理这批消息,也就是堵塞在这,不会让后面的先执行。

代码案例

如何让一个订单的binlog都进入到一个Message,代码如下:
在这里插入图片描述
消费者如何按照顺序获取一个MessageQueue的消息,代码如下:

在这里插入图片描述
这里使用的是MessageListenerOrderly,也就是Consumer对每一个ConsumeQueue,都仅仅用一个线程来处理其中的消息,如果使用多线程,那么还是会出现乱序的。

消息过滤

如果一个Topic里,还有其他不需要的消息,那么就可以对这个Topic进行过滤,在发消息的时候,指定Tag,还可以给消息设置一些属性。代码如下:

在这里插入图片描述
那么消费者在消费消息的时候,就可以根据Tag进行过滤,或者根据消息的属性的值进行过滤,代码如下:

在这里插入图片描述
在这里插入图片描述
其他的过滤语法还有很多,如下:

(1)数值比较,比如:>,>=,<,<=,BETWEEN,=;
(2)字符比较,比如:=,<>,IN;
(3)IS NULL 或者 IS NOT NULL;
(4)逻辑符号 AND,OR,NOT;
(5)数值,比如:123,3.1415;
(6)字符,比如:‘abc’,必须用单引号包裹起来;
(7)NULL,特殊的常量
(8)布尔值,TRUE 或 FALSE

消息延迟

如果在订单支付的页面,下了单,但是迟迟没有支付,那么肯定是需要一段时间后,释放掉这个订单的,然后状态改为取消订单,但是大量的订单消息,一直用几个线程来进行扫描,将订单超时的更新,很消耗性能,所以MQ的延迟消息就可以大量的使用了。

我们可以将消息发送到MQ中,并指定这条消息是延迟消息,要等待多少分钟后,才会被消费者消费到。那么消费者30分钟后看到了这个消息,可以去数据库中查询状态,如果没支付,那么就取消订单,如果支付了,那么就不用管了。

代码案例

生产者:
在这里插入图片描述
其中delayTimeLevel就是延迟级别,延迟级别支持以下:

1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

那么3就是延迟10s的意思。

消费者:

在这里插入图片描述

生产实践经验总结

1.灵活运用Tag来过滤数据

对于订单系统,可以按照家电订单,食物订单等进行过滤筛选

2.基于消息key来定位消息是否丢失

可以对消息设置key,message.setKey(orderId),那么这个消息到Borker就会基于hash索引,存放在indexFile索引文件中,然后我们可以通过MQ提供的命令去根据key查询这个消息:

mqadmin queryMsgByKey -n 127.0.0.1:9876 -t SCANRECORD -k orderId

3.消息零丢失的补充

对于金融系统,都要有超高的高可用保障,一般MQ集群彻底崩溃了,生产者就要把消息写入到本地磁盘持久化,或者写入到数据库,等到MQ恢复了以后,再把持久化消息继续投递到MQ里

4.提高消费者的吞吐量

如果发现消费的比较慢,那么可以提高消费者的并行度,部署更多的Consumer机器,但是Topic的MessageQueue也要对应的增加,如果只有4个MessageQueue,然后有5台Consumer,那么有一台是获取不到消息的。

然后就是可以增加consumer的线程数量,可以设置consumer端的参数:consumeThreadMin、consumeThreadMax,这样一台consumer机器上的消费线程越多,消费的速度就越快。

此外,还可以开启消费者的批量消费功能,就是设置consumeMessageBatchMaxSize参数,他默认是1,但是你可以设置的多一些,那么一次就会交给你的回调函数一批消息给你来处理了,此时你可以通过SQL语句一次性批量处理一些数据,比如:update xxx set xxx where id in (xx,xx,xx)。

5.消费历史消息

其实consumer是支持设置从哪里开始消费消息的,常见的有两种:一个是从Topic的第一条数据开始消费,一个是从最后一次消费过的消息之后开始消费。对应的是:CONSUME_FROM_LAST_OFFSET,CONSUME_FROM_FIRST_OFFSET。

一般都会选择从第一条开始,如果重启之后,就会定位到上一次消费的offset。

消息追踪

对于一条消息的丢失,可能想要了解一个消息轨迹,协助去线上排查,RocketMQ支持消息轨迹功能。

首先在Broker配置文件里开启traceTopicEnable=true,然后这时候Broker就会自动创建出来内部的Topic,RMQ_SYS_TRACE_TOPIC,这个Topic用来存储所有的消息轨迹追踪数据的。

之后在创建生产者和消费者的时候,对第二个参数,enableMesgTrace,设置为true,如下:

在这里插入图片描述
那么Broker,Producer,Consumer都配置好了之后,就可以上报这个消息到内置的RMQ_SYS_TRACE_TOPIC里去,那么在Broker端就会有:消息存储的Topic、消息存储的位置、消息的key、消息的tags。

在Consumer端,他也会上报一些轨迹到内置的RMQ_SYS_TRACE_TOPIC里去,包括如下一些东西:Consumer的信息、投递消息的时间、这是第几轮投递消息、消息消费是否成功、消费这条消息的耗时。

接着我们想要查询消息轨迹,在RocketMQ控制台,导航栏里就有一个消息轨迹,在里面可以创建查询任务,根据messageId,message key或者Topic来查询,查询任务执行完毕之后,就可以看到消息轨迹的界面了,就会显示上面说的数据。

消息积压

如果在消费者端,发生数据库无法写入,导致消费者系统几乎处于停滞不动的状态,那么生产者可能会在高峰期内,一直往里写入消息,积压100多w条消息。

解决方法:1.如果消息是允许丢失,那么可以紧急修改消费者的代码,将消费到的消息直接丢弃,不做任何处理,这样可以让积压的消息全部丢失掉。

2.一般来说都不能直接丢失,最常见的办法就是先等待数据库的恢复,然后根据线上Topic的MessageQueue的数量进行处理

比如Topic有20个MessageQueue,然后只有4个消费者在消费,那么可以多申请16台消费者系统的实例,同时对所有的MessageQueue一起消费,提高消费速度,那么积压的消息很快就可以被处理完,之后在下线多余的16台机器就可以了。

如果这个Topic的MessageQueue只有4个,那么就可以将消费者代码临时更改,写入到其他的Topic,并且此Topic的MessageQueue要多,比如有20个,然后在临时增加20个消费者系统进行消费,提高处理能力。

金融级系统RocketMQ集群崩溃高可用设计

金融级的系统,如果他的MQ集群突然都崩溃了,那么就需要进行降级方案,通常的思路是,需要在你发送消息到MQ代码里去try catch捕获异常,如果你发现发送消息到MQ有异常,此时需要重试,但是重试3次还是失败,那么可能是MQ集群崩溃了,那么就可以将数据写入本地磁盘文件,或者写入数据库,NoSQL存储中去,一旦MQ集群恢复,就要有一个后台线程把之前持久化的消息依次按照顺序发送到MQ集群里去,这里最终要的就是要保证消息的顺序性,持久化的时候就要顺序一个一个持久化写入,之后取出也要一个一个按照顺序取出写入,然后消费。

kafka到RocketMQ的双写 + 双读的方案

首先如果要切换MQ的情况,不能直接简单粗暴全部停机,然后更新代码再全部重新上线,不符合逻辑。

首先做到双写,也就是所有的Producer系统中,引入一个双写的代码,就是同事往kafka和RocketMQ中去写入,持续一周左右,MQ里的数据就是都差不多了,因为里面数据最多就保留一周。

光是双写是不够的,还要进行双读,就是所有的Consumer在消费数据的时候不仅要从kafka里读取数据,也要从RocketMQ中读取数据,分别用一抹一样的逻辑处理一遍。只不过kafka里还是走核心逻辑处理,RocketMQ取到的可以用一样的逻辑处理,但是不能具体的落入数据库之类的地方。

而且Consumer在消息读取的时候,需要统计每个MQ当日读取和处理的消息的数量,这很重要,同时对于RocketMQ读取到的消息处理之后的结果,写入一个临时的存储中。

当你发现持续双写和双读一段时间后,所有的Consumer系统通过对比发现,处理的消息数量一致,同时处理得到的结果也一致,就可以判断当前kafka和RocketMQ里的消息是一致的。

这时候就可以正式切换,停机Producer系统,再重新修改后上限,所有的Consumer系统全部下线修改代码再上限,全部基于RocketMQ来获取消息,计算和处理,结果写入存储中。

RocketMQ思维导图

MQ生产集群部署h在这里插部署入图片描述

订单系统架构改造

在这里插入图片描述

RocketMQ 的底层实现原理

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值