RabbitMQ涉及的重要的概念
Channel | 信道。消息的读写操作在信道中进行,客户端可以建立多个信道,每个信道代表一个会话。 |
Message | 消息。应用程序和MQ服务之间传送的数据,消息可以非常简单,也可以很复杂。有Properties和body组成。Properties为外包装,可以对消息进行修饰,比如消息的优先级、延迟等高级特性;body就是消息体内容。 |
Exchange | 交换器,一个MQ服务器可以有一个或多个交换器。应用程序通过交换器,按照一定路由规则将消息写入MQ的一个或多个队列中。其中,如果交换器路由不到队列,可以将消息返回给生产者,或者直接丢弃。RabbitMQ中的交换器类型有:Direct、topic、fanout、headers。 |
Binding | 绑定。交换器和队列之间的虚拟映射规则,意思是将交换器和队列绑定在一起。在建立这个映射关系,需要由Routing Key去标识。 |
RoutingKey | 路由键。交换器在接受到生产者发送的消息后,会根据RoutingKey去决定将消息发送到某个队列上。命名通常由一个"."分割的字符串,如"com.rabbitmq"。 |
Queue | 队列,一个MQ服务可以有一个或多个队列。用来保存消息和供消费者读取消息。 |
push模式 | rabbitMQ消费消息的模式之一。指MQ服务主动将消息推送给消费者程序,前提是消费者程序按照规定监听了MQ。 |
pull模式 | rabbitMQ消费消息的模式之一。指消费者程序主动获取消息,假如消费者程序不需要使用MQ中的数据时,则不获取。 |
org.springframework.boot
spring-boot-starter-amqp
# MQ连接地址
spring.rabbitmq.addresses=127.0.0.1
# MQ连接端口
spring.rabbitmq.port=5672
# 账号
spring.rabbitmq.username=guest
# 密码
spring.rabbitmq.password=guest
springboot-rabbitMQ提供了一个封装好的操作对象RabbitTemplate。
@Bean
public RabbitTemplate initrRabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
return rabbitTemplate;
}
a.配置
/**
* 直连型交换机(exchange)配置
* */
@Configuration
public class DirectRabbitConfig {
/**交换机:可以创建多个交换机.所以声明了三个**/
public static final String Exchange_A = "Exchange_A";
public static final String Exchange_B = "Exchange_B";
public static final String Exchange_C = "Exchange_C";
/**队列:一个MQ服务可以创建多个队列.所以声明了三个**/
public static final String Queue_A = "Queue_A";
public static final String Queue_B = "Queue_B";
public static final String Queue_C = "Queue_C";
// 生产者将消息发送给交换器的时候,会发送一个routingKey,用来指定路由规则,这样交换机就知道把消息发送到哪一个队列.
// 通常路由键为一个"."分割的字符,比如:com.rabbitmq
// 以下代码就是指定路由和队列绑定规则的
// return BindingBuilder.bind(队列).to(交换器).with(routingKey);
// 一个交换机可以绑定多个消息队列,也就是消息通过一个交换机,可以分发到不同的队列当中去
/**路由键:交换机和队列之间需要路由键绑定,所以声明了三个**/
public static final String RoutingKey_A = "RoutingKey_A";
public static final String RoutingKey_B = "RoutingKey_B";
public static final String RoutingKey_C = "RoutingKey_C";
// 初始化队列
@Bean
public Queue initQueueA() {
// 构造函数中。第一个参数:指定队列的名称;第二个参数:是否持久化消息
return new Queue(Queue_A, true);
}
@Bean
public Queue initQueueB() {
return new Queue(Queue_B, true);
}
@Bean
public Queue initQueueC() {
return new Queue(Queue_C, true);
}
// 初始化一个 directExchange
@Bean
public DirectExchange initDirectExchange() {
// 构造函数中.第一个参数:指定交换机;第二个参数:是否持久化;第三个参数:是否自动删除
return new DirectExchange(Exchange_A, true, false);
}
// 绑定交换器和队列 一个交换机可以绑定多个消息队列,也就是消息通过一个交换机,可以分发到不同的队列当中去。
@Bean
public Binding initBindingA() {
return BindingBuilder.bind(initQueueA()).to(initDirectExchange()).with(RoutingKey_A);
}
@Bean
public Binding initBindingB() {
return BindingBuilder.bind(initQueueB()).to(initDirectExchange()).with(RoutingKey_B);
}
@Bean
public Binding initBindingC() {
return BindingBuilder.bind(initQueueC()).to(initDirectExchange()).with(RoutingKey_C);
}
}
b.生产消息
@RunWith(SpringRunner.class)
@SpringBootTest
public class FirstRabbitMQTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* Direct 生产消息
* */
@Test
public void method() {
Map map = new HashMap<>();
map.put("name", "zepal");
map.put("age", "18");
map.put("gender", "男");
map.put("timestamp", System.currentTimeMillis());
String json = JSONObject.toJSONString(map);
// 将消息发送到交换机(Exchange_A),指定路由键(RoutingKey_B)
// 由DirectRabbitConfig配置中的绑定关系,可以确定消息会通过Exchange_A发送到Queue_B队列中
rabbitTemplate.convertAndSend(DirectRabbitConfig.Exchange_A, DirectRabbitConfig.RoutingKey_B, json);
System.out.println("ok");
}
}
成功运行后,可以在RabbitMQ管理后台成功看到此条消息。
c.push模式消费
@Service
@RabbitListener(queues = DirectRabbitConfig.Queue_B)
public class DirectReceiver {
/**
* 除此之外,还可以创建多个消费者监听同一队列.
*
如果多个消费者监听同一队列.会以轮询的方式对消息进行消费,而且不存在重复消费。
* @param message 从MQ中获取的信息.message参数类型需要和生产者投递的类型一致.如果生产者投递的类型是Map,那么message的类型也需要时Map,不能是String
* */
@RabbitHandler
public void process(String message) {
System.out.println("DirectReceiver消费者收到消息 : " + message);
}
}
启动项目或项目处于运行中,会输出结果:
DirectReceiver消费者收到消息 : {"gender":"男","name":"zepal","age":"18","timestamp":1595830452670}
d.pull模式消费(注:如果在一个系统中测试,需要把push模式的消费者注释掉,不然启动单元测试会被push模式的消费者把消息消费掉之后,让下面的代码产生异常)
/**
* pull模式 手动拉取消息
* @throws IOException
* @throws ClassNotFoundException
* */
@Test
public void methodD() throws IOException {
// 获取一个MQ连接
Connection connection = connectionFactory.createConnection();
// 获取一个信道
Channel channel = connection.createChannel(false);
// 参数1:表示从哪个队列中获取.参数2:是否自动确认
GetResponse getResponse = channel.basicGet(DirectRabbitConfig.Queue_B, false);
byte[] body = getResponse.getBody();
// 消息体body转string
String json = new String(body, "UTF-8");
System.out.println(json);
// 转其他对象参考。这里的转换类型需要根据生产者投递类型决定
// ByteArrayInputStream in = new ByteArrayInputStream(body);
// ObjectInputStream sIn = new ObjectInputStream(in);
// Map map = (Map) sIn.readObject();
// 手动确认消息已正确消费,可以从队列中移除
channel.basicAck(getResponse.getEnvelope().getDeliveryTag(),false);
}
运行单元测试会输出以下结果:
{"gender":"男","name":"zepal","age":"18","timestamp":1595831508608}
a.配置
/**
* 主体型交换机(Topic Exchange)配置
* */
@Configuration
public class TopicRabbitConfig {
/**声明一个交换机**/
public static final String Topic_Exchange = "Topic_Exchange";
/**声明一个队列**/
public static final String Topic_Queue = "Topic_Queue";
/**声明一个路由键**/
public static final String Topic_RoutingKey = "topic.#";
/**
*
初始化交换机
* */
@Bean
public TopicExchange initTopicExchange() {
TopicExchange topicExchange = new TopicExchange(Topic_Exchange, true, false);
return topicExchange;
}
/**
*
初始化队列
* */
@Bean
public Queue initQueue() {
Queue queue = new Queue(Topic_Queue);
return queue;
}
/**
*
绑定交换机和队列
* */
@Bean
public Binding initBinding() {
return BindingBuilder.bind(initQueue()).to(initTopicExchange()).with(Topic_RoutingKey);
}
}
b.生产消息
/**
* Topic Exchange 生产者
* */
@Test
public void topicProviderTest() {
Map map = new HashMap<>();
map.put("name", "zepal");
map.put("age", "18");
map.put("gender", "男");
map.put("timestamp", System.currentTimeMillis());
String json = JSONObject.toJSONString(map);
// 在配置中的路由键"topic.#"
rabbitTemplate.convertAndSend(TopicRabbitConfig.Topic_Exchange, "topic.zepal", json);
System.out.println("ok");
}
执行成功后,可以在管理后台看到此条消息记录,如下。
说明:
Topic Exchange和Direct Exchange。消息投递和消费过程差不多,但是Topic Exchange的特点是,它的RoutingKey在绑定过程中有一定规则。
*:英文星号。用来匹配一个单词,必须出现。示例:路由键声明为topic.*,那么生产消息时,推送的路由键能匹配的有,topic.xxx。topic/topic.xxx.yyy/xxx.topic不能被匹配
#:英文井号。用来匹配任意数量的单词(0个或多个)。示例:路由键声明为topic.#,那么生产消息时,发送到MQ的路由键可以匹配的有topic.xxx/topic.xxx.yyy/topic。
*和#还可以声明在最前面,比如:*.topic.*/#.topic.#/*.topic.#
c.消费消息
Topic Exchange的消费消息方式和Direct Exchange类似,区别就是Topic Exchange消费监听的队列也支持通配。通配规则和生产者一样。(如果把RoutingKey写死,就成了直连型交换器(Direct Exchange),直连型交换机名称的由来估计就是因为固定的RoutingKey,直接连接交换机与队列)。
扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。
a.配置
/**
* 扇型交换机(Fanout Exchange)配置
* */
@Configuration
public class FanoutRabbitConfig {
/**声明一个交换机**/
public static final String Fanout_Exchange = "Fanout_Exchange";
/**声明一个队列**/
public static final String Fanout_Queue = "Fanout_Queue";
/**
*
初始化一个交换机
* */
@Bean
public FanoutExchange initFanoutExchange() {
FanoutExchange fanoutExchange = new FanoutExchange(Fanout_Exchange);
return fanoutExchange;
}
/**
*
初始化一个队列
* */
@Bean
public Queue initQueue() {
Queue queue = new Queue(Fanout_Queue);
return queue;
}
/**
*
初始化绑定关系。Fanout Exchange不需要路由键绑定.
*
同样一个交换机可以绑定多个队列,那么往一个交换机发送消息,多个队列都会收到
* */
@Bean
public Binding initBinding() {
return BindingBuilder.bind(initQueue()).to(initFanoutExchange());
}
}
b.生产消息
/**
* Fanout Exchange 生产者
* */
@Test
public void fanoutProviderTest() {
Map map = new HashMap<>();
map.put("name", "zepal");
map.put("age", "18");
map.put("gender", "男");
map.put("timestamp", System.currentTimeMillis());
String json = JSONObject.toJSONString(map);
// 不用路由键置null
rabbitTemplate.convertAndSend(FanoutRabbitConfig.Fanout_Exchange, null, json);
System.out.println("ok");
}
c.消费消息
和Direct消费消息一样。
从处理过程上讲,处理速度:Fanout > Direct > Topic
不管是在投递消息还是在消费消息的过程,都有可能因某些原因导致失败。所以RabbitMQ提供了消息确认机制。
1.投递消息确认
在配置文件中加入以下配置
# 投递确认:确认消息已发送到交换机(Exchange)
spring.rabbitmq.publisher-confirms=true
# 投递确认:确认消息已发送到队列(Queue)
spring.rabbitmq.publisher-returns=true
重新配置RabbitTemplate
@Bean
public RabbitTemplate initrRabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
// 设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("ConfirmCallback(数据:correlationData) : " + correlationData);
System.out.println("ConfirmCallback(确认情况:ack) : " + ack);
System.out.println("ConfirmCallback(原因:cause) : " + cause);
// TODO 业务扩展
}
});
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("ReturnCallback(消息:message): " + message);
System.out.println("ReturnCallback(回应码:replyCode): " + replyCode);
System.out.println("ReturnCallback(回应信息:replyText): " + replyText);
System.out.println("ReturnCallback(交换机:exchange): " + exchange);
System.out.println("ReturnCallback(路由键:routingKey): " + routingKey);
// TODO 业务扩展
}
});
return rabbitTemplate;
}
在重新配置RabbitTemplate过程中,加入了两个回调函数。
在消息投递过程中,大致可以分为以下几种情况:
a.消息投递到MQ服务,但是相关的交换机不存在或交换机和队列都不存在;
b.消息投递到MQ服务,交换器存在,但是队列不存在;
c.消息投递到MQ服务,交换机和队列都存在,消息投递成功;
在分别模拟以上3种场景过程中,不难发现以下情况:
a.交换机不存在,只调用ConfirmCallback函数,此时boolean ack参数输出false;
b.交换机存在,队列不存在,ConfirmCallback和ReturnCallback都被调用,此时boolean ack参数同样输出false,在ReturnCallback会输出其它相关信息;
c.当消息投递成功,只调用ConfirmCallback函数,此时boolean ack参数输出true。
2.消费消息确认
编写手动确认的消费者
@Service
public class AckReceiver implements ChannelAwareMessageListener {
private static final Logger logger = LoggerFactory.getLogger(AckReceiver.class);
@Override
public void onMessage(Message message, Channel channel) throws Exception {
// 消息在MQ中的唯一标识
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
byte[] body = message.getBody();// 获取消息体
String json = new String(body, "UTF-8");
System.out.println(json);// TODO 消费消息逻辑
channel.basicAck(deliveryTag, true);// 手动确认消息
// 如果要操作多个队列:1.可以创建多个多个消费者2.根据在当前消费者根据队列的不同做不同的操作,如下:
// 获取队列
// String consumerQueue = message.getMessageProperties().getConsumerQueue();
// if("myQueueName".equals(consumerQueue)) {
// // TODO 消费逻辑
// }
} catch (Exception e) {
channel.basicReject(deliveryTag, false);// 拒绝消息
// 除了basicReject();方法之外,还有basicNack()方法用于拒绝消息
logger.error("消息者:AckReceiver消费失败。", e);
}
}
}
将上面的消费者加入监听
@Autowired
private AckReceiver ackReceiver;
/**
* 手动确认
* */
@Bean
public SimpleMessageListenerContainer initSimpleMessageListenerContainer(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
container.setAcknowledgeMode(AcknowledgeMode.MANUAL); // RabbitMQ默认是自动确认,这里改为手动确认消息
//设置一个队列:这是前面创建的扇型交换器中的队列
container.setQueueNames(FanoutRabbitConfig.Fanout_Queue);
//如果同时设置多个如下: 前提是队列都是必须已经创建存在的,如果要操作多个队列进行手动确认,就需要配置多个
// container.setQueueNames("myQueueName1","myQueueName2","myQueueName3");
container.setMessageListener(ackReceiver);
return container;
}
在上面使用到的消息确认方法。详细说明如下:
Channel.basicAck(deliveryTag, boolean); 用于确认消息;表明已经正常消费消息。参数1:消息在队列中的唯一标识。
Channel.basicReject(deliveryTag, boolean);用于否定确认。一次只能拒绝一条消息。参数1:消息在队列中的唯一标识;参数2:true表示当前消息会重新回到队列中去,false表示直接丢掉消息。
Channel.basicNack(deliveryTag, boolean, boolean);用于否定确认。一次可以拒绝多条消息。参数1:消息在队列中的唯一标识;参数2:如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认;参数3:是否重新回到队列。
========================结束线================================