快速入门
1、安装
1)获取 rabbitmq 的镜像
方式一 (直接远程拉去镜像)
docker pull rabbitmq:3-management
方式二 (导入mq.tar)
docker load -i mq.tar
如果丢失镜像名称与版本,执行如下命令
docker tag rabbitmq:3-management 镜像ID
2)启动容器
docker run -d -p 15672:15672 -p 5672:5672 --name mq --hostname mq --restart=always -v mq-plugins:/plugins
-e RABBITMQ_DEFAULT_USER=itcast -e RABBITMQ_DEFAULT_PASS=123321 rabbitmq:3-management
参数说明:
5672: MQ的通信端口
15672:管控台的端口
如果丢失镜像名称与版本,执行如下命令
docker tag rabbitmq:3-management 镜像ID
SpringAMQP使用
1、导入依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、配置文件
spring:
rabbitmq:
host: 192.168.138.100 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: itcast # 用户名
password: 123321 # 密码
Simple
1、声明队列(图形化方式创建)
2、生产者
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* simple
* @throws Exception
*/
@Test
public void simpleTest() throws Exception {
// 这个api必须先声明队列
rabbitTemplate.convertAndSend("","simple.queue","纯爱战神");
}
3、消费者
@Component
public class MessageListener {
//消费完自动签收
@RabbitListener(queues = "simple.queue")
public void simpleConsume(String message){
System.out.println("收到的消息:" + message);
}
}
WorkQueue
1、声明队列(图形化方式创建)
2、生产者
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* workQueue
* @throws Exception
*/
@Test
public void workQueueTest() throws Exception {
for (int i = 0; i < 100; i++) {
rabbitTemplate.convertAndSend("","mySimple.queue", "我想你了+" + i);
}
}
3、消费者 (模拟两个消费者消费水平不一样)
@Component
public class MessageListener {
@RabbitListener(queues = "mySimple.queue")
public void workQueue1Consume(String message){
try {
Thread.sleep(20);
System.out.println("GZR收到:" + message);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@RabbitListener(queues = "mySimple.queue")
public void workQueue2Consume(String message){
try {
Thread.sleep(80);
System.out.println("CYT收到:" + message);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4、配置文件 (让他们按需获取,体现其竞争关系)
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
Fanout
1、声明队列与交换机(配置类方式)
@Configuration
public class FanoutConfig {
// 声明队列
@Bean
public Queue itheimaFanoutQueue1(){
return QueueBuilder.durable("itheima.fanout.queue1").build();
}
@Bean
public Queue itheimaFanoutQueue2(){
return QueueBuilder.durable("itheima.fanout.queue2").build();
}
//声明交换机
@Bean
public FanoutExchange itheimaFanout(){
return ExchangeBuilder.fanoutExchange("itheima.fanout").build();
}
//进行绑定关系
@Bean
public Binding queue3FanoutExchange(Queue itheimaFanoutQueue1, FanoutExchange itheimaFanout){
return BindingBuilder.bind(itheimaFanoutQueue1).to(itheimaFanout);
}
@Bean
public Binding queue4FanoutExchange(Queue itheimaFanoutQueue2,FanoutExchange itheimaFanout){
return BindingBuilder.bind(itheimaFanoutQueue2).to(itheimaFanout);
}
}
2、生产者
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* fanout
* @throws Exception
*/
@Test
public void fanoutTest() throws Exception {
rabbitTemplate.convertAndSend("itcast.fanout","","我想你了");
}
}
3、消费者
@Component
public class MessageListener {
@RabbitListener(queues = {"fanout.queue1"})
public void fanout1Consume(String message){
System.out.println("GZR收到:" + message);
}
@RabbitListener(queues = {"fanout.queue2"})
public void fanout2Consume(String message){
System.out.println("CYT收到:" + message);
}
}
Direct
1、声明队列(注解方式创建)
2、生产者
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* direct
* @throws Exception
*/
@Test
public void directTest() throws Exception {
rabbitTemplate.convertAndSend("itcast.direct","blue","你是我的朱砂痣");
rabbitTemplate.convertAndSend("itcast.direct","yellow","你是我的白月光");
rabbitTemplate.convertAndSend("itcast.direct","red","你是我的遗憾");
}
3、消费者
@Component
public class MessageListener {
@RabbitListener(bindings = {
@QueueBinding(value = @Queue(name = "direct.queue1",durable = "true"),
exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
key = {"blue","red"})
})
public void direct1Consume(String message){
System.out.println("GZR收到:" + message);
}
@RabbitListener(bindings = {
@QueueBinding(value = @Queue(name = "direct.queue2",durable = "true"),
exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
key = {"yellow","red"})
})
public void direct2Consume(String message){
System.out.println("CYT收到:" + message);
}
}
Topic
1、声明队列(注解方式创建)
2、生产者
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* topic
* @throws Exception
*/
@Test
public void topicTest() throws Exception {
rabbitTemplate.convertAndSend("itcast.topic","china.big","你一直都是我的朱砂痣");
rabbitTemplate.convertAndSend("itcast.topic","big.news","你一直都是我的白月光");
rabbitTemplate.convertAndSend("itcast.topic","china.news","你一直都是我的遗憾");
}
3、消费者
@Component
public class MessageListener {
@RabbitListener(bindings = {
@QueueBinding(value = @Queue(name = "topic.queue1",durable = "true"),
exchange = @Exchange(name = "itcast.topic",type = ExchangeTypes.TOPIC),
key = {"china.#"})
})
public void topic1Consume(String message){
System.out.println("GZR收到:" + message);
}
@RabbitListener(bindings = {
@QueueBinding(value = @Queue(name = "topic.queue2",durable = "true"),
exchange = @Exchange(name = "itcast.topic",type = ExchangeTypes.TOPIC),
key = {"#.news"})
})
public void topic2Consume(String message){
System.out.println("CYT收到:" + message);
}
}
消息转换器
1、使用JSON转换器自动转化(消费者与生产者都要导入依赖且配置bean)
1)导入依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.5</version>
</dependency>
2)配置bean
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
3)示例
生产者)
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送对象
* @throws Exception
*/
@Test
public void sendObjectTest() throws Exception {
Map<String,Object> map = new HashMap<>();
map.put("name","桂志蓉");
map.put("age",24);
rabbitTemplate.convertAndSend("","simple.queue",map);
}
2、使用json手动转化(消费者与生产者都要导入依赖)
1)导入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
2)示例
生产者)
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送对象
* @throws Exception
*/
@Test
public void sendObjectTest() throws Exception {
Map<String,Object> map = new HashMap<>();
map.put("name","桂志蓉");
map.put("age",24);
String json = JSONObject.toJSONString(map);
rabbitTemplate.convertAndSend("","simple.queue",json);
}
消息可靠性
生产者确认机制
1、配置文件(生产者中配置)
spring:
rabbitmq:
publisher-confirm-type: correlated
publisher-returns: true
template:
mandatory: true
2、定义ConfirmCallback(ConfirmCallback可以在发送消息时指定,因为每个业务处理confirm成功或失败的逻辑不一定相同)
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
CorrelationData correlationData = new CorrelationData(uuid);
correlationData.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable throwable) {
// 出异常,走这里
log.error("发送信息出问题了:" + throwable.getMessage() + correlationData.getId());
// 记录日志,重发
log.info("出异常,重新发信息---->111");
//rabbitTemplate.convertAndSend("itcast.direct", "blue","GoodsMessage",correlationData);
}
@Override
public void onSuccess(CorrelationData.Confirm confirm) {
//没出异常,走这里
if(confirm.isAck()){
log.warn("消息正确到达交换机---->666" + correlationData.getId());
}else {
log.error("发送失败了,没有到达交换机");
// 记录日志,重发
log.info("发送失败,重新发信息---->222");
//消息发送失败回调的方法,一个消息对应一个
//rabbitTemplate.convertAndSend("itcast.direct", "blue","GoodsMessage111",correlationData);
}
}
});
rabbitTemplate.convertAndSend("itcast.direct", "blue","GoodsMessage",correlationData);
3)定义Return回调(每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目加载时配置)
@Configuration
@Slf4j
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());
// 如果有业务需要,可以重发消息
log.warn("重发消息");
//rabbitTemplate.convertAndSend(exchange,routingKey,message.getBody());
});
}
}
持久化机制
1、交换机持久化
默认就是持久化
2、队列持久化
默认就是持久化
3、消息持久化
默认就是持久化
*在发送消息时,使用Message对象,并设置delivery-mode为持久化
MessageProperties messageProperties = new MessageProperties();
// 设置消息持久化
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
//设置消息的唯一id
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
messageProperties.setMessageId(uuid);
Message message = new Message("GoodsMessage".getBytes(StandardCharsets.UTF_8),messageProperties); rabbitTemplate.send("itcast.direct", "blue", message, correlationData);
消费者ack机制
1、none:只要消息到达消费者,Spring直接返回ack到MQ(一般不使用)
MQ收到ack,会把队列中的消息删除
消息会丢失
消费者配置
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: none # 关闭ack
2、manual:手动ack
消费成功,调用API给MQ返回ack
消费失败,调用API给MQ返回nack,并且让消息重回队列
消费者配置
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual #手动ack
消费者代码
@RabbitListener(bindings = @QueueBinding(
value =@Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
key = {"blue","red"}
))
public void directQueue1(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) Long tag){
try {
log.info("direct.queue1收到的消息: {}",message);
Thread.sleep(2000);
int i = 1/0;
//手动ack
channel.basicAck(tag,false);
} catch (Exception e) {
//e.printStackTrace();
try {
//手动nack,让消息重回队列。参数三表示是否重回队列
channel.basicNack(tag,false,true);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
*3、auto:自动ack。消费消息不出异常,返回ack给MQ。消费消息出异常了,返回nack,把消息重回队列
1)本地重试
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto #自动ack
retry:
enabled: true #开启消费者失败重试
initial-interval: 1000 #初始的失败等待时长为1秒
multiplier: 2 #失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 #最大重试次数
stateless: true #true无状态;false有状态。如果业务中包含事务,这里改为false
2)失败策略
1、RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
2、ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
3、RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
3)使用 RepublishMessageRecoverer
编写配置类
@Configuration
@Slf4j
public class RabbitConfig {
//定义错误队列
@Bean
public Queue errorQueue(){
return QueueBuilder.durable("error.queue").build();
}
//定义错误交换机
@Bean
public DirectExchange errorExchange(){
return ExchangeBuilder.directExchange("error.direct").build();
}
//进行绑定
@Bean
public Binding errorQueueDirect(Queue errorQueue,DirectExchange errorExchange){
return BindingBuilder.bind(errorQueue).to(errorExchange).with("error");
}
//失败策略:重试次数耗尽后,会把消息投递到错误的队列,由错误队列去消费
@Bean
public MessageRecoverer publishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");
}
}
处理错误队列中的消息
@RabbitListener(queues = "error.queue")
public void errorMessage(String msg, Message message){
log.info("error.queue 收到的消息" + msg);
//记录日志,后面人工干预,向运维人员不断发邮件
//可以通过message拿到各种消息
}
保证消息可靠性总结
1、生产者开启confirm机制,保证消息正确到达交换机
2、生产者开启return机制,保证消息正确到达队列
3、交换机、队列、消息进行持久化
4、消费者开启手动ack,或者自动ack + 重试耗尽的失败策略,定义错误交换机队列,后期通过人工进行干预
消息重复消费问题
使用redis做确认机制,防止在非幂等性操作下多次消费信息,造成数据一致性的问题
代码演示
@Autowired
private StringRedisTemplate stringRedisTemplate;
//============手动ack
@RabbitListener(bindings = @QueueBinding(
value =@Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
key = {"blue","red"}
))
public void directQueue1(String mes, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) Long tag, Message message){
try {
log.info("direct.queue1收到的消息: {}",mes);
//获取消息的唯一id
String messageId = message.getMessageProperties().getMessageId();
//0代表这个消息正在消费,1代表这个消息被消费完了,设置过期时间预防死锁的情况
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(messageId, "0", 10, TimeUnit.SECONDS);
if(flag){
//这个消息没有被消费过,进行消费
log.info("directQueue1正在处理业务");
int i = 1/0;
stringRedisTemplate.opsForValue().set(messageId, "1", 10, TimeUnit.SECONDS);
//手动ack
channel.basicAck(tag,false);
}else {
//消息已经被消费过
String value = stringRedisTemplate.opsForValue().get(messageId);
if("1".equals(value)){
//手动ack,1代表这个消息被消费完了,可能还没被签收过,这个签收一下
channel.basicAck(tag,false);
}
// 正在被消费,这个就不做操作
}
} catch (Exception e) {
// e.printStackTrace();
try {
//手动nack,让消息重回队列。参数三表示是否重回队列
channel.basicNack(tag,false,true);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
消息积压问题
生产者生产消费的速度 远高于 消费者消费消息的速度?于是就会造成消息积压在MQ
1、设计有问题
重新设置生产者与消费者数量匹配(如多个生产者、一个消费者)
2、消费者有问题
1)消费者出异常了
解决消费者代码
2)消费者宕机了
第一步:修复宕机的情况
第二步:临时开启多个消费者,来以倍速消费积压的消息。当积压的消息消费的差不多的情况,关闭临时消费者
死信交换机
1、死信
1)消费者使用basic.reject 或 basic.nack声明消费失败,并且消息的requeue参数设置为false
2)消息是一个过期消息,超时无人消费
3)要投递的队列消息满了,无法投递
2、死信交换机
如果这个包含死信的队列配置了 dead-letter-exchange 属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为 死信交换机 (Dead Letter Exchange,检查DLX)。
3、TTL
超时未消费,消息变成死信的两种情况
1)消息所在的队列设置了超时时间
2)消息本身设置了超时时间
如果两者都设置了,以短的时间为优先
4、延迟【时】队列(一种实现 消费者延迟收到消息 的模式)
在RabbitMQ中,没有延迟队列的功能。可以使用 TTL + 死信队列 的方式实现延迟队列
1)声明死信交换机和死信队列
//声明死信交换机跟死信队列
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "dl.ttl.queue"),
exchange = @Exchange(name = "dl.ttl.direct", type = ExchangeTypes.DIRECT),
key = {"ttl"}
))
public void dlTtlQueueMessage(String message) {
log.info("死信队列dl.ttl.queue收到的消息: {}", message);
//订单超时未处理代码
}
2)声明普通交换机和普通队列、绑定关系
@Configuration
public class TtlQueueConfig {
//普通队列:这个队列的消息是无人消费,到达超时时间,才会变成死信,路由死信交换机,最终到达死信队列
@Bean
public Queue ttlQueue(){
return QueueBuilder.durable("ttl.queue")
.deadLetterExchange("dl.ttl.direct")//死信交换机
.deadLetterRoutingKey("ttl")//死信路由key
.ttl(5000)//死信过期时间
.build();
}
//普通交换机
@Bean
public DirectExchange ttlDirect(){
return new DirectExchange("ttl.direct");
}
@Bean
public Binding ttlQueueTtlDirect(Queue ttlQueue,DirectExchange ttlDirect){
return BindingBuilder.bind(ttlQueue).to(ttlDirect).with("ttl");
}
}
3)发消息
不指定消息过期时间
public void dalayTest() throws Exception {
MessageProperties properties = new MessageProperties();
Message message = new Message("goodBoy".getBytes(StandardCharsets.UTF_8),properties );
rabbitTemplate.convertAndSend("ttl.direct", "ttl", message);
}
指定消息过期时间
public void dalayTest() throws Exception {
MessageProperties properties = new MessageProperties();
//设置消息过期时间
properties.setExpiration("3000");
Message message = new Message("goodBoy".getBytes(StandardCharsets.UTF_8),properties );
rabbitTemplate.convertAndSend("ttl.direct", "ttl", message);
}
5、DelayExchange插件(实现延时队列)
官网
https://blog.rabbitmq.com/posts/2015/04/scheduling-messages-with-rabbitmq
1)安装
1)把插件拖动到服务器中mq插件目录
docker volume inspect 数据卷名称 (进入放插件的地方,再将插件放进去)
2)开启插件
2.1进入容器内部
docker exec -it mq bash
2.2应用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
3)原理
1、DelayExchange需要将一个交换机声明为delayed类型
2、发送消息到delayExchange时的执行过程
1、接收消息
2、判断消息是否具备x-delay属性
3、如果有x-delay属性,说明是延迟消息,持久化到硬盘,读取x-delay值,作为延迟时间
4、返回routing not found结果给消息发送者
5、x-delay时间到期后,重新投递消息到指定队列
4)实现
1、声明DelayExchange交换机
注解方式【推荐】
//使用插件完成延时队列
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue"),
exchange = @Exchange(name = "delay.direct", type = ExchangeTypes.DIRECT,delayed = "true"),
key = {"delay"}
))
public void delayQueueMessage(String message) {
log.info("延时队列delay.queue收到的消息: {}", message);
//订单超时未处理代码
}
2、生产者发送消息
@Test
public void delayPluginTest() throws Exception {
MessageProperties properties = new MessageProperties();
//设置消息过期时间
properties.setHeader("x-delay",8000);
Message message = new Message("PluginBoy".getBytes(StandardCharsets.UTF_8),properties);
rabbitTemplate.convertAndSend("delay.direct", "delay", message);
}