MQ消息的可靠性
消息的可靠性即避免消息丢失。
代码以RabbitMQ为例。
Channel:
void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;
String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException;
消息从Producer到Broker
如果发送成功,Broker返回Ack。
事务模式
Channel:
Tx.SelectOk txSelect() throws IOException; // 开启事务模式
Tx.CommitOk txCommit() throws IOException; // 提交
Tx.RollbackOk txRollback() throws IOException; // 回滚
确认模式
Channel:
Confirm.SelectOk confirmSelect() throws IOException; // 开启确认模式
// 同步确认
boolean waitForConfirms() throws InterruptedException; // 是否所有消息被确认
boolean waitForConfirms(long timeout) throws InterruptedException, TimeoutException; // 带超时时间,是否所有消息被确认
void waitForConfirmsOrDie() throws IOException, InterruptedException; // 确认所有消息,如果有有未确认抛IOException
void waitForConfirmsOrDie(long timeout) throws IOException, InterruptedException, TimeoutException; // 带超时时间,确认所有消息,如果有未确认抛IOException
// 异步确认
void addConfirmListener(ConfirmListener listener);
public interface ConfirmListener {
// multiple为true,即确认多个消息时,deliveryTag为最后一个确认的消息序号(消息序号从1开始)
// multiple为false,即确认单个消息时,deliveryTag为确认的消息序号(消息序号从1开始)
void handleAck(long deliveryTag, boolean multiple)
throws IOException;
void handleNack(long deliveryTag, boolean multiple)
throws IOException;
}
消息从Exchange到Message Queue
失败的可能原因和解决方案
1.交换机未持久化
解决方案:
Channel:
Exchange.DeclareOk exchangeDeclare(String exchange,
String type,
boolean durable,
boolean autoDelete,
boolean internal,
Map<String, Object> arguments) throws IOException;
durable设为true
2.队列未持久化
解决方案:
Channel:
Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) throws IOException;
durable设为true
3.路由键和绑定键不匹配
解决方案:
指定交换机的备份交换机,到达交换机的消息没有对应的队列,将消息发送给备份交换机。
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("alternate-exchange","TEST_ALTERNATE_EXCHANGE");
channel.exchangeDeclare("TEST_EXCHANGE","topic", false, false, false, arguments);
或者回发消息,告知生产者。
消息在队列中
丢失的可能原因和解决方案
1.消息未持久化
解决方案:
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.deliveryMode(2); // 2代表持久化
Channel:
void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;
消息从Message Queue到Consumer
Channel:
String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException;
GetResponse basicGet(String queue, boolean autoAck) throws IOException;
注:basicConsume是监听器将队列中的消息推送给消费者;basicGet是消费者从队列中拉取。
自动Ack:autoAck为true,消费者接收到就返回Ack。
手动Ack:autoAck为为false;调用basicAck,消费者接收到消息,手动返回Ack,如处理完消息后确认:
Channel:
void basicAck(long deliveryTag, boolean multiple) throws IOException; // 确认
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException; // 拒绝一个或多个
void basicReject(long deliveryTag, boolean requeue) throws IOException;// 拒绝一个
requeue可以控制是否将消息重新入队。
如果消息持久化了,没有确认,或者拒绝后重新入队还没有被消费,重启RabbitMQ后会造成消息重复消费。
如果队列只对应一个消费者,每次都拒绝消息然后重新入队,会造成死循环。
注意:Ack不会到达生产者。
生产者知道消费者消费了消息
消费者回调
消息者执行生产者预定义的API(Http或TCP接口)回复消息。
补偿机制
方案一:生产者定时检查消费者回调超时的消息,重发消息。
方案二:消费者本地消息表记录超时未确认的消息,发送给生产者,生产者重发消息。
衰减机制
重发消息后又回调超时可以采取衰减机制,重发间隔越来越大,如:1分钟重发、2分钟重发、5分钟重发、10分钟重发…
注意控制重发次数,不能无限次重发。
消息幂等性
重发消息需保证消息不会被重复消费。
消息需有唯一标识,如转账消息对应有流水号(转账双方的ID + 转账时间戳)。
关于转账时间戳,可以从毫秒级推进至微秒或纳秒级,或者在同一毫秒内不允许再次支付:支付过于频繁,请稍后重试。
微信在转账双方相同,转账时间戳很接近时,会有提示:你已在当前商户支付过一笔相同金额的订单,请确认是否继续支付。防止用户转账误操作。
最终一致
可以引入对账机制防止最终的消息丢失。如银行和支付宝、银联的夜间对账(以银行的核心系统为准)。(支付宝在各大银行创建账户。支付宝用户跨行转账,会成为同一银行内用户和支付宝账户的转账)
消息的顺序性
一个队列对应多个消费者,即使消息被顺序取出,但消费者消费消息的速度不同,不能保证消息被顺序消费。如果消息有顺序性的需求(如发布微博,发表评论),可以一个队列只对应一个消费者进行消费。