生产端消息的可靠性投递方案
何为生产端的可靠性投递
- 保障消息的成功发出
- 保障
MQ节点成功接收 - 发送端收到
MQ节点(Broker)确认应答(已收到) - 完善消息进行补偿机制
可靠性投递的方案(不加事务)
1. 消息落库(持久化至数据库),对消息状态进行打标,如若消息未响应,进行轮询操作。
-
Step1:把业务消息落库,再生成一条消息落库到消息DB用来记录(譬如消息刚创建,正在发送中
status: 0)。(缺点:对数据库进行两次持久化) -
Step2:生产端发送消息。
-
Step3:
Broker端收到后,应答至生产端。Confirm Listener异步监听Broker的应答。 -
Step4:应答表明消息投递成功后,去消息DB中抓取到指定的消息记录,更新状态,如
status: 1 -
Step5:如在Step3中出现网络不稳定等情况,导致
Listener未收到消息成功确认的应答。那么消息数据库中的status就还是0,而Broker可能是接收到消息的状态。因此设定一个规则(定时任务),例如消息在落库5分钟后(超时)还是0的状态,就把该条记录抽取出来。 -
Step6:重新投递
-
Step7:限制一个重试的次数,譬如3次,如果大于3次,即为投递失败,更新
status的值。(用补偿机制去查询消息失败的原因,人工)
2. 消息的延迟投递,做二次确认,回调检查。(高并发场景)
-
Upstream service:生产端 -
Downstream service:消费端 -
Step1:业务消息落库后,发送消息至
Broker。 -
Step2:紧接着发送第二条延迟(设置延迟时间)检查的消息。
-
Step3:消费端监听指定的队列接收到消息进行处理
-
Step4:处理完后,生成一条响应消息发送到
Broker。 -
Step5:由
Callback服务去监听该响应消息,收到该响应消息后持久化至消息DB(记录成功状态)。 -
Step6:到了延迟时间,延迟发送的消息也被
Callback服务的监听器监听到后,去检查消息DB。如果未查询到成功的状态,Callback服务需要做补偿,发起RPC通讯,让生产端重新发送。生产端通过介绍到的命令中所带的id去数据库查询该业务消息,再重新发送,即跳转到Step1。
该方案减少了对数据库的存储,保证了性能。
消费端的幂等性保障
幂等性的概念
通俗的说就是执行N次操作的结果是相同的。
借鉴数据库的乐观锁机制。
执行一条更新数据库的SQL语句:
(避免并发问题,添加一个版本号,执行过减操作后递增version,就不会重复减)
UPDATE T_REPS SET COUNT = COUNT - 1,VERSION = VERSION + 1 WHERE VERSION = 1
消费端保障幂等性
避免消息的重复消费: 消费端实现幂等性,接收到多条相同的消息,但不会重复消费,即收到多条一样的消息。
方案:
-
唯一ID + 指纹码机制
唯一ID + 指纹码(业务规则、时间戳等拼接)机制,利用数据库主键去重SELECT COUNT(1) FROM T_ORDER WHERE ID = 唯一ID + 指纹码未查询到就insert,如有说明已处理过该消息,返回失败。- 优点:实现简单
- 缺点:高并发下有数据库写入的性能瓶颈
- 解决方案:根据ID进行分库分表、算法路由
-
利用
Redis的原子性
需要考虑的问题:
是否要落库数据库,如落库,数据库和缓存如何做到数据的一致性?
不落库,数据存储在缓存中,如何设置定时同步的策略(可靠性保障)?
Confirm确认消息
Confirm消息确认机制的概念
指生产者投递消息后,如果Broker收到消息,则会给生产者一个应答。
生产者进行接收应答,用来确认这条消息是否正常发送到Broker。
是消息可靠性投递的核心保障。
确认机制的流程图
发送消息与监听应答的消息是异步操作。
确认消息的实现
- 在
channel开启确认模式:channel.confirmSelect(); - 在
channel添加监听:channel.addConfirmListener(ConfirmListener listener); 返回监听成功和失败的结果,对具体结果进行相应的处理(重新发送、记录日志等待后续处理等)
具体代码:
Producer
public class ConfirmProducer {
private static final String EXCHANGE_NAME = "confirm_exchange";
private static final String ROUTING_KEY = "confirm.key";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.58.129");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/test");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
// 指定消息的投递模式: 确认模式
channel.confirmSelect();
// 发送消息
String msg = "Send message of confirm demo";
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, null, msg.getBytes());
// 添加确认监听
channel.addConfirmListener(new ConfirmListener() {
// 成功
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("========= Ack ========");
}
// 失败
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("========= Nack ========");
}
});
}
}
Consumer
public class ConfirmConsumer {
private static final String EXCHANGE_NAME = "confirm_exchange";
private static final String ROUTING_KEY = "confirm.#";
private static final String QUEUE_NAME = "confirm_queue";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.58.129");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/test");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
// 绑定交换机与队列, 指定路由键
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, true);
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
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("Received message : " + msg);
}
};
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
控制台输出:
Return消息机制
用于处理一些不可路由的消息。
基础API
有一个关键配置项:Mandatory:true,则监听器会接收到路由不可达的消息,然后进行处理;Mandatory:false,Broker会自动删除该消息。默认是false。
流程图
消息的生产者通过制定
Exchange和RoutingKey,把消息投递到某一个队列中,消费者监听队列,进行消费。
但在一些情况下,发送消息时,Exchange不存在或RoutingKey路由不到,Return Listener就会监听这种不可达的消息,然后进行处理。
Return Listener 代码
Consumer
public class ReturnConsumer {
private static final String EXCHANGE_NAME = "return_exchange";
private static final String ROUTING_KEY = "return.#";
private static final String QUEUE_NAME = "return_queue";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.58.129");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/test");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
// 绑定交换机与队列, 指定路由键
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, true);
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Receive Message —— " + new String(body));
}
};
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
Producer
public class ReturnProducer {
private static final String EXCHANGE_NAME = "return_exchange";
private static final String ROUTING_KEY = "return.key";
private static final String ROUTING_KEY_ERROR = "wrong.key";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.58.129");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/test");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
// 消息
String msg = "Send message of return demo";
// 添加并设置Return监听器
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.err.println("============ handleReturn ============");
System.err.println("replyCode —— " + replyCode);
System.err.println("replyText —— " + replyText);
System.err.println("exchange —— " + exchange);
System.err.println("routingKey —— " + routingKey);
System.err.println("properties —— " + properties);
System.err.println("body —— " + new String(body));
}
});
// 设置Mandatory为true, 可以进行后续处理, 不会删除消息。
// channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, true,null, msg.getBytes());
// 发送消息
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY_ERROR, true, null, msg.getBytes());
}
}
handleReturn的参数输出:
具体的处理可以在该方法下编写。
消费端限流
消费端限流的概念
当巨量消息瞬间全部推送时,单个客户端无法同时处理这些数据,服务器容易故障。因此要进行消费端限流。
RabbitMQ提供了一种Qos(服务质量保证)功能,即在非自动确认前提下,如果一定数目的消息未被确认前(通过consume或者channel设置Qos值),不进行消费新消息。
/**
* Request specific "quality of service" settings.
*
* These settings impose limits on the amount of data the server
* will deliver to consumers before requiring acknowledgements.
* Thus they provide a means of consumer-initiated flow control.
* @see com.rabbitmq.client.AMQP.Basic.Qos
* @param prefetchSize maximum amount of content (measured in
* octets) that the server will deliver, 0 if unlimited
* @param prefetchCount maximum number of messages that the server
* will deliver, 0 if unlimited
* @param global true if the settings should be applied to the
* entire channel rather than each consumer
* @throws java.io.IOException if an error is encountered
*/
void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;
prefetchSize:消息限制大小,一般为0,不做限制。prefetchCount:一次处理消息的个数,一般设置为1global:一般为false。true,在channel级别做限制;false,在consumer级别做限制(要手动ack)
代码演示
Consumer
public class QosConsumer {
private static final String EXCHANGE_NAME = "qos_exchange";
private static final String ROUTING_KEY = "qos.#";
private static final String QUEUE_NAME = "qos_queue";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.58.129");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/test");
connectionFactory.setUsername("orcas");
connectionFactory.setPassword("1224");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
// 绑定交换机与队列, 指定路由键
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, true);
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Receive Message —— " + new String(body));
// 手动ack签收
channel.basicAck(envelope.getDeliveryTag(), false); // 不批量签收
}
};
/**
* prefetchSize: 0 不限制消息大小
* prefetchCount: 一次处理消息的个数, ack后继续推送
* global: false 应用在consumer级别
*/
channel.basicQos(0, 1, false);
//限流:autoAck需设置为false, 关闭自动签收
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
}
}
Producer
public class QosProducer {
private static final String EXCHANGE_NAME = "qos_exchange";
private static final String ROUTING_KEY = "qos.key";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.58.129");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/test");
connectionFactory.setUsername("orcas");
connectionFactory.setPassword("1224");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
String msg = "Send message of QOS demo";
for (int i = 0; i < 5; i ++) {
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, true, null, msg.getBytes());
}
}
}
限流需要设置channel.basicQos(0, 1, false);
关闭autoAck,且需要手动签收。
在重写的handleDelivery方法中,如果没有进行手动签收channel.basicAck(),
那么消费端在接收消息时,因为prefetchCount设置为1,只会接收1条消息,剩下的消息的等待中,并不会被推送,直到手动ack后。
队列
消费端ACK与重回队列机制
-
消费端的手工
ACK和NACK:
消费端进行消费时,可能由于业务异常,会调用NACK拒绝确认,而到了一定次数,就直接ACK,将异常消息进行日志的记录,然后进行补偿。 由于服务器宕机等严重问题,消费端没消费成功,重发消息后,需要手工ACK保障消费端消费成功。 -
消费端的重回队列: 将没有处理成功的消息重新回递给
Broker。一般在实际应用中,会关闭重回队列。
TTL队列
TTL:Time To Live,生存时间。
可以指定消息的过期时间。
可以指定队列的过期时间,从消息入队列开始计算,超过了队列的超时时间设置,那么消息会自动清除。
控制台演示:
声明队列,设置TTL时长:
声明交换机:
添加绑定:
发送消息:
十秒后,因为TTL过期,消息消失。
消息的TTL:
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.deliveryMode(2)
.expiration("10000")
.build();
死信队列
DLX:Dead-Letter-Exchange
当消息在队列中变成死信时,能被重新publish到另一个Exchange,该Exchange就是DLX。
发生死信队列的情况:
- 消息被拒绝(
basic.reject/ basic.nack)并且requeue=false(没有重回队列) - 消息
TTL过期 - 队列达到最大长度
死信队列的设置:
- 正常声明交换机,队列并绑定,需要在队列上设置一个参数:
arguments.put("x-dead-letter-exchange", "dlx.exchange"); - 声明死信队列的
Exchange和Queue,然后进行绑定:Exchange: dlx.exchangeQueue: dlx.queueRoutingKey: #
- 在消息过期、
requeue、队列达到最大长度时(即为死信),消息会发送到指定的dlx.exchange交换机上,消费者会监听该交换机所绑定的死信队列。
代码演示:
public class DlxConsumer {
private static final String EXCHANGE_NAME = "dlx_exchange";
private static final String ROUTING_KEY = "dlx.#";
private static final String QUEUE_NAME = "dlx_queue";
// DLX
private static final String DLX_EXCHANGE = "dlx.exchange";
private static final String DLX_QUEUE = "dlx.queue";
private static final String DLX_ROUTING_KEY = "#";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.58.129");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/test");
connectionFactory.setUsername("orcas");
connectionFactory.setPassword("1224");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, true);
// 1. 设置死信队列的参数
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", DLX_EXCHANGE);
channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
// 2. 声明死信队列
channel.exchangeDeclare(DLX_EXCHANGE, BuiltinExchangeType.TOPIC, true, false, null);
channel.queueDeclare(DLX_QUEUE, true, false, false, null);
channel.queueBind(DLX_QUEUE, DLX_EXCHANGE, DLX_ROUTING_KEY);
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Receive Message —— " + new String(body));
// 手动ack签收
channel.basicAck(envelope.getDeliveryTag(), false); // false 不批量签收
}
};
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
}
}