<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
spring:
rabbitmq:
host: 192.168.150.128 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: root # 用户名
password: 123456 # 密码
@Component
@Slf4j
public class SpringRabbitListener {
/**
* 简单消息队列监听
*/
@RabbitListener(queuesToDeclare = @Queue("normal.queue"))
public void normalRabbitListener(String msg){
log.info("普通消息队列收到消息:{}",msg);
}
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAMQPTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void publish() {
String queueName = "normal.queue";
String msg = "HelloWorld";
rabbitTemplate.convertAndSend(queueName, msg);
}
}
模型:多个消费者同时绑定同一个消息队列,但每个消费者消费能力不同,默认条件下消息按照轮询的规则公平分配到各消费者,这样就会因为消费者的性能差异导致消息处理速度变慢。
/**
* 工作队列 1,处理 1 条消息需要 50 ms
*/
@RabbitListener(queuesToDeclare = @Queue("work.queue"))
public void workRabbitListener1(String msg) throws InterruptedException {
log.info("工作队列监听 1 收到消息:{}",msg);
Thread.sleep(50);
}
/**
* 工作队列 2,处理 1 条消息需要 1000 ms
*/
@RabbitListener(queuesToDeclare = @Queue("work.queue"))
public void workRabbitListener2(String msg) throws InterruptedException {
log.info("工作队列监听 2 收到消息:{}",msg);
Thread.sleep(1000);
}
所以要解决此现象,就要改变消息分配机制(预取机制),做到每次只取一条消息,消息处理完成后才可继续获取消息。以实现能者多劳
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
消息到达广播交换机后,会路由给所有与之绑定的消息队列。
1.1 监听器
/**
* 声明消息队列,与广播交换机绑定,两个消息队列都能收到消息
* @param msg
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue("fanout.queue1"),
exchange = @Exchange(name = "fanout.exchange", type = ExchangeTypes.FANOUT)
))
public void fanoutExchangeListener1(String msg){
log.info("广播消息队列1收到消息:{}",msg);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue("fanout.queue2"),
exchange = @Exchange(name = "fanout.exchange", type = ExchangeTypes.FANOUT)
))
public void fanoutExchangeListener2(String msg){
log.info("广播消息队列2收到消息:{}",msg);
}
1.2 消息发送
/**
* fanout交换机消息发送
*/
@Test
public void fanoutMsg(){
String exchange = "fanout.exchange";
String msg = "helloworld";
rabbitTemplate.convertAndSend(exchange,null,msg); //第二个参数为routingKey,在广播交换机中没有作用,可以随便写
}
相比fanout交换机,增加了对routingKey的处理,在绑定交换机和队列的时候声明其routingKey,交换机会根据消息发送时指定的routingKey将消息路由到指定的消息队列。
2.1 消息监听
/**
* 消息队列1:key为 "red","yellow"
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue("direct.queue1"),
exchange = @Exchange(name = "direct.exchange", type = ExchangeTypes.DIRECT),
key = {"red","yellow"}
))
public void directExchangeListener1(String msg){
log.info("广播消息队列1收到消息:{}",msg);
}
/**
* 消息队列2:key为 "green"
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue("direct.queue2"),
exchange = @Exchange(name = "direct.exchange", type = ExchangeTypes.DIRECT),
key = {"green"}
))
public void directExchangeListener2(String msg){
log.info("广播消息队列2收到消息:{}",msg);
}
/**
* 消息队列3:key为 "yellow","green
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue("direct.queue3"),
exchange = @Exchange(name = "direct.exchange", type = ExchangeTypes.DIRECT),
key = {"yellow","green"}
))
public void directExchangeListener3(String msg){
log.info("广播消息队列3收到消息:{}",msg);
}
2.2 消息发送
/**
* direct交换机消息发送
*/
@Test
public void directMsg(){
String exchange = "direct.exchange";
String msg = "helloworld";
String routingKey = "red";
rabbitTemplate.convertAndSend(exchange,routingKey,msg);
}
相当于对direct交换机的加强,routingKey支持通配符:
Routingkey
一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如:item.insert
通配符规则:
#
:匹配0个或多个词
*
:匹配不多不少恰好1个词举例:
item.#
:能够匹配item.spu.insert
或者item.spu
或者item
item.*
:只能匹配item.spu
3.1 消息监听
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenTopicQueue1(String msg){
System.out.println("消费者接收到topic.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenTopicQueue2(String msg){
System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】");
}
3.2 消息发送
/**
* topicExchange
*/
@Test
public void testSendTopicExchange() {
// 交换机名称
String exchangeName = "itcast.topic";
// 消息
String message = "喜报!孙悟空大战哥斯拉,胜!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}
RabbitTemplate默认消息转换器是将消息通过 JDK 进行序列化,但是这种方式消息体积大、可读性差且有安全漏洞。
通过配置更合适的消息转换器,可以实现向消息队列发送序列化对象
<dependency>
<groupId>com.fasterxml.jackson.dataformatgroupId>
<artifactId>jackson-dataformat-xmlartifactId>
<version>2.9.10version>
dependency>
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启publisher-confirm 并设置确认模式为异步回调
publisher-returns: true # 开启publish-return功能
template:
mandatory: true # 消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息
每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目加载时配置
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取RabbitTemplate
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
// 设置ReturnCallback
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
// 投递失败,记录日志
log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
replyCode, replyText, exchange, routingKey, message.toString());
// 如果有业务需要,可以重发消息
});
}
}
@Test
public void testSendMessage2SimpleQueue() throws InterruptedException {
// 1.消息体
String message = "hello, spring amqp!";
// 2.全局唯一的消息ID,需要封装到CorrelationData中
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
// 3.添加callback
correlationData.getFuture().addCallback(
result -> {
if(result.isAck()){
// 3.1.ack,消息成功
log.debug("消息发送成功, ID:{}", correlationData.getId());
}else{
// 3.2.nack,消息失败
log.error("消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
}
},
ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
);
// 4.发送消息
rabbitTemplate.convertAndSend("camq.topic", "simple", message, correlationData);
// 休眠一会儿,等待ack回执
Thread.sleep(2000);
}
none模式下,消息投递是不可靠的,可能丢失
auto模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack
manual:自己根据业务情况,判断什么时候该ack
开启消费者消息确认且设置模式为 **auto**,只有当消息被成功处理完成后,返回ack,消息队列的消息才会被删除,当消息处理过程出现异常,则会返回unack,队列中的消息状态**重新回到ready状态**(也叫requeue),重新发给消费者。
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto # 开启自动消息确认模式
缺点:当消费者出现异常后,消息会不断requeue(重入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力。因此为了避免徒增服务器压力,可以设置本地失败重试。
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000 # 初识的失败等待时长为1秒
multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecovery接口来处理,它包含三种不同的实现:
将重试失败策略设置为RepublishMessageRecoverer:
1)在consumer服务中定义处理失败消息的交换机和队列
@Bean
public DirectExchange errorMessageExchange(){
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue(){
return new Queue("error.queue", true);
}
@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
return BindingBuilder.bind(errorQueue()).to(errorMessageExchange()).with("error");
}
2)定义一个RepublishMessageRecoverer,关联队列和交换机
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
注意:失败消息是由消费者发送到交换机的!!!
17:35:35:490 WARN 38180 — [ntContainer#0-1] o.s.a.r.retry.RepublishMessageRecoverer : Republishing failed message to exchange ‘error.direct’ with routing key error
在consumer服务中,定义一组死信交换机、死信队列:
@Configuration
public class CommonConfig {
//声明普通消息队列,指定死信交换机
@Bean
public Queue simpleQueue(){
return QueueBuilder.durable("simple.queue")
.deadLetterExchange("dl.exchange")
.deadLetterRoutingKey("deadLetter")
.build();
}
//声明死信交换机
@Bean
public DirectExchange dlExchange(){
return new DirectExchange("dl.exchange", true, false);
}
//声明死信队列
@Bean
public Queue dlQueue(){
return new Queue("dl.queue", true);
}
//绑定死信交换机和死信队列
@Bean
public Binding dlBinding(){
return BindingBuilder.bind(dlQueue()).to(dlExchange()).with("deadLetter");
}
}
手动制造普通消息队列消息消费异常
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(String msg) {
System.out.println("消费者接收到simple.queue的消息:【" + msg + "】");
System.out.println(1/0);
}
@RabbitListener(queues = "dl.queue")
public void listenErrorQueue(String msg) {
System.out.println("死信队列dl.queue的消息:【" + msg + "】");
}
}
测试:直接向消息队列发送消息
@Test
public void testSendMessage2SimpleQueue() throws InterruptedException {
// 1.消息体
String message = "hello, spring amqp!";
// 2.全局唯一的消息ID,需要封装到CorrelationData中
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
// 3.添加callback
correlationData.getFuture().addCallback(
result -> {
if(result.isAck()){
// 3.1.ack,消息成功
log.debug("消息发送成功, ID:{}", correlationData.getId());
}else{
// 3.2.nack,消息失败
log.error("消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
}
},
ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
);
// 4.发送消息
rabbitTemplate.convertAndSend("simple.queue", message);
// 休眠一会儿,等待ack回执
Thread.sleep(2000);
}
@Configuration
public class CommonConfig {
//ttl消息队列,队列有效期 5秒
@Bean
public Queue ttlQueue(){
return QueueBuilder.durable("ttl.queue")
.ttl(5000)
.deadLetterExchange("dl.exchange")
.deadLetterRoutingKey("deadLetter")
.build();
}
//声明死信交换机
//声明死信队列
//绑定死信交换机和死信队列
}
发送消息时,也可以指定消息的有效时间,最终变成死信以消息和队列的最短时间为准,注意要看到效果,ttl队列中的消息不要被消费
Message message = MessageBuilder
.withBody("hello, ttl message".getBytes(StandardCharsets.UTF_8))
.setExpiration("2000")
.build();
DelayExchange需要将一个交换机声明为delayed类型。当我们发送消息到delayExchange时,流程如下:
1.安装Delay消息插件
2.声明Delay交换机和队列
//Delay交换机
@Bean
public DirectExchange delayExchange(){
return ExchangeBuilder
.directExchange("delay.exchange")
.delayed() //声明为 Delay交换机
.durable(true)
.build();
}
//Delay消息队列
@Bean
public Queue delayQueue(){
return new Queue("delay.queue");
}
//绑定 delay消息队列和 delay交换机
@Bean
public Binding delayBinding(){
return BindingBuilder.bind(delayQueue()).to(delayExchange()).with("delay");
}
3.消息发送时携带 X-Delay 消息头才能被delay交换机处理
Message message = MessageBuilder
.withBody("hello, ttl message".getBytes(StandardCharsets.UTF_8))
.setHeader("x-delay",2000)
.build();
解决大量数据堆积问题的两个方向:1.增加消费者个数 2.扩大队列容量。
从RabbitMQ的3.6.0版本开始,增加了惰性队列的概念,队列接收到的消息直接存入硬盘,消费者读取消息前先从硬盘加载到内存;硬盘存储成本低空间大,可以存储百万级数据。
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues
命令解读:
rabbitmqctl
:RabbitMQ的命令行工具set_policy
:添加一个策略Lazy
:策略名称,可以自定义"^lazy-queue$"
:用正则表达式匹配队列的名字'{"queue-mode":"lazy"}'
:设置队列模式为lazy模式--apply-to queues
:策略的作用对象,是所有的队列//Delay消息队列
@Bean
public Queue lazyQueue(){
return QueueBuilder
.durable("lazy.queue")
.lazy() //开启 x-queue-mode 为 lazy模式
.build();
}
@RabbitListener(queuesToDeclare = @Queue(
name = "lazy.queue",
durable = "true",
arguments = @Argument(name = "x-queue-mode", value = "lazy")
))
public void listenLazyQueue(String msg) {
System.out.println("消费者接收到lazy.queue的消息:【" + msg + "】");
}
普通集群,或者叫标准集群(classic cluster),具备下列特征:
镜像集群:本质是主从模式,具备下面的特征:
仲裁队列:仲裁队列是3.8版本以后才有的新功能,用来替代镜像队列,具备下列特征:
1.创建仲裁队列
@Bean
public Queue quorumQueue() {
return QueueBuilder
.durable("quorum.queue") // 持久化
.quorum() // 仲裁队列
.build();
}
2.SpringAMQP连接MQ集群
注意,这里用address来代替host、port方式
spring:
rabbitmq:
addresses: 192.168.150.105:8071, 192.168.150.105:8072, 192.168.150.105:8073
username: itcast
password: 123321
virtual-host: /