续
订阅模型 -Direct
有选择性的接收消息
在订阅模式中,生产者发布消息,所有消费者都可以获取所有消息。
在路由模式中,我们将添加一个功能 - 我们将只能订阅一部分消息。 例如,我们只能将重要的错误消息引导到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。
但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。
P:生产者,向Exchange发送消息,发送消息时,会指定一个 routing key。
X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
C1:消费者,其所在队列指定了需要routing key 为 error 的消息
C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息
代码demo:
/**
* 生产者,模拟为商品服务
*/
public class Send {
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明exchange,指定类型为direct
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 消息内容
String message = "商品删除了, id = 1001";
// 发送消息,并且指定routing key 为:delete,代表删除
channel.basicPublish(EXCHANGE_NAME, "delete", null, message.getBytes());
System.out.println(" [商品服务:] Sent '" + message + "'");
channel.close();
connection.close();
}
}
/**
* 消费者1
*/
public class Recv {
private final static String QUEUE_NAME = "direct_exchange_queue_1";
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。假设此处需要update和delete消息
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者1] received : " + msg + "!");
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
/**
* 消费者2
*/
public class Recv2 {
private final static String QUEUE_NAME = "direct_exchange_queue_2";
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者2] received : " + msg + "!");
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
最后启动,两个消费者均会收到delete的消息,如果在发送insert的消息,就只有带‘insert’的key接收的消息。起到了Route的作用
订阅模型 -Topic
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!起到了一个匹配的作用。
Routingkey`一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
#:匹配一个或多个词
*:匹配不多不少恰好1个词
举例:
audit.#:能够匹配audit.irs.corporate 或者 audit.irs
audit.*:只能匹配audit.irs
demo实现:
public class Send {
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明exchange,指定类型为topic
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 消息内容
String message = "新增商品 : id = 1001";
// 发送消息,并且指定routing key 为:insert ,代表新增商品
channel.basicPublish(EXCHANGE_NAME, "item.insert", null, message.getBytes());
System.out.println(" [商品服务:] Sent '" + message + "'");
channel.close();
connection.close();
}
}
public class Recv {
private final static String QUEUE_NAME = "topic_exchange_queue_1";
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。需要 update、delete
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.update");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.delete");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者1] received : " + msg + "!");
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
public class Recv2 {
private final static String QUEUE_NAME = "topic_exchange_queue_2";
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.*");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者2] received : " + msg + "!");
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
运行可看出,生产者发送了item.insert的消息,消费者1只绑定了‘item.update’和‘item.delete’,所以接受不到生产者发送的消息。消费者2绑定了item.*所以可匹配一个或多个字段,因此可以接收到生产者所发出的消息。
问题一:如果RabbitMQ服务挂掉了怎么办?
可以采用持久化,包括容器持久化及消息持久化,
//如在声明交换机时,增加一个参数设置为true。
channel.exchangeDeclare(EXCHANGE_NAME, "topic","ture");
队列:将第二个参数durable改为true
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
//发布消息,将第三个参数的null属性改为MessageProperties.PRESISTENT_TEXT_PLAIN
channel.basicPublish(EXCHANGE_NAME, "item.insert", MessageProperties.PRESISTENT_TEXT_PLAIN, message.getBytes());
生产者确认机制:RabbitMQ向生产者发送一条消息
解决消息丢失?
1)ACK消费者确认
2)持久化
3)发送消息前,将消息持久化到数据库,并记录消息状态(可靠消息服务)
4)生产者确认。
幂等性(同一接口被重复执行,其结果一致)
Spring AMQP
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring:
rabbitmq:
host: 39.108.254.46
username: kf
password: kf
virtual-host: /kf
##端口默认15672
@Component
public class Listener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "spring.test.queue", durable = "true"),
exchange = @Exchange(
value = "spring.test.exchange",
ignoreDeclarationExceptions = "true",
type = ExchangeTypes.TOPIC
),
key = {"#.#"}))
public void listen(String msg){
System.out.println("接收到消息:" + msg);
}
}
- `@Componet`:类上的注解,注册到Spring容器
- `@RabbitListener`:方法上的注解,声明这个方法是一个消费者方法,需要指定下面的属性:
- `bindings`:指定绑定关系,可以有多个。值是`@QueueBinding`的数组。`@QueueBinding`包含下面属性:
- `value`:这个消费者关联的队列。值是`@Queue`,代表一个队列
- `exchange`:队列所绑定的交换机,值是`@Exchange`类型
- `key`:队列和交换机绑定的`RoutingKey`
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class MqDemo {
@Autowired
private AmqpTemplate amqpTemplate;
//测试消息发送
@Test
public void testSend() throws InterruptedException {
String msg = "hello, Spring boot amqp";
this.amqpTemplate.convertAndSend("spring.test.exchange","a.b", msg);
// 等待10秒后再结束
Thread.sleep(10000);
}
}
SpringBoot 配置使用
1.配置文件
spring:
rabbitmq:
host: 192.168.0.1
port: 5672
username: admin
password: public
# virtual-host: GHost
virtual-host: /
# 确认消息已发送到交换机
publisher-confirms: true
# 消息发送失败后返回
publisher-returns: true
listener:
# 默认配置是simple
type: simple
simple:
# 手动ack Acknowledge mode of container. auto none
acknowledge-mode: manual
# Maximum number of unacknowledged messages that can be outstanding at each consumer.
prefetch: 3
2.RabbitTemplate配置
@Configuration
public class RabbitTemplateConfig {
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate=new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
rabbitTemplate.setMandatory(true);
//推送到server回调
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) ->
log.info("ConfirmCallback correlationData:{},ack:{},cause:{}",correlationData,ack,cause));
//消息返回给生产者, 路由不到队列时返回给发送者 先returnCallback,再 confirmCallback
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.info("ReturnCallback message:{},replyCode:{},replyText:{},exchange:{},routingKey:{}",
message,replyCode,replyText,exchange,routingKey);
});
return rabbitTemplate;
}
}
3.消息发送
@RestController
@RequestMapping("mqTest1")
public class MqTest1Controller {
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("streamPush")
public String streamPush(){
rabbitTemplate.convertAndSend(HeadersRabbitConfig.HEADERS_EXCHANGE_NAME,"","I am a msg".getBytes(),message -> {
Map<String,Object> headers=message.getMessageProperties().getHeaders();
headers.put("format","pdf");
//headers.put("type","report");
headers.put("han","shaohua");
return message;
});
return "hehe";
}
}
4.消息监听
@Component
@RabbitListener(queues = {TopicRabbitConfig.TOPIC_QUEUE_NAME})
public class TopicHandler2 {
@RabbitHandler
public void processByteMsg(@Headers MessageHeaders headers, byte[] msgPayload) {
String msg = new String(msgPayload);
log.info("TopicHandler2收到消息:{},exchange:{},routingKey:{},queue:{}",
msg, headers.get("amqp_receivedExchange"),
headers.get("amqp_receivedRoutingKey"),
headers.get("amqp_consumerQueue"));
}
@RabbitHandler
public void processStringMsg(@Headers MessageHeaders headers, Channel channel, String msg,
@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
@Header(AmqpHeaders.CONSUMER_TAG) String consumerTag) throws Exception {
log.info("TopicHandler2收到消息:{},exchange:{},routingKey:{},queue:{}",
msg, headers.get("amqp_receivedExchange"),
headers.get("amqp_receivedRoutingKey"),
headers.get("amqp_consumerQueue"));
channel.basicAck(deliveryTag, false);
//拒绝消息, requeue为true会重入队列, 消息就会一直接受
//channel.basicNack(deliveryTag,false,false);
//channel.basicCancel(consumerTag);
}
}
5.交换机绑定队列关系
5.1 Fanout
Fanout类型的exchange会忽略routingKey,所以配置中无法设置routingKey
@Configuration
public class FanoutRabbitConfig {
public static final String FANOUT_QUEUE_NAME="htFanoutQueue";
public static final String FANOUT_EXCHANGE_NAME="htFanoutExchange";
//发送消息时只需要创建exchange即可
@Bean
FanoutExchange fanoutExchange(){
return new FanoutExchange(FANOUT_EXCHANGE_NAME);
}
@Bean
public Queue fanoutQueue(){
//durable:true mq服务器重启后仍然存在 false重启后队列自动删除
return new Queue(FANOUT_QUEUE_NAME,false);
}
/**
* 队列绑定fanout类型的exchange是无法设置routingKey
* @return
*/
@Bean
Binding bindingFanout(){
return BindingBuilder.bind(fanoutQueue()).to(fanoutExchange());
}
}
5.2 direct
/**
* direct类型exchange routingKey匹配
*/
@Configuration
public class DirectRabbitConfig {
static final String DIRECT_QUEUE_NAME="htDirectQueue";
public static final String DIRECT_EXCHANGE_NAME="htDirectExchange";
public static final String ROUTING_KEY="htDirectRouting";
//发送消息是只需要创建exchange即可
@Bean
DirectExchange directExchange(){
return new DirectExchange(DIRECT_EXCHANGE_NAME);
}
//接收消息是需要声明队列,然后通过routingKey将队列与exchange绑定
@Bean
public Queue directQueue(){
return new Queue(DIRECT_QUEUE_NAME,false);
}
@Bean
Binding bindingDirect(){
return BindingBuilder.bind(directQueue()).to(directExchange()).with(ROUTING_KEY);
}
}
3.Topic
@Configuration
public class TopicRabbitConfig {
static final String TOPIC_QUEUE_NAME="htTopicQueue";
static final String TOPIC_QUEUE1_NAME="htTopicQueue1";
public static final String TOPIC_EXCHANGE_NAME="htTopicExchange";
/**
* #匹配0个或多个单词,*匹配一个单词,即使一个的多个routingKey都匹配上了,但该队列只收到一次消息
*/
public static final String TOPIC="htTopicRouting.*";
public static final String TOPIC1="htTopicRouting.topic1";
//发送消息是只需要创建exchange即可
@Bean
TopicExchange topicExchange(){
return new TopicExchange(TOPIC_EXCHANGE_NAME);
}
//接收消息是需要声明队列,然后通过routingKey将队列与exchange绑定
//带通配符的topic
@Bean
public Queue topicQueue(){
//durable:true mq服务器重启后让然存在 false重启后队列自动删除
return new Queue(TOPIC_QUEUE_NAME,false);
}
@Bean
Binding bindingTopic(){
return BindingBuilder.bind(topicQueue()).to(topicExchange()).with(TOPIC);
}
//topic1
@Bean
public Queue topicQueue1(){
return new Queue(TOPIC_QUEUE1_NAME,false);
}
@Bean
Binding bindingTopic1(){
return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with(TOPIC1);
}
}
4.header
@Configuration
public class HeadersRabbitConfig {
static final String HEADERS_QUEUE_NAME="htHeadersQueue";
public static final String HEADERS_EXCHANGE_NAME="htHeadersExchange";
//发送消息是只需要创建exchange即可
@Bean
HeadersExchange headersExchange(){
return new HeadersExchange(HEADERS_EXCHANGE_NAME);
}
@Bean
public Queue headersQueue(){
return new Queue(HEADERS_QUEUE_NAME,false);
}
@Bean
Binding bindingDirect(){
Map<String, Object> arguments = new HashMap<>();
arguments.put("format", "pdf");
arguments.put("type", "report");
//x-match为any
return BindingBuilder.bind(headersQueue()).to(headersExchange()).whereAny(arguments).match();
}
@Bean
Binding bindingAllDirect(){
Map<String, Object> arguments = new HashMap<>();
arguments.put("format", "pdf");
arguments.put("type", "report");
//x-match为all
return BindingBuilder.bind(headersQueue()).to(headersExchange()).whereAll(arguments).match();
}
}