一、和Springboot整合
1、引入 pom 依赖:
<!--rabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>1.5.17.RELEASE</version>
</dependency>
2、配置 yml 文件
spring:
#消息队列配置
rabbitmq:
host: www.anbaba.wang
port: 5672
username: ${userName}
password: ${password}
virtual-host: /
二、代码
1、创建队列和交换机
1.1 创建配置文件 RabbitConfig.java
@Configuration
public class RabbitConfig{
private final String PAY_LOG = "queue.pay.log";
private final String GOODS_LOG = "queue.goods.log";
private final String PAY_MESSAGE = "queue.pay.message";
/**
* 创建队列
* @return
*/
@Bean
public Queue helloQueue() {
Map<String, Object> args = new HashMap<>();
//设置队列消息过期时间为10秒
args.put("x-message-ttl", 10 * 1000);
//设置死信交换机
args.put("x-dead-letter-exchange", "directExchange");
//设置死信交换机绑定的routeKey
args.put("x-dead-letter-routing-key", "direct");
//durable 是否持久化,exclusive 是否排他,autoDelete 当所有消费客户端连接断开后,是否自动删除
return new Queue("hello", true, false, false, args);
}
@Bean
public Queue directQueue() {
return new Queue("direct");
}
/**
* DirectExchange:直连交换机
*/
@Bean
DirectExchange directExchange() {
return new DirectExchange("directExchange", false, false);
}
/**
* FanoutExchange:扇形交换机
*/
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange("fanoutExchange");
}
/**
* TopicExchange:主题交换机
*/
@Bean
TopicExchange topicExchange() {
return new TopicExchange("topicExchange");
}
/**
* DirectExchange
* 该类交换机是必须完全匹配 routeKey 才能发送到指定队列
* 可以不用写下面的绑定routeKey的操作
* 因为在发送消息的时候,直接指定routeKey就可以发送到指定的队列
*/
@Bean
Binding bindingExchangeDirectQueueHello(Queue helloQueue, DirectExchange helloExchange) {
return BindingBuilder.bind(helloQueue).to(helloExchange).with("hello");
}
/**
* FanoutExchange (扇形交换机 - 广播类型)
* 1、该交换机只需要将指定队列绑定到该交换机上,再往交换机发送信息
* 则该信息就会被广播到已被绑定所有的队列上
* 所以无需routeKey来进行绑定
* 2、如果该交换机没有与任何队列绑定,则消息就会被丢弃
*/
@Bean
Binding bindingExchangeQueueDirect(Queue directQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(directQueue).to(fanoutExchange);
}
/**
* TopicExchange
* 该交换机与每一个队列绑定并且注明绑定队列所关心的主题
* 发送消息的时候带上routeKey 模糊匹配到则发送给相应队列
*/
@Bean
Binding bindingExchangeQueuePayLog(Queue payLog, TopicExchange topicExchange) {
return BindingBuilder.bind(payLog).to(topicExchange).with("#.pay.log.#");
}
}
2 、消费者
@Component
public class DirectReceiver {
@RabbitListener(queues = "direct")
@RabbitHandler
public void process(String message) {
System.out.println("接收者 DirectReceiver," + message);
}
@RabbitListener(queues = "hello")
@RabbitHandler
public void hello(String message) {
System.out.println("接收者 hello," + message);
}
/** 注解方式创建并监听 */
@RabbitListener(bindings = @QueueBinding(
value=@Queue(value = "test.queue",autoDelete="false"),
exchange = @Exchange(
value = "topic.exchange",
type = ExchangeTypes.TOPIC),
key = "#.test.#"
))
@RabbitHandler
public void test(String message) {
JSONObject obj = JSONObject.parseObject(message);
System.out.println("接收者 test," + obj.getString("selectMap"));
}
}
3、生产者
@RestController
public class MqController {
@Autowired
private AmqpTemplate rabbitTemplate;
@PostMapping("/send/{routeKey}")
public Object sendMsg(
@PathVariable("routeKey") String routeKey, @RequestBody String req) throws Exception {
rabbitTemplate.convertAndSend("helloExchange", routeKey, req);
return "success!";
}
}
三、简单介绍
1、RabbitMq的作用
首先为什么引入项目,三大点:解耦,异步,削峰。因为只是简单介绍,就不仔细介绍了,这里主要是记录。
2、指定队列的属性
@Bean
public Queue helloQueue() {
Map<String, Object> args = new HashMap<>();
//设置队列消息过期时间为10秒
args.put("x-message-ttl", 10 * 1000);
//设置死信交换机
args.put("x-dead-letter-exchange", "directExchange");
//设置死信交换机绑定的routeKey
args.put("x-dead-letter-routing-key", "direct");
//durable 是否持久化,exclusive 是否排他,autoDelete 当所有消费客户端连接断开后,是否自动删除
return new Queue("hello", true, false, false, args);
}
3、死信队列和延迟队列
1、死信队列和死信交换机都是普通的队列和交换机,只不过接受的信息都是 ’ 死信 ’ ,什么情况会出现死信:消息过期,消息拒收,队列达到了最大长度。
2、rabbitMq是没有直接的延迟队列的实现,但是我们可以通过设置消息的TTL和死信队列来配合实现一个延迟队列。
比如设置消息三十分钟过期,三十分钟后消息进入死信队列,再去死信队列消费该消息。
可以应用在:比如下单后三十分钟之内有效,三十分钟后可以进入死信进行异常处理。
4、消息的持久化
防止消息丢失,进行交换机、队列和消息的持久化。使用convertAndSend来发送消息默认就是持久化的。新建队列和交换机指定durable参数为true。
5、消息确认机制
为什么需要消息确认机制。比如我们生产者生产一条消息,发送到交换机,但是在发送的过程中,交换机宕机了,但是我们的生产者还是认为该条消息发送成功了,这样就会导致这条消息丢失了,这不是我们想要看到的结果。所以我们需要在发送消息到交换机的时候确认消息是否安全发送到了交换机,然后交换机和消息进行持久化,即可初步防止消息丢失。
配置文件开启确认:
# RabbitMQ 消息队列配置
rabbitmq:
host: www.anbaba.wang
port: 5672
username: admin
password: admin
virtual-host: /
publisher-confirm-type: correlated # 开启消息确认模式
publisher-returns: true # 开启消息未送达回调机制
listener:
simple:
prefetch: 2 # 单个请求中处理的消息个数,unack的最大数量
concurrency: 1 # 消费者最小数量
max-concurrency: 10 # 消费者最大数量
acknowledge-mode: manual # 设置消费端手动 ack
retry:
enabled: true # 开启重试机制
max-attempts: 5 # 最大重试次数
type: simple
5.1 统一配置的方式
配置类,统一配置RedisTemplate:
@Configuration
public class RabbitDirectConfig {
@Bean
RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
// 设置为true 后,如果交换机没有找到合适的队列存放消息,则会将该消息返回给生产者
// 如果设置为false,则会直接丢弃该条信息
rabbitTemplate.setMandatory(true);
// 设置消息确认机制 ack
rabbitTemplate.setConfirmCallback( new SendConfirmCallback());
// 设置消息返回队列机制
rabbitTemplate.setReturnCallback( new SendReturnCallback());
return rabbitTemplate;
}
}
回调函数:
import com.anbaba.main.common.exception.CommonException;
import com.anbaba.main.common.exception.ErrorEnum;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
public class SendConfirmCallback implements RabbitTemplate.ConfirmCallback {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
if (ack) {
System.out.println("确认成功");
} else {
System.out.println("确认失败");
}
}
}
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
public class SendReturnCallback implements RabbitTemplate.ReturnCallback {
@Override
public void returnedMessage(Message message, int i, String reason, String exchange, String queue) {
System.out.println("消息未正确路由到队列!");
}
}
5.2 单独配置的方式
每一个生产者一个单独的回调函数:
import com.anbaba.main.common.exception.CommonException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import java.util.Date;
@Service("send")
public class Send {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 消息确认回调
*/
final RabbitTemplate.ConfirmCallback confirmCallback = new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("correlationData: " + correlationData);
System.out.println("ack: " + ack);
if (!ack) {
System.out.println("异常处理....");
}
}
};
/**
* 消息未正确路由至队列回调
*/
final RabbitTemplate.ReturnCallback returnCallback = new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("return exchange: " + exchange + ", routingKey: "
+ routingKey + ", replyCode: " + replyCode + ", replyText: " + replyText);
}
};
/**
* 发送消息
* @param a
* @param b
* @param c
*/
public void send(String a, String b, String c) {
/**
* Mandatory为true时,消息通过交换器无法匹配到队列会返回给生产者
* 为false时,匹配不到会直接被丢弃
*/
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback(confirmCallback);
rabbitTemplate.setReturnCallback(returnCallback);
//保证全局唯一即可,作为消息的ID
CorrelationData correlationData = new CorrelationData("1234567890" + new Date());
rabbitTemplate.convertAndSend(a, b, c, correlationData);
System.out.println("发送完毕");
}
}
6、手动 ack 消息
1、 为什么需要ack消息?如果消费者消费了一条消息,但是在消费的过程中出现了异常,没有正确消费该条消息,那么交换机也会认为你已经消费了这条消息,所以直接在交换机中删除了这条消息,导致了消息丢失。
2、 我们可以在消息正确消费之后手动确认这条消息,让交换机知道我们已经正确消费了这条消息,你可以删除这条消息了。
3、 如果在消费过程中出现异常了,可以手动 nack 拒绝这条消息,可以选择重新将这条消息退回到交换机进行重试,但是重试一般都不能解决问题,因为异常可能一直存在。所以可以将该条消息放入死信队列,人工处理或其他方式集中处理。
上代码:
@RabbitListener(queues = "direct")
@RabbitHandler
public void consumeMessage(Channel channel, Message message) throws IOException {
System.out.println("接收者 DirectReceiver: " + message);
/**
* 手动 ack 消息
* deliveryTag:该消息的index
* multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
*/
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
@RabbitListener(queues = "hello")
@RabbitHandler
public void hello(Channel channel, Message message) throws IOException {
try {
//模拟消费过程,查看 Nack 的消息状态
Thread.sleep(10000);
System.out.println("hello 消费成功:" + message);
} catch (Exception e) {
System.out.println("出现异常了哥们儿~");
}
/**
* 拒绝一条或多条消息
* deliveryTag:该消息的index
* multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息。
* requeue:被拒绝的是否重新入队列 ( 可能导致无限循环重试,因为异常不一定因为重试而解决 )
*/
channel.basicNack(message.getMessageProperties().getDeliveryTag(), true,true);
}