使用场景:消息中间件主要用来解耦合以及削峰,当一些业务与主体业务的关联关系没有特别强,或者在某一段时间后发生,可以采用消息中间件,在其他的业务方法中处理该业务逻辑,并不影响当前业务的处理。当某些业务在同一时间访问量较大时,可以先将请求放到消息中间件中,降低对数据等的压力。
rabbitmq一般业务可能需要的:1:简单的发送消息(使用不同的模式)2:发送消息后是否收到(消息确认)3:消息的自动接收与手动接收4:拒绝消息5:rabbitmq中的事务。
1:交换机
主要使用的分为三种:Fanout Exchange、Direct Exchange 以及Topic Exchange。其他还包括:Header Exchange、Default Exchange、Dead Letter Exchange,只不过不太常用。
Fanout Exchange:
不处理路由键。你只需要简单的将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。Fanout交换机转发消息是最快的。 任何发送到Fanout Exchange的消息都会被转发到与该Exchange绑定(Binding)的所有Queue上。
1.可以理解为路由表的模式
2.这种模式不需要RouteKey
3.这种模式需要提前将Exchange与Queue进行绑定,一个Exchange可以绑定多个Queue,一个Queue可以同多个Exchange进行绑定。
例子
Queue中的构造方法的参数:name-队列名称,durable-持久化,默认true,exclusive-是否独有的默认false,autodelete-自动删除,默认false
@Configuration
@Log4j2
public class RabbitConfig {
//队列 起名:TestDirectQueue
@Bean
public Queue TestDirectQueue() {
return new Queue("TestFanOutQueue",true);
}
@Bean
public Queue TestDirectQueue2() {
return new Queue("TestFanOutQueue2",true);
}
//Direct交换机 起名:TestDirectExchange
@Bean
FanoutExchange TestDirectExchange() {
return new FanoutExchange("TestFanoutExchange");
}
@Bean
Binding bindingDirect() {
return BindingBuilder.bind(TestDirectQueue()).to(TestDirectExchange());
}
@Bean
Binding bindingDirect2() {
return BindingBuilder.bind(TestDirectQueue2()).to(TestDirectExchange());
}
}
消息生产者:
@PostMapping("/testMq1")
public void testMq1(){
Map<String,Object> map = new HashMap();
map.put("name","zhangsan" );
map.put("pwd","000000" );
User user = new User();
user.setId(1L);
user.setName("张三");
rabbitTemplate.convertAndSend("TestFanoutExchange" ,"",user );
}
消息消费者:
@Component
@RabbitListener(queues = "TestFanOutQueue2")
@Log4j2
public class TestListener2 {
@RabbitHandler
public void listerner1(User test){
try{
System.out.println("监听器2接收到信息:"+test.toString());
}catch (Exception e){
log.info("接收消息失败");
}
}
}
@Component
@RabbitListener(queues = "TestFanOutQueue")
public class TestListener {
@RabbitHandler
public void listerner1(User test){
System.out.println("监听器1接收到信息:"+test.toString());
}
}
消费结果
如果消费业务报错的话,消息不会被消费。并且如果消费者1报错并不影响消费者2的消费(因为是不同的队列)
Direct Exchange
Direct Exchange - 处理路由键。需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键 “dog”,则只有被标记为“dog”的消息才被转发,不会转发dog.puppy,也不会转发dog.guard,只会转发dog。
任何发送到Direct Exchange的消息都会被转发到RouteKey中指定的Queue。
1.一般情况可以使用rabbitMQ自带的Exchange:”"(该Exchange的名字为空字符串,下文称其为default Exchange)。
2.这种模式下不需要将Exchange进行任何绑定(binding)操作
3.消息传递时需要一个“RouteKey”,可以简单的理解为要发送到的队列名字。
配置文件:
//----------------------------------------------direct exchange--------------------------------------------
@Bean
public Queue TestDirectQueue3() {
return new Queue("TestFanOutQueue3",true);
}
@Bean
public Queue TestDirectQueue4() {
return new Queue("TestFanOutQueue4",true);
}
@Bean
DirectExchange DirectExchange1(){
return new DirectExchange("directExchange1");
}
@Bean
DirectExchange DirectExchange2(){
return new DirectExchange("directExchange2");
}
/**
* 首先两个队列绑定一个路由
*/
@Bean
Binding bindingDirect3() {
return BindingBuilder.bind(TestDirectQueue3()).to(DirectExchange1()).with("routing1");
}
@Bean
Binding bindingDirect4() {
return BindingBuilder.bind(TestDirectQueue4()).to(DirectExchange1()).with("routing1");
}
消息生产者:与上面类似,只不过加了路由参数
@PostMapping("/testMq1")
public void testMq1(){
Map<String,Object> map = new HashMap();
map.put("name","zhangsan" );
map.put("pwd","000000" );
User user = new User();
user.setId(1L);
user.setName("张三");
rabbitTemplate.convertAndSend("directExchange1" ,"routing1",user );
}
消费者与上面类似,两个队列同时收到消息
topic exchange:
主题交换机,这个交换机其实跟直连交换机流程差不多,但是它的特点就是在它的路由键和绑定键之间是有规则的。(简单的说与直连交换机相比添加了一个通配符,*和#,通过通配符可以发到不同的路由中)
简单地介绍下规则:
* (星号) 用来表示一个单词 (必须出现的)
# (井号) 用来表示任意数量(零个或多个)单词
通配的绑定键是跟队列进行绑定的,举个小例子
队列Q1 绑定键为 *.TT.* 队列Q2绑定键为 TT.#
如果一条消息携带的路由键为 A.TT.B,那么队列Q1将会收到;
如果一条消息携带的路由键为TT.AA.BB,那么队列Q2将会收到;
当一个队列的绑定键为 "#"(井号) 的时候,这个队列将会无视消息的路由键,接收所有的消息。
当 * (星号) 和 # (井号) 这两个特殊字符都未在绑定键中出现的时候,此时主题交换机就拥有的直连交换机的行为。
所以主题交换机也就实现了扇形交换机的功能,和直连交换机的功能。
2:死信队列以及延时队列
死信队列:
DLX(Dead Letter Exchange),死信交换器。当队列中的消息被拒绝、或者过期会变成死信,死信可以被重新发布到另一个交换器,这个交换器就是DLX,与DLX绑定的队列称为死信队列。
造成死信的原因:
- 信息被拒绝
- 信息超时
- 超过了队列的最大长度
使用死信队列的方法,首先创建一个死信队列以及交换机相互绑定,然后创建一个普通的队列,该队列绑定死信交换机以及路由,那么如果向普通队列中发送消息,消息没有被消费过时或者被拒绝后则会进入死信队列中,然后根据私信队列的消费逻辑消费。
死信队列:
/**
* 声明死信队列
* @return Queue
*/
@Bean
public Queue dlxQueue() {
return new Queue("deadQueue");
}
@Bean
DirectExchange DeadExchange(){
return new DirectExchange("deadExchange");
}
/**
* 首先两个队列绑定一个路由
*/
@Bean
Binding bindingDirect5() {
return BindingBuilder.bind(dlxQueue()).to(DeadExchange()).with("deadKey");
}
普通对列绑定死信队列:
@Bean
public Queue TestDirectQueue2() {
Map<String, Object> argsMap= Maps.newHashMap();
argsMap.put("x-dead-letter-exchange","deadExchange");
argsMap.put("x-dead-letter-routing-key","deadKey");
return new Queue("TestFanOutQueue2",true,false,false,argsMap);
}
//Direct交换机 起名:TestDirectExchange
@Bean
FanoutExchange TestDirectExchange() {
return new FanoutExchange("TestFanoutExchange");
}
@Bean
Binding bindingDirect2() {
return BindingBuilder.bind(TestDirectQueue2()).to(TestDirectExchange());
}
PS:如果对队列进行了修改,那么之前的队列最好删除否则,启动时由于两个队列名称相同但是配置不同,会报错。
生产者:
/**
* 测试rabbitmq
*/
@PostMapping("/testMq1")
public void testMq1(){
Map<String,Object> map = new HashMap();
map.put("name","zhangsan" );
map.put("pwd","000000" );
User user = new User();
user.setId(1L);
user.setName("张三");
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
/**
* 当mandatory标志位设置为true时
* 如果exchange根据自身类型和消息routingKey无法找到一个合适的queue存储消息
* 那么broker会调用basic.return方法将消息返还给生产者(就是走未确认业务)
* 当mandatory设置为false时,出现上述情况broker会直接将消息丢弃
*/
rabbitTemplate.setMandatory(true);
MessagePostProcessor messagePostProcessor = message -> {
MessageProperties messageProperties = message.getMessageProperties();
// 设置编码
messageProperties.setContentEncoding("utf-8");
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
// 设置过期时间10*1000毫秒
messageProperties.setExpiration("5000");
return message;
};
rabbitTemplate.convertAndSend("TestFanoutExchange" ,"",user,messagePostProcessor );
}
3:消息确认
消息确认主要分两个方面:一方面消息由消费者发出后,服务器是否收到消息。另一方面是指消费者消费可以手动确认收到消息以及拒绝消息。消息确认主要是防止消息丢失以及消息重复消费方面的问题。(关于配置的话可以在rebbitmq配置文件中进行全局怕配置,也可以在某个代码逻辑中进行单独的业务逻辑配置)
3.1 在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两个选项用来控制消息的投递可靠性模式。
rabbitmq 整个消息投递的路径为:producer->rabbitmq broker cluster->exchange->queue->consumer
message 从 producer 到 rabbitmq broker cluster 则会返回一个 confirmCallback 。
message 从 exchange->queue 投递失败则会返回一个 returnCallback 。我们将利用这两个 callback 控制消息的最终一致性和部分纠错能力。
例子:首先在配置文件中把确认模式打开:publisher-confirms: true
配置消息确认可以在配置文件中或者发送消息代码中添加confirm逻辑
一般在项目中可以在confirm以及returnMessage中添加相应的逻辑,例如消息发送不成功相关的操作。
3.2消息接收确认
首先关于rabbitmq的配置
rabbitmq:
host: 39.100.192.198
port: 5672
username: guest
password: guest
publisher-confirms: true
publisher-returns: true
listener:
direct:
acknowledge-mode: manual
simple:
acknowledge-mode: manual #采取手动应答
#concurrency: 1 # 指定最小的消费者数量
#max-concurrency: 1 #指定最大的消费者数量
retry:
enabled: true # 是否支持重试
消息生产者:
/**
* 测试rabbitmq
*/
@PostMapping("/testMq1")
public void testMq1(){
Map<String,Object> map = new HashMap();
map.put("name","zhangsan" );
map.put("pwd","000000" );
User user = new User();
user.setId(1L);
user.setName("张三");
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
/**
* 当mandatory标志位设置为true时
* 如果exchange根据自身类型和消息routingKey无法找到一个合适的queue存储消息
* 那么broker会调用basic.return方法将消息返还给生产者(就是走未确认业务)
* 当mandatory设置为false时,出现上述情况broker会直接将消息丢弃
*/
rabbitTemplate.setMandatory(true);
rabbitTemplate.convertAndSend("TestFanoutExchange" ,"",user );
}
消费者:
@Component
@RabbitListener(queues = "TestFanOutQueue2")
@Log4j2
public class TestListener2 {
@RabbitHandler
@SneakyThrows
public void listerner1(User test,Channel channel,Message message){
try{
System.out.println("监听器2接收到信息:"+test.toString());
int i = 1/0; // 测试异常
//手动ack应答
//告诉服务器收到这条消息 已经被我消费了 可以在队列删掉 这样以后就不会再发了
// 否则消息服务器以为这条消息没处理掉 后续还会在发,true确认所有消费者获得的消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
log.info("消息消费成功:id:{}",message.getMessageProperties().getDeliveryTag());
}catch (Exception e){
log.info("接收消息失败");
// 如果不写下方的代码,消息没有接收到之后不会重新接收了,就会在哪里放着
//否认消息,拒接该消息重回队列(消息不会重新接受了)
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
// 消息失败,可以重新接收
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);
}
}
}
4:面试中常见的问题
1:大量消息在mq里积压了几个小时了还没解决
这种时候只能操作临时扩容,以更快的速度去消费数据了。具体操作步骤和思路如下:
①先修复consumer的问题,确保其恢复消费速度,然后将现有consumer都停掉。
②临时建立好原先10倍或者20倍的queue数量(新建一个topic,partition是原来的10倍)。
③然后写一个临时分发消息的consumer程序,这个程序部署上去消费积压的消息,消费之后不做耗时处理,直接均匀轮询写入临时建好分10数量的queue里面。
④紧接着征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的消息。
⑤这种做法相当于临时将queue资源和consumer资源扩大10倍,以正常速度的10倍来消费消息。
⑥等快速消费完了之后,恢复原来的部署架构,重新用原来的consumer机器来消费消息。
2: