(不加事务)
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
避免消息的重复消费:
消费端实现幂等性,接收到多条相同的消息,但不会重复消费,即收到多条一样的消息。
方案:
1.唯一ID + 指纹码机制
SELECT COUNT(1) FROM T_ORDER WHERE ID = 唯一ID + 指纹码
未查询到就insert
,如有说明已处理过该消息,返回失败2.利用Redis的原子性
需要考虑的问题:
指生产者投递消息后,如果Broker收到消息,则会给生产者一个应答。
生产者进行接收应答,用来确认这条消息是否正常发送到Broker。
是消息可靠性投递的核心保障。
channel.confirmSelect();
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);
}
}
用于处理一些不可路由的消息。
有一个关键配置项:
Mandatory
:true,则监听器会接收到路由不可达的消息,然后进行处理;false,Broker会自动删除该消息。默认是false。
消息的生产者通过制定Exchange和RoutingKey,把消息投递到某一个队列中,消费者监听队列,进行消费。
但在一些情况下,发送消息时,Exchange不存在或RoutingKey路由不到,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:一次处理消息的个数,一般设置为1
global:一般为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和NACK:
消费端进行消费时,可能由于业务异常,会调用NACK拒绝确认,而到了一定次数,就直接ACK,将异常消息进行日志的记录,然后进行补偿。
由于服务器宕机等严重问题,消费端没消费成功,重发消息后,需要手工ACK保障消费端消费成功。
消费端的重回队列:
将没有处理成功的消息重新回递给Broker。
一般在实际应用中,会关闭重回队列。
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
(没有重回队列)死信队列的设置:
arguments.put("x-dead-letter-exchange", "dlx.exchange");
Exchange: dlx.exchange
Queue: dlx.queue
RoutingKey: #
代码演示:
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);
}
}