MQ的优势
应用解耦 异步提速 消峰填谷
JMS 是java的一种规范 (java message service)
AMQP协议:一种高级消息队列的协议
RabbitMQ的安装与配置
第一种方式: Docker (推荐)
1 拉取镜像
docker pull rabbitmq:3.8-management
2 运行容器
docker run \
-e RABBITMQ_DEFAULT_USER=root \
-e RABBITMQ_DEFAULT_PASS=root \
-v mq-plugins:/plugins \
--name mq \
--hostname mq \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3.8-management
3 访问rabbitmq所在的主机的15672端口就可以进入管理界面
第二种方式: 安装包安装(麻烦,需要的自行百度)
SpringBoot整合RabbitMQ
1 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2 添加配置
spring:
rabbitmq:
port: 5672
username: root
password: root
virtual-host: /
host: 192.168.200.10
hello world (需要到管理界面先创建队列,demo.queue1)
不管发送消息还是接收消息,都需要先导入依赖 ,指定mq的地址
发消息:
1 注入RabbitTemplate
2 使用convertAndSend方法发送
第一个参数 队列名
第二个参数 消息的内容
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void sendMsg() {
rabbitTemplate.convertAndSend("demo.queue1","123");
}
接收消息 ,创建一个web项目
1 新建一个监听器类,并交由spring核心容器管理
2 新建一个方法用来接收消息。使用@RabbitListener
3 运行项目
@Slf4j
@Component
public class DemoListener {
@RabbitListener(queues = "demo.queulle1")
public void consumer(String msg) {
log.info("msg ===========> {}",msg);
}
}
工作队列案例 (多个消费者一起消费队列中的消息)
需要添加配置
spring:
rabbitmq:
port: 5672
username: root
password: root
virtual-host: /
host: 192.168.200.10
listener:
simple:
prefetch: 1 #让每个消费者,只有消费完才可以继续取消息
消费者:
@RabbitListener(queues = "work.queue")
public void listenWorkQueue1(String msg) throws InterruptedException {
log.info("listener1 ===========> {} {}",msg, LocalTime.now());
Thread.sleep(20);
}
@RabbitListener(queues = "work.queue")
public void listenWorkQueue2(String msg) throws InterruptedException {
log.error("listener2 ===========> {} {}",msg,LocalTime.now());
Thread.sleep(200);
}
生产者:
@Test
public void workQueue() throws InterruptedException {
for (int i = 1; i <= 50; i++) {
rabbitTemplate.convertAndSend("work.queue","workQueue " + i);
Thread.sleep(20);
}
}
发布订阅案例
简单得案例,跟工作队列模型都是一条消息只能有一个消费者消费,消费了就没了。
发布订阅模型是可以同时发送给多个消费者。实现方式是加入了路由器。
常见的exchange有:
fanout (广播) 会发送给所有跟这个路由器绑定的队列
direct (路由) 发送给绑定为key的队列。
topic (主题) 比路由模式多了通配符
fanout 代码实现
发送消息:
@Test
public void demoFanout() throws InterruptedException {
rabbitTemplate.convertAndSend("demo.fanout","","hello every one");
}
接收消息:
@RabbitListener(bindings = @QueueBinding(
value = @Queue("fanout.queue1"),
exchange = @Exchange(name = "demo.fanout",type = ExchangeTypes.FANOUT)
))
public void listenDemoQueue1(String msg) {
log.info("queue1 ===========> {}",msg);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue("fanout.queue2"),
exchange = @Exchange(name = "demo.fanout",type = ExchangeTypes.FANOUT)
))
public void listenDemoQueue2(String msg) {
log.info("queue2 ===========> {}",msg);
}
direct 代码实现
发送消息:
@Test
public void demoDirect() throws InterruptedException {
//rabbitTemplate.convertAndSend("demo.direct","red","hello ,how are you");
//rabbitTemplate.convertAndSend("demo.direct","blue","hello ,how are you");
rabbitTemplate.convertAndSend("demo.direct","all","hello ,how are you");
}
接收消息:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "demo.direct",type = ExchangeTypes.DIRECT),
key = {"blue","all"}
))
public void listenDemoQueue1(String msg) {
log.info("bule,all ===========> {}",msg);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue("direct.queue2"),
exchange = @Exchange(name = "demo.direct",type = ExchangeTypes.DIRECT),
key = {"red","all"}
))
public void listenDemoQueue2(String msg) {
log.info("red,all ===========> {}",msg);
}
Topic (跟direct 相比,多了通配符)
消费者:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "demo.topic",type = ExchangeTypes.TOPIC),
key = {"china.#"}
))
public void listenTopicQueue1(String msg) {
log.info("china ===========> {}",msg);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue("topic.queue2"),
exchange = @Exchange(name = "demo.topic",type = ExchangeTypes.TOPIC),
key = {"#.news.#"}
))
public void listenDemoQueue2(String msg) {
log.info("news ===========> {}",msg);
}
生产者:
@Test
public void demoTopic() throws InterruptedException {
//rabbitTemplate.convertAndSend("demo.topic","china.news.test","china news");
//rabbitTemplate.convertAndSend("demo.topic","news.test","china news");
rabbitTemplate.convertAndSend("demo.topic","china.test","china news");
}
消息转换器
底层的对象序列化是使用jdk的默认的。
我们不用jdk的默认,用json格式
实现步骤:
1 导入依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
2 定义bean
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
消费者跟生产序列器需要使用一样的
MQ的常见问题与解决方式
1 - 消息可靠性 确保发送的消息至少被消费一次
2 - 延迟消息问题 如何实现消息的延迟投递
3 - 消息堆积问题 如何解决百万消息的堆积,无法及时消费的问题
4 - 高可用问题 搭建集群
消息可靠性问题
可能丢失:
1 解决方法:发送者确认机制
发送者 -> exchange
exchange -> queue
2 解决方法: 消息持久化
消息到达queue,然后宕机了,mq是基于内存存储,启动的时候消息丢失了。
3 解决方案 : 消费者确认机制
消费者拿到消息未及时消费 宕机了
解决生产者到队列之间可能存在丢失的问题 -- 生产者确认机制
RabbitMQ提供了一种publisher confirm机制来避免消息发送到MQ的过程中丢失。
publisher-comfirm 发送者确认
当消息投递到交换机,返回ack
当消息未投递到交换机,返回nack
publisher-return 发送者回执
当消息到达交换机但未到达队列,就会在返回ack的同时返回路由到队列失败的原因。
SpringAMQP实现生产者确认机制的实现:
1 添加配置 需要先开启异步方式、开启回执功能、开启路由失败的策略
spring:
rabbitmq:
port: 5672
username: root
password: root
virtual-host: /
host: 192.168.200.10
publisher-confirm-type: correlated #异步方式
publisher-returns: true #开启publish-return功能
template:
mandatory: true #开启路由失败时的策略,false是直接丢弃信息
2 配置ReturnCallback 每个RabbitTemplate只能配置一个
我们要实现当spring核心容器创建完之后对RabbitTemplate设置ReturnCallback。
@Slf4j
@Configuration
public class RabbitMQConfig implements ApplicationContextAware {
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
rabbitTemplate.setReturnCallback((message, errCode, errInfo, exchange, key) -> log.error("消息发送失败,{} 原因: {} exchange: {} key: {} 消息内容: {} ",errCode,errInfo,exchange,key,message.toString()));
}
}
3 发送消息时配置ComfirmCallback
@Slf4j
@SpringBootTest(classes = PublishApplication.class)
@RunWith(SpringRunner.class)
public class AdvanceDemoRabbitMQTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendMsgAndComfirmCallback() {
CorrelationData correlationData = new CorrelationData(IdUtil.simpleUUID());
String id = correlationData.getId();
correlationData.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable throwable) {
log.error("id: {} 消息发送异常,msg : {}",throwable.getMessage());
}
@Override
public void onSuccess(CorrelationData.Confirm confirm) {
if(confirm.isAck()) {
log.info("id: {} 消息发送成功!",id);
}else {
log.error("id: {} 消息发送失败 msg: {}",id,confirm.getReason());
}
}
});
rabbitTemplate.convertAndSend("demo.exchange","","hello!",correlationData);
}
}
消息持久化
消息发送到mq之后,如果mq宕机,那么消息会丢失。所以需要让mq的消息持久化
使用SpringAMQP默认交换机跟队列还有消息都是持久化的。
交换机跟队列的配置
//durable = "true" 持久化,autoDelete = "false" 不自动删除
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "demo.queue",durable = "true",autoDelete = "false"),
exchange = @Exchange(name = "demo.exchange",type = ExchangeTypes.FANOUT,durable = "true",autoDelete = "false")
))
消息的发送默认也是持久化
消费者确认机制
RabbitMQ支持消费者确认机制,当消费者成功处理消息之后会向RabbitMQ发送ack回执,MQ收到ack回执才会删除该条消息。
SpringAMQP允许配置三种模式:
none : 关闭ack,MQ默认消息发送出去就是被处理了,会直接删除这条消息。
manual : 手动ack,需要在消息消费之后手动调用api发送ack给MQ。
auto : 自动ack , 在消费消息的代码中如果没有出现异常,代码结束之后就会自动向MQ发送ack,如果出现异常即出现nack。(推荐使用)
配置方法:
spring:
rabbitmq:
port: 5672
username: root
password: root
virtual-host: /
host: 192.168.200.10
listener:
simple:
prefetch: 1
acknowledge-mode: auto #自动ack
失败重试机制
如果代码本身有问题,消费者确认机制会导致代码陷入死循环,给MQ带来很多压力。
解决方式:可以利用Spring的retry机制,在本地进行重试。
使用方式:
1 配置 开启retry、配置每次失败的等待时间、最多重试次数等
spring:
rabbitmq:
port: 5672
username: root
password: root
virtual-host: /
host: 192.168.200.10
listener:
simple:
prefetch: 1
acknowledge-mode: manual #手动ack
retry:
enabled: true #开启retry
initial-interval: 1000 #初始的失败等待时间
multiplier: 1 #计算下次失败的等待时间
max-attempts: 3 #最多重试次数
stateless: true #true 状态 false 有状态 ,如果业务中包含事务,那这边改成false
使用RabbitMQ实现延迟任务
死信交换机
成为死信的三种情况:
1 被消费者返回nack或则reject。
2 消息超时未消费。
3 队列满了,最早进入队列的消息也会变成死信。
给队列绑定死信交换机:
TTL
time-to-live : 超时时间
ttl 队列跟消息都可以设置,设置在队列上从消息到达队列开始进行计时,当计时结束消息还未被消费就会变成死信,设置在消息上从消息发送出的那一个开始倒计时,如果时间到还未被消,也会变成死信。
基于死信交换机跟TTL实现延迟:
1 - 需要先给队列指定死信交换机,路由key,设置ttl超时时间。
//==========================配置队列超时时间==============================
@Bean
public DirectExchange ttlExchange() {
return new DirectExchange("ttl.direct");
}
@Bean
public Queue ttlQueue() {
return QueueBuilder.durable("ttl.queue")
.deadLetterExchange("dl.direct")
.deadLetterRoutingKey("dl")
.ttl(10_000)
.build();
}
@Bean
public Binding ttlBinding() {
return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with("ttl");
}
//======================================================================
安装DelayExchange插件
链接:https://pan.baidu.com/s/1wVl3lFKdQJ64UpbTUd7BUg?pwd=3okb
提取码:3okb
上传插件:
docker volume inspect mq-plugins
查看数据卷位置
将插件上传到这个数据卷的目录
安装插件:
docker exec -it mq bash
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
使用插件:
在管理界面创建一个交换机。
发送消息的时候指定x-delay
SpringAMQP使用延迟队列插件:
消费者:主要多了设置交换机属性 delayed = "true"
@Slf4j
@Component
public class DelayListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue",durable = "true"),
exchange = @Exchange(name = "delay.direct",delayed = "true"),
key = "delay"
))
public void listenDelayQueue(String msg) {
log.info(msg);
}
}
发消息: 主要多了设置Message的头 setHeader("x-delay", "5000")
@Test
public void testSendDelay() {
Message msg = MessageBuilder.withBody("helloDelay".getBytes()).setDeliveryMode(MessageDeliveryMode.PERSISTENT).setHeader("x-delay", "5000").build();
CorrelationData correlationData = new CorrelationData(IdUtil.simpleUUID());
correlationData.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable throwable) {
}
@Override
public void onSuccess(CorrelationData.Confirm confirm) {
if(confirm.isAck()) {
log.info("{} 发送成功",correlationData.getId());
}
}
});
rabbitTemplate.convertAndSend("delay.direct","delay",msg,correlationData);
}
消息堆积问题
消息堆积问题: 当消费者的消费速度小于生生产者的生产速度,就有可能出现消息堆积问题。
1 - 增加消费者。
2 - 提高消费者的性能(使用多线程)。
3 - 增加队列的容量,提高堆积的上限。
惰性队列
特征:
1 - 接收到消息直接写入磁盘,而非内存。
2 - 消费者需要消费消息才会从磁盘中读取并加载内存
3 - 支持百万条的消息存储。
优点:
1 - 存储的消息容量大。
2 - 没有间接性的page-out,性能比较稳定
缺点:
1 - 基于磁盘存储,消息的读写时间较长.
2 - 性能受限于磁盘IO。
惰性队列的开启方式
在运行中的队列改成惰性队列
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues
使用SpringAMQP声明多线队列的俩种方式:
@Bean的方式
@Bean
public Queue lazyQueue() {
return QueueBuilder.durable("queue").lazy().build();
}
注解方式:
@RabbitListener(bindings = @QueueBinding(
value = @Queue("lazy.queue"),
exchange = @Exchange(name = "lazy.direct",type = ExchangeTypes.DIRECT),
key = "lazy",
arguments = @Argument(name = "x-queue-mode",value = "lazy")
))
public void listenLazyQueue(String msg) {
log.info(msg);
}
高可用问题 -- MQ集群
普通集群
分布式集群,将队列分布到各个节点,从而提升整个集群的功能。
在普通集群中交换机的资源是共享的,还有队列的元信息(相当于应用了队列的地址)
实现方式: 以目录/usr/local/mq为例
1 - 获取cookie(集群中MQ传递消息就是通过cookie)
docker exec -it mq cat /var/lib/rabbitmq/.erlang.cookie #返回一个cookie
2 - 准备集群配置
2 - 1 rabbitmq.conf
loopback_users.guest = false
listeners.tcp.default = 5672
cluster_formation.peer_discovery_backend = rabbit_peer_discovery_classic_config
cluster_formation.classic_config.nodes.1 = rabbit@mq1
cluster_formation.classic_config.nodes.2 = rabbit@mq2
cluster_formation.classic_config.nodes.3 = rabbit@mq3
2 - 2 创建一个文件,记录获取到的cookie,并设置权限为只读。
# 创建cookie文件
touch .erlang.cookie
# 写入cookie
echo "获取到的cooike" > .erlang.cookie
# 修改cookie文件的权限
chmod 600 .erlang.cookie
3 - 创建集群每个mq的目录,并将配置文件以及cookie文件拷贝进去。
mkdir mq1 mq2 mq3
cp rabbitmq.conf mq1
cp rabbitmq.conf mq2
cp rabbitmq.conf mq3
cp .erlang.cookie mq1
cp .erlang.cookie mq2
cp .erlang.cookie mq3
4 - 启动集群
创建网络:
docker network create mq-net
运行容器:
docker run -d --net mq-net \
-v ${PWD}/mq1/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=root \
-e RABBITMQ_DEFAULT_PASS=root \
--name mq1 \
--hostname mq1 \
-p 8071:5672 \
-p 8081:15672 \
rabbitmq:3.8-management
docker run -d --net mq-net \
-v ${PWD}/mq2/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=root \
-e RABBITMQ_DEFAULT_PASS=root \
--name mq2 \
--hostname mq2 \
-p 8072:5672 \
-p 8082:15672 \
rabbitmq:3.8-management
docker run -d --net mq-net \
-v ${PWD}/mq3/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=root \
-e RABBITMQ_DEFAULT_PASS=root \
--name mq3 \
--hostname mq3 \
-p 8073:5672 \
-p 8083:15672 \
rabbitmq:3.8-management
镜像集群
官方文档地址:
Classic Queue Mirroring — RabbitMQ
主从集群普通集群的基础上,添加了主从备份功能(也就是队列也共享),提高集群的数据可用性。
但是主从同步存在丢失数据的可能。
镜像集群是在普通集群的基础上去加配置。
镜像配置的三种模式
exactly 精确模式(推荐) ha-params的值是等于镜像节点加主节点
rabbitmqctl set_policy ha-two "^two\." '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'
"^two\." 这个策略的意思是以two开头的队列都会创建一个镜像节点。
all (无需指定全部都是主节点的镜像节点)
rabbitmqctl set_policy ha-all "^all\." '{"ha-mode":"all"}'
nodes (需要指定镜像节点)
rabbitmqctl set_policy ha-nodes "^nodes\." '{"ha-mode":"nodes","ha-params":["rabbit@nodeA", "rabbit@nodeB"]}'
仲裁队列(推荐使用)
底层采用Raft协议确保主从数据的一致性。用来替换镜像队列。
主从同步效果跟镜像队列一致。(配置简单)
但是主从同步是基于Raft协议,强一致性。
实现方式:
使用SpringAMQP创建一个仲裁队列
@Bean
public Queue quorumQueue() {
return QueueBuilder.durable("quorum.queue").quorum().build();
}
使用java连接MQ集群
spring:
rabbitmq:
port: 5672
username: root
password: root
virtual-host: /
addresses: 192.168.200.10:8071,192.168.200.10:8072,192.168.200.10:8073
publisher-confirm-type: correlated #异步方式等待MQ返回,并执行confirmCallback
publisher-returns: true #开启publisher-returns
template:
mandatory: true #开启失败策略,当消息发送失败就会接收回执,并执行处理代码
集群扩容
1)启动一个新的MQ容器:
docker run -d --net mq-net \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=itcast \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq4 \
--hostname mq5 \
-p 8074:15672 \
-p 8084:15672 \
rabbitmq:3.8-management
2)进入容器控制台:
docker exec -it mq4 bash
3)停止mq进程
rabbitmqctl stop_app
4)重置RabbitMQ中的数据:
rabbitmqctl reset
5)加入mq1:
rabbitmqctl join_cluster rabbit@mq1
6)再次启动mq进程
rabbitmqctl start_app
增加仲裁队列副本
我们先查看下quorum.queue这个队列目前的副本情况,进入mq1容器:
docker exec -it mq1 bash
执行命令:
rabbitmq-queues quorum_status "quorum.queue"
结果:
现在,我们让mq4也加入进来:
rabbitmq-queues add_member "quorum.queue" "rabbit@mq4"
结果:
再次查看:
rabbitmq-queues quorum_status "quorum.queue"
查看控制台,发现quorum.queue的镜像数量也从原来的 +2 变成了 +3: