文章目录
- 1.RabbitMQ基本介绍
- 2.RabbitMQ简单使用案例
- 2.1在RabbitMQ平台上创建一个队列
- 2.2编写生产者代码
- 2.3编写消费者代码
- 3.RabbitMQ如何保证消息不丢失
- 4.RabbitMQ工作队列
- 5.RabbitMQ交换机(exchange)
- 5.1RabbitMQ Fanout 发布订阅
- 5.2 Direct交换机
- 5.3 Topic主题模式
- 6.MQ如何获取消费者消费结果
- 7.rabbitmq死信队列
- 7.1原理
- 7.2演示
- 7.3应用场景
- 8.RabbitMQ重试策略与幂等性问题
- 8.1重试
- 8.2幂等性问题
参考蚂蚁课堂
1.RabbitMQ基本介绍
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件),RabbitMQ服务器是用Erlang语言编写的。然后我们安装rabbitMQ就行了,安装完之后访问localhost://15672即可进入到rabbitMQ管理平台,然后输入账号密码登录,账号密码为guest,guest。
这里面有一个Virutual Hosts的概念,这个相当于RabbitMQ的虚拟消息服务器VirtualHost,每个VirtualHost相当于一个相对独立的RabbitMQ服务器,每个VirtualHost之间是相互隔离的,exchange,queue,message不能互通。
然后我们看一下RabbitMQ常见的端口号
15672 — RabbitMQ管理平台的端口号
25672 — 集群通信端口号
Amqp 5672 — RabbitMQ内部通信的一个端口号
2.RabbitMQ简单使用案例
RabbitMQ使用的一般步骤首先要在管理平台端创建一个队列,然后编写生产者代码,然后编写消费者代码。
2.1在RabbitMQ平台上创建一个队列
首先先创建一个VirtualHost,然后设置Permission设置成自己的账号guest
然后我们就可以在这个VirtualHost里面创建Queue。
添加队列的时候要指明这个队列属于哪个VirtualHost,然后设置一个队列名称,然后Durability设置是否可以持久化。然后添加就完事了。
2.2编写生产者代码
这个生产者的任务就是向消息队列中投放消息,首先他必须要和RabbitMQ建立一个连接。
public class RabbitMQConnection {
public static Connection getConnection() throws IOException, TimeoutException {
//1.创建connectionFactory
ConnectionFactory connectionFactory = new ConnectionFactory();
//2.配置Host
connectionFactory.setHost("127.0.0.1");
//3.设置Port
connectionFactory.setPort(5672);
//4.设置账户和密码
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
//5.设置VirtualHost
connectionFactory.setVirtualHost("/wjzVirtualHost");
return connectionFactory.newConnection();
}
}
配置好我们的IP地址端口号和VirtualHost。然后我们编写生产者代码
public class Producer {
private static final String QUEUE_NAME = "wjz-queue";
public static void main(String[] args) throws IOException, TimeoutException {
//1.创建一个新连接
Connection connection = RabbitMQConnection.getConnection();
//2.设置channel
Channel channel = connection.createChannel();
//3.发送消息
String msg = "wjz,nb!!!";
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
System.out.println("消息投递成功");
channel.close();
connection.close();
}
}
生产者这边要建立和RabbitMQ服务器之间的连接,然后向消息队列中发送消息,然后关闭连接。然后basicPublish第一个参数是交换机我们这个比较简单就没有,然后第二个是消息队列名称你的队列的名叫啥你就写啥,第三个参数是props一些配置信息比如说过期时间,优先级,投递模式之类的,第四个参数是消息。
然后我们运行一下看看这条消息有没有被投递到MQ服务器。
如图所示,队列里的Ready为1,然后我们下一步就是把消息队列里的消息从队列中取出来。
2.3编写消费者代码
public class Consumer {
private static final String QUEUE_NAME = "wjz-queue";
public static void main(String[] args) throws IOException, TimeoutException, IOException, TimeoutException {
// 1.创建连接
Connection connection = RabbitMQConnection.getConnection();
// 2.设置通道
Channel channel = connection.createChannel();
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消费者获取消息:" + msg);
// 消费者完成 消费该消息
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 3.监听队列
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
}
}
消费者我们也是首先创建一个连接,然后设置通道,在这个通道里面拿到消息之后消费掉。最后这个监听队列这个,第二个参数如果设置为true就是自动签收,我获取成功了就会立即从消息队列中移除。但是如果说我这个消息正在处理然后处理失败了,这个时候我要做一个补偿,但是这个时候MQ里已经没有了这条消息。这是不对的所以我们一般都是手动签收把这个参数设置为false。手动签收就是我消费者收到消息之后如果成功消费了会发给MQ一个通知这个时候MQ才会清除。
好了解释完了运行一下看看结果。
3.RabbitMQ如何保证消息不丢失
MQ服务器端在默认的情况下都会对队列中的消息实现持久化。所以就算MQ宕机里面的消息也不会丢失。
如图所示为了保证消息不丢失RabbitMQ有一个消息确认机制,生产者向队列投递一条消息,同时MQ回向生产者返回一个消息确认。这样就确保了生产者投递信息到MQ一定是成功的。这个MQ发回给生产者的Ack可以使用同步或者异步机制,同步的话就是如果MQ服务器不返回确认生产者就会阻塞,异步的话就是有一个观察者监听是否投递成功,如果投递成功MQ返回一个确认给生产者。
消费者必须要将消息消费成功之后就会发一个通知给MQ服务器让MQ把消息从队列当中删除。
4.RabbitMQ工作队列
默认的传统队列是为均摊消费,存在不公平性;如果每个消费者速度不一样的情况下,均摊消费是不公平的。我们希望达到一个能者多劳的效果。这就需要我们采用工作队列。工作队列的实现就是两个消费者能者多劳,能力强的多分配,能力弱的少分配,我们可以通过channel.basicQos(int);这个函数来设置这个消费者一次要从队列中拉取多少条消息,同时给MQ服务器端发送一个Ack确认消息已经被处理了这样MQ会从队列中删除这条消息。
下面我们可以通过代码来演示一下这个效果
producer
public class Producer {
private static final String QUEUE_NAME = "wjz-queue";
public static void main(String[] args) throws IOException, TimeoutException {
//1.创建一个新连接
Connection connection = RabbitMQConnection.getConnection();
//2.设置channel
Channel channel = connection.createChannel();
//3.发送消息
for (int i = 0; i < 10; i++) {
String msg = "wjz nb:i" + i;
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
}
System.out.println("消息投递成功");
channel.close();
connection.close();
}
}
首先这个生产者会和MQ服务器建立一个连接,然后向MQ中投放10条消息。
consumer1
public class Consumer1 {
private static final String QUEUE_NAME = "wjz-queue";
public static void main(String[] args) throws IOException, TimeoutException, IOException, TimeoutException {
// 1.创建连接
Connection connection = RabbitMQConnection.getConnection();
// 2.设置通道
Channel channel = connection.createChannel();
//指定我们消费者每次批量获取消息
channel.basicQos(2);
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消费者获取消息:" + msg);
try {
// 消费者完成 删除该消息
channel.basicAck(envelope.getDeliveryTag(), false);
}catch (Exception e){
}
//
}
};
// 3.监听队列
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
}
}
消费者1也是要和MQ服务器建立一个连接,然后拉取MQ中的消息,channel.basicQos(2)参数为2说明一次要拉取2条消息。然后消息处理成功之后向MQ服务器发出确认,这时候MQ服务器就会把这两条消息从队列中删除。
public class Consumer2 {
private static final String QUEUE_NAME = "wjz-queue";
public static void main(String[] args) throws IOException, TimeoutException, IOException, TimeoutException {
// 1.创建连接
Connection connection = RabbitMQConnection.getConnection();
// 2.设置通道
Channel channel = connection.createChannel();
channel.basicQos(1);
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(1000);
} catch (Exception e) {
}
String msg = new String(body, "UTF-8");
System.out.println("消费者获取消息:" + msg);
// 消费者完成 删除该消息
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 3.监听队列
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
}
}
消费者2和消费者1的区别是他处理消息有1s的延时,代表他处理能力弱。所以我们让他一次获取1条消息。
然后我们看一下结果
这就很明显了能力较强的消费者获取了9条消息,较弱的获取了1条消息,这就是工作队列要达到的效果。
5.RabbitMQ交换机(exchange)
假如说有两种消息队列,我的生产者要向这个队列里面投递消息,邮件消费者和短信消费者,分别去消费这两个队列里的信息。如果这个队列有很多种那么生产者这边的代码就会写的很长,造成大量冗余,这就需要一个交换机,交换机会自动路由选择生产者生产的消息投递到不同的队列中去。综上所述交换机的作用就是路由消息存放在哪个队列当中,里面有一个路由key作为分发的规则。
5.1RabbitMQ Fanout 发布订阅
fanout发布订阅原理
1.需要创建两个队列,每个队列对应一个消费者;
2.队列需要绑定我们的交换机
3.生产者投递消息到交换机中,通过交换机分发给这两个队列存放起来。
4.消费者从队列中提取这个消息
然后我们通过代码演示一下这个效果。
生产者代码
public class ProducerFanout {
/**
* 定义交换机的名称
*/
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建Connection
Connection connection = RabbitMQConnection.getConnection();
// 创建Channel
Channel channel = connection.createChannel();
// 通道关联交换机
channel.exchangeDeclare(EXCHANGE_NAME, "fanout", true);
String msg = "wjz, nb!!";
channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes());
channel.close();
connection.close();
}
}
我们可以看到他会定义交换机的名称同时连接这个交换机。,之后就会向这个交换机里发送消息。
邮件消费者
public class MailConsumer {
/**
* 定义邮件队列
*/
private static final String QUEUE_NAME = "fanout_email_queue";
/**
* 定义交换机的名称
*/
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("邮件消费者...");
// 创建我们的连接
Connection connection = RabbitMQConnection.getConnection();
// 创建我们通道
final Channel channel = connection.createChannel();
// 关联队列消费者关联队列
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("邮件消费者获取消息:" + msg);
}
};
// 开始监听消息 自动签收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
邮件消费者关联到我们的邮件队列。同理短信消费者也是一样。
public class SmsConsumer {
/**
* 定义短信队列
*/
private static final String QUEUE_NAME = "fanout_email_sms";
/**
* 定义交换机的名称
*/
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("短信消费者...");
// 创建我们的连接
Connection connection = RabbitMQConnection.getConnection();
// 创建我们通道
final Channel channel = connection.createChannel();
// 关联队列消费者关联队列
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("短信消费者获取消息:" + msg);
}
};
// 开始监听消息 自动签收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
然后我们在RabbitMQ控制台创建这些交换机和队列。
rabbitMQ创建交换机
注意这个交换机类型得选择fanout。然后我们看一下启动之后的运行结果
首先启动两个消费者,他们等待消费,然后启动生产者向交换机中投递消息。最后看看消费者能否成功的获取到消息。
最后的结果表明,我生产者向fanout类型的交换机投递消息,那么绑定这个交换机的队列都能拿到。
5.2 Direct交换机
当交换机类型为direct类型时我可以指定路由key,根据这个key将消息分发到指定的队列当中。
direct交换机,当生产者投递一个消息时会带上一个路由key,交换机根据这个路由key来判断应该投递到哪个队列中去。所以我们提前在RabbitMQ控制台上创建好交换机以及相应的队列。
然后添加相应的队列。
然后我们来看一下direct交换机的演示代码
public class ProducerDirect {
/**
* 定义交换机的名称
*/
private static final String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建Connection
Connection connection = RabbitMQConnection.getConnection();
// 创建Channel
Channel channel = connection.createChannel();
// 通道关联交换机
channel.exchangeDeclare(EXCHANGE_NAME, "direct", true);
String msg = "wjz,nb!!!";
channel.basicPublish(EXCHANGE_NAME, "email", null, msg.getBytes());
channel.close();
connection.close();
}
}
在这里我们指定了该生产者的路由key为email,所以他的消息会被投放到邮件队列当中。
邮件消费者
public class MailConsumer {
/**
* 定义邮件队列
*/
private static final String QUEUE_NAME = "direct_email_queue";
/**
* 定义交换机的名称
*/
private static final String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("邮件消费者...");
// 创建我们的连接
Connection connection = RabbitMQConnection.getConnection();
// 创建我们通道
final Channel channel = connection.createChannel();
// 关联队列消费者关联队列
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "email");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("邮件消费者获取消息:" + msg);
}
};
// 开始监听消息 自动签收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
这个是邮件消费者的代码,通过queueBind方法绑定了队列,和交换机,同时也指定了路由key是email。
短信消费者
public class SmsConsumer {
/**
* 定义短信队列
*/
private static final String QUEUE_NAME = "direct_sms_queue";
/**
* 定义交换机的名称
*/
private static final String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("短信消费者...");
// 创建我们的连接
Connection connection = RabbitMQConnection.getConnection();
// 创建我们通道
final Channel channel = connection.createChannel();
// 关联队列消费者关联队列
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "sms");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("短信消费者获取消息:" + msg);
}
};
// 开始监听消息 自动签收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
这个是短信消费者的代码,通过queueBind方法绑定了队列,和交换机,同时也指定了路由key是sms。然后我们运行一下上述程序看看结果。
根据上述结果我们可以看到,消息被投递到了邮件队列,最后邮件消费者消费,而短信消费者没有收到消息。综上所述direct_exchange是根据路由key投放到不同的消息队列当中。
5.3 Topic主题模式
当交换机类型为topic类型时,根据队列绑定的路由key模糊转发到具体的队列中存放。
#表示支持匹配多个词,*表示只能匹配一个词。
如图所示,生产者发送消息key为a.sms,说明这个主题就是a,后面的队列需要订阅到这个主题,订阅的方式就是在路由key上绑定为和当前生产者相同的主题后面模糊匹配,如图所示生产者投递的消息就会放到短信队列当中,最后会被短信消费者消费。下面我们通过代码演示一下。
(在RabbitMQ添加队列和交换机的步骤略)
生产者
public class ProducerTopic {
/**
* 定义交换机的名称
*/
private static final String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建Connection
Connection connection = RabbitMQConnection.getConnection();
// 创建Channel
Channel channel = connection.createChannel();
// 通道关联交换机
channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);
String msg = "wjz,nb";
channel.basicPublish(EXCHANGE_NAME, "wjz.sms", null, msg.getBytes());
channel.close();
connection.close();
}
}
邮件消费者
public class MailConsumer {
/**
* 定义邮件队列
*/
private static final String QUEUE_NAME = "topic_email_queue";
/**
* 定义交换机的名称
*/
private static final String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("邮件消费者...");
// 创建我们的连接
Connection connection = RabbitMQConnection.getConnection();
// 创建我们通道
final Channel channel = connection.createChannel();
// 关联队列消费者关联队列
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "xxx.*");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("邮件消费者获取消息:" + msg);
}
};
// 开始监听消息 自动签收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
短信消费者
public class SmsConsumer {
/**
* 定义短信队列
*/
private static final String QUEUE_NAME = "topic_sms_queue";
/**
* 定义交换机的名称
*/
private static final String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("短信消费者...");
// 创建我们的连接
Connection connection = RabbitMQConnection.getConnection();
// 创建我们通道
final Channel channel = connection.createChannel();
// 关联队列消费者关联队列
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "wjz.*");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("短信消费者获取消息:" + msg);
}
};
// 开始监听消息 自动签收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
然后我们看一下结果
消费者订阅了wjz这个主题所以他会收到生产者投递来的消息。
6.MQ如何获取消费者消费结果
如图所示这是一个订单服务的流程,首先生产者会投递消息给交换机,这条消息就是类似于我要下单的一个通知,然后交换机会给订单队列发送消息,然后订单的消费者会从队列中获取这个消息,然后将订单插入到数据库当中。但是我的生产者无法得知这条消息是否插入成功。
这个解决方案如下图所示
首先生产者将消息投递到交换机中,然后MQ服务器端返回一个全局事务Id给生产者。然后生产者这一端每隔两秒钟查看数据库看看这条消息是否成功被消费。这个流程在rabbitmq中没有被实现,在rocketmq中实现了这个功能。
7.rabbitmq死信队列
死信队列俗称备胎队列,消息中间件因为某种原因拒收该消息后,可以转移到死信队列中存放,死信队列也可以有交换机和路由key等。
7.1原理
当以下几种情况出现时订单队列中的msg会向死信队列中转移。
- 当生产者向订单交换机投递消息,分发到订单队列此时消费者没有及时获取到我们的消息,消息在队列中过期之后,会转移到备胎死信队列存放。
- 队列达到最大长度时会转移到我们的死信队列当中。
- 订单消费者的代码本身有bug,所以无法消费消息队列中的消息,消息消费失败,这时候消费者一定要重试,但是消费者有bug无论重试多少次也失败所以就会放到死信队列里。
7.2演示
我们演示的效果是我有一个生产者我上来会将消息投递到订单交换机,交换机会把消息分发给订单队列,但是我把订单消费者注释掉这样就不会有人消费订单队列的消息。这时订单队列就会把消息给死信队列也就是备胎,然后死信消费者会消费死信队列的消息。
config
@Component
public class DeadLetterMQConfig {
/**
* 订单交换机
*/
@Value("${wjz.order.exchange}")
private String orderExchange;
/**
* 订单队列
*/
@Value("${wjz.order.queue}")
private String orderQueue;
/**
* 订单路由key
*/
@Value("${wjz.order.routingKey}")
private String orderRoutingKey;
/**
* 死信交换机
*/
@Value("${wjz.dlx.exchange}")
private String dlxExchange;
/**
* 死信队列
*/
@Value("${wjz.dlx.queue}")
private String dlxQueue;
/**
* 死信路由
*/
@Value("${wjz.dlx.routingKey}")
private String dlxRoutingKey;
/**
* 声明死信交换机
*
* @return DirectExchange
*/
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange(dlxExchange);
}
/**
* 声明死信队列
*
* @return Queue
*/
@Bean
public Queue dlxQueue() {
return new Queue(dlxQueue);
}
/**
* 声明订单业务交换机
*
* @return DirectExchange
*/
@Bean
public DirectExchange orderExchange() {
return new DirectExchange(orderExchange);
}
/**
* 声明订单队列
*
* @return Queue
*/
@Bean
public Queue orderQueue() {
// 订单队列绑定我们的死信交换机
Map<String, Object> arguments = new HashMap<>(2);
arguments.put("x-dead-letter-exchange", dlxExchange);
arguments.put("x-dead-letter-routing-key", dlxRoutingKey);
return new Queue(orderQueue, true, false, false, arguments);
}
/**
* 绑定死信队列到死信交换机
*
* @return Binding
*/
@Bean
public Binding binding() {
return BindingBuilder.bind(dlxQueue())
.to(dlxExchange())
.with(dlxRoutingKey);
}
/**
* 绑定订单队列到订单交换机
*
* @return Binding
*/
@Bean
public Binding orderBinding() {
return BindingBuilder.bind(orderQueue())
.to(orderExchange())
.with(orderRoutingKey);
}
}
producer
@RestController
public class OrderProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 订单交换机
*/
@Value("${wjz.order.exchange}")
private String orderExchange;
/**
* 订单路由key
*/
@Value("${wjz.order.routingKey}")
private String orderRoutingKey;
@RequestMapping("/sendOrder")
public String sendOrder() {
String msg = "wjz,nb";
rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, msg, message -> {
// 设置消息过期时间 10秒过期
message.getMessageProperties().setExpiration("10000");
return message;
});
return "success";
}
}
dead-consumer
@Slf4j
@Component
public class OrderDlxConsumer {
/**
* 死信队列监听队列回调的方法
*
* @param msg
*/
@RabbitListener(queues = "wjz_order_queue")
public void orderConsumer(String msg) {
log.info(">死信队列消费订单消息:msg{}<<", msg);
}
}
order-onsumer
@Component
@Slf4j
public class OrderConsumer {
/**
* 监听队列回调的方法
*
* @param msg
*/
@RabbitListener(queues = "wjz_order_queue")
public void orderConsumer(String msg) {
log.info(">>正常订单消费者消息MSG:{}<<", msg);
}
}
如图所示我不注释订单消费者订单消费者消费。
注释掉订单消费者后消息会转移到死信队列当中让死信队列去消费。
7.3应用场景
订单超时回滚的设计:这个我们其实可以通过redis去设计,将订单号作为key,设置一个超时时间如果超时了就取通知客户端将数据库回滚。我们也可以通过死信队列的方式设计,我们可以创建一个普通的队列没有对应的消费者消费消息,在规定的过期时间后就会将该消息转移到死信备胎队列,然后我们在死信消费者里查询订单号码是否已经成功支付,如果没成功支付则回滚。
8.RabbitMQ重试策略与幂等性问题
我们有以下这个场景,我们要下一个订单通过mq异步方式去下单,消费者去消费但是,消费者代码有异常,说明消息处理失败,所以消费者不断重试,最后数据库中有了很多笔相同的订单。
8.1重试
controller
@RequestMapping("/sendOrder")
public String sendOrder() {
// 生成全局id
String orderId = System.currentTimeMillis() + "";
log.info("orderId:{}", orderId);
String orderName = "每特教育svip课程报名";
orderProducer.sendMsg(orderName, orderId);
return orderId;
}
producer
public void sendMsg(String orderName, String orderId) {
OrderEntity orderEntity = new OrderEntity(orderName, orderId);
rabbitTemplate.convertAndSend("/wjz_order", "", orderEntity, message -> {
return message;
});
}
consumer
@Slf4j
@Component
@RabbitListener(queues = "fanout_order_queue")
public class FanoutOrderConsumer {
@Autowired
private OrderManager orderManager;
@Autowired
private OrderMapper orderMapper;
@RabbitHandler
public void process(OrderEntity orderEntity, Message message, Channel channel) throws IOException {
log.info(">>orderEntity:{}<<", orderEntity.toString());
String orderId = orderEntity.getOrderId();
if (StringUtils.isEmpty(orderId)) {
return;
}
int result = orderManager.addOrder(orderEntity);
int i = 1 / 0;
log.info(">>插入数据库中数据成功<<");
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
为了一会重试次数不要太多我们可以在yml文件当中规定一下重试的相关规则这样他就不会一直重试。
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /wjzVirtualHost
listener:
simple:
retry:
####开启消费者(程序出现异常的情况下会)进行重试
enabled: true
####最大重试次数
max-attempts: 5
####重试间隔时间
initial-interval: 3000
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
然后我们启动这个服务然后访问到发送到sendOrder这个接口,观察数据库的情况。
我们可以观察到订单出现重复,那是因为当我们消费者处理执行我们业务代码的时候如果抛出异常的情况下,在这个时候mq会自动触发重试机制,默认情况下rabbitmq是无限次数的重试,所以需要人为指定重试次数。
一般情况下,我们消费者获取消息,调用第三方接口,但是第三方接口失败需要进行重试,因为可能是因为网络延迟暂时调不通,重试多次可能调通。但是消费者代码本身的问题,就没必要重试了
8.2幂等性问题
当我们消费者处理执行业务代码时,如果抛出异常的情况下在这个时候mq会自动触发重试机制,mq在重试过程中,有可能引发消费者重复消费的问题。这就是消息幂等性问题。幂等性保证了数据库结果唯一。对于这个问题我们有如下解决方案。
生产者在投递消息的时候会生成一个全局唯一Id,放在我们的消息中,消费者获取到我们的该消息,可以根据该全局唯一id实现去重复。如果业务逻辑是insert操作那就判断一下之前有没有如果没有就插入 ,有的话就不插入。如果业务逻辑是update操作,使用乐观锁。
我们看一下全局唯一Id的效果。
@Slf4j
@Component
@RabbitListener(queues = "fanout_order_queue")
public class FanoutOrderConsumer {
@Autowired
private OrderManager orderManager;
@Autowired
private OrderMapper orderMapper;
@RabbitHandler
public void process(OrderEntity orderEntity, Message message, Channel channel) throws IOException {
log.info(">>orderEntity:{}<<", orderEntity.toString());
String orderId = orderEntity.getOrderId();
if (StringUtils.isEmpty(orderId)) {
return;
}
OrderEntity dbOrderEntity = orderMapper.getOrder(orderId);
if (dbOrderEntity != null) {
log.info("另外消费者已经处理过该业务逻辑");
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
return;
}
int result = orderManager.addOrder(orderEntity);
int i = 1 / 0;
log.info(">>插入数据库中数据成功<<");
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
我们可以根据订单Id查询数据库看看里面有没有相应的订单如果没有再插入有就算了。我们运行看一下结果。
这样就算报错了重试也不会有重复的数据了。