消息如何保障100%投递成功
什么是生产端的可靠性投递?
- 保障消息的成功发出
- 保障MQ节点的成功接收
- 发送端收到MQ节点(Broker)的确认应答
- 完善的消息进行补偿机制
BAT/TMD 互联网大厂的解决方案
- 消息落库,对消息状态进行达标
1、消息入库(业务库、消息库),大场景并发一般不考虑事务,影响效率。采用补偿的方式确认消息发送
2、消息发送
3、确认应答(confirm listener 异步监听)
4、更改消息状态为成功发送
5、从消息库获取未成功消息
6、再次发送
7、设定重发次数上限,超出上限设为发送失败
保障MQ如果采用上述第一种方法,是否适用于高并发场景? - 消息的延迟投递,做二次确认,回调检查(主流方案)
主要目的。减少数据库的读写操作。
UpstreamServer:上游系统生产端
DownstreamServer : 下游系统消费端
MQserver:消息中间件(rabbitm)
Callback Server:回调服务
消息首先完成业务入库
1、发送消息
2、再次发送(延迟消息投递)
3、消费端监听,接收消息
4、返回消息接收确认(新消息)
5、callback监听确认消息(消息入库)
6、callback监听延迟消息,检查消息库
7、RPC进行未完成的消息再次投递
幂等性
在海量订单产生的业务高峰期,如何避免消息的重复消费问题?
- 唯一id+指纹码 机制,利用数据库主键去重
1、select count(1)from t_table where id = 唯一id+指纹码
2、好处:实现简单
3、坏处:高并发下数据库写入的性能瓶颈
4、解决方案:跟进id进行分库分表进行算法路由 - 利用redis的原子性操作
1、是否需要进行数据库入库,如果入库,关键解决的问题是数据库和缓存如何做到原子性,一致性。
2、如果不落库,那么都存储到缓存,如何设定当时同步策略
Confirm
- 消息的确认,是指生产者投递消息后,如果Broker收到消息,测绘给我们生产者一个应答。
- 生产者进行接收应答,用来确认这条消息是否正常的发送到broker,这种方式也是消息的可靠性投递的核心保障
如何实现confirm确认消息
1、在channel上开启确认模式:channel.confirmSelect();
2、在channel上添加监听:addConfirmListener,监听成功和失败的返回结果,根据具体的结果对消息进行重发、或记录日志等后续处理。
创建生产着
public static final String EXCHANGE_NAME = "test_exchange_confirm";
public static final String ROUTING_KEY = "routingkey.confirm";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionsUtils.getConnection();
// 创建信道
Channel channel = connection.createChannel();
//开启确认模式
channel.confirmSelect();
// 发送消息
String message = "test message for confirm";
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, null, message.getBytes());
System.out.println("发送成功:" + message);
// 添加确认监听
channel.addConfirmListener(new ConfirmListener() {
/**
* @param deliveryTag 消息唯一标签
* @param multiple 批量
*/
@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("---no-ack---");
}
});
}
创建消费者
public static final String EXCHANGE_NAME = "test_exchange_confirm";
public static final String EXCHANGE_TYPE = "direct";
public static final String ROUTING_KEY = "routingkey.confirm";
public static final String QUEUE_NAME = "test_queue_confirm";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionsUtils.getConnection();
// 创建信道
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, EXCHANGE_TYPE, true, false, false ,null);
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
// 创建消费者进行监听获取消息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("接收消息message: " + new String(body));
}
};
channel.basicConsume(QUEUE_NAME, true, consumer);
}
启动消费者进行监听,进行消息发送
Return
return listener 用于处理一些不可路由的消息
- 消息生产者通过指定一个exchange和routingkey吧消息发送到某个队列中,然后小给这进行监听队列,进行消费处理
- 如果在发送消息的时候,当前的exchange不存在或者指定的routingkey路由不到,这个时候如果我们需要监听这种不可达消息,就需要使用return listener。
基础api的关键配置项 - Mandatory:如果为true,则监听器会接收到路由不可达的消息,然后进行后续的处理,如果为false,那么broker端会自动删除该消息
创建生产者
public static final String EXCHANGE_NAME = "test_exchange_return";
// public static final String ROUTING_KEY = "routingkey.return";
public static final String ROUTING_KEY = "routingkey.return.abc";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionsUtils.getConnection();
// 创建信道
Channel channel = connection.createChannel();
//开启确认模式
channel.confirmSelect();
// 发送消息
String message = "test message for 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.out.println("---return---");
System.out.println("replyCode: " + replyCode);
System.out.println("replyText: " + replyText);
System.out.println("exchange: " + exchange);
System.out.println("routingKey: " + routingKey);
System.out.println("properties: " + properties);
System.out.println("body: " + new String(body));
}
});
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY,true, null, message.getBytes());
System.out.println("发送成功:" + message);
}
创建消费者
public static final String EXCHANGE_NAME = "test_exchange_return";
public static final String EXCHANGE_TYPE = "topic";
public static final String ROUTING_KEY = "routingkey.*";
public static final String QUEUE_NAME = "test_queue_return";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionsUtils.getConnection();
// 创建信道
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, EXCHANGE_TYPE, true, false, false ,null);
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
// 创建消费者进行监听获取消息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("接收消息message: " + new String(body));
}
};
channel.basicConsume(QUEUE_NAME, true, consumer);
}
我们采用topic模式的exchange,消费端采用的routingkey是routingkey.*,也就是说生产者发送ROUTING_KEY = "routingkey.return"的时候,消费者可以接受到消息,换成ROUTING_KEY = "routingkey.return.abc“的时候,就可以接收到返回的信息,前提是生产者发送消息时 mandatory参数一定要设置为true,否则会被自动删除消息。我们运行看下结果
- 使用ROUTING_KEY = “routingkey.return”
- 使用ROUTING_KEY = “routingkey.return.abc”