RabbitMQ消息100%可靠性投递的解决方案实现(一)

慕课网:RabbitMQ消息中间件极速入门与实战 课程

RabbitMQ学习——高级特性

RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性
https://blog.csdn.net/u012211603/article/details/86118129

消息如何保证100%投递成功

什么是生产端的可靠性投递

保障消息的成功发出
保障MQ节点的成功接收
发送端收到MQ节点(Broker)确认应答
完善的消息进行补偿机制

生产端-可靠性传递

常见解决方案:

消息落库,对消息状态进行打标

在这里插入图片描述
BIZ DB:订单数据库(或其他具体业务)
MSG DB:消息数据库
第1步:将订单入库,创建一条MSG(状态为0) 入MSG DB库
第2步:将消息发出去
第3步:监听消息应答(来自Broker)
第4步:修改消息的状态为1(成功)
第5步:分布式定时任务抓取状态为0的消息
第6步:将状态为0的消息重发
第7步:如果尝试了3次(可按实际情况修改)以上则将状态置为2(消息投递失败状态)

这种方案,需要入两次库,在高并发的场景下性能可能不是那么好

消息的延迟投递,做二次确认,回调检查

在这里插入图片描述
第二种方案可以避免消息对象与业务对象同时入库

Upstream service:上游服务,可能为生产端
Downstream service:下游服务,可能为消费端
MQ Broker:可能为集群
Callback service:回调服务,监听confirm消息

第1步:首先业务数据落库,成功才后第一次消息发送
第2步:紧着着发送第2条消息(可以用于寻找第1条消息),用于延迟(可能2,3分钟后才发送)消息投递检查
第3步:Broker端收到消息后,消费端进行消息处理
第4步:处理成功后,发送confirm消息
第5步:收到confirm消息后,将消息进行持久化存储
第6步:收到了delay消息,检查DB数据库,若对应的第1条消息已处理完成,则不做任何事情;若收到了delay消息,检查DB数据库,发现对应的第1条消息处理失败(或无记录),则发送重传命令到上游服务,循环第1步

消息幂等性

幂等性

比如我们执行一条更新库存的SQL语句(乐观锁):

update T_REPS set count = count - 1,version = version + 1 where version = 1

第一步,查出version
第二步,通过这个version进行更新

可以进行幂等性保障
所以,啥是幂等性呢?
执行某个操作,无论执行多少次,结果都是一致的,就说具有幂等性。

如何避免重复消费

在海量订单产生的业务高峰期,如何避免消息的重复消费?

  • 消费端实现幂等性,然后永远不会消费多次,即使收到多条一样的消息

主流解决方案

消费端实现幂等性的主流解决方案有以下两种:
唯一ID +指纹码 机制
利用数据库主键去重

指纹码:可能是业务规则,时间戳+具体银行范围的唯一信息码,能保障这次操作的绝对唯一

比如select count(1) from T_ORDER where id = <唯一ID+指纹码>

将唯一ID+指纹码设成主键,如果上面SQL返回1,说明已经操作了,则不需要再次操作;否则才去执行操作

优点: 实现简单
缺点:高并发下有数据库写入的性能瓶颈(解决方案:通过ID进行分库分表进行算法路由)

利用Redis的原子性实现

  • 通过setnx等命令

SET 订单号 时间戳 过期时间

SET 1893505609317740 1466849127 EX 300 NX

利用Redis进行幂等,需要考虑的问题:

如果要进行数据落库,关键解决的问题是数据库和缓存如何做到数据一致性。
如果不落库,那么都存在缓存中,如何设置定时同步的策略(同步是指将数据存储到数据库中,不落库指的是暂时不落库,不可能永远不落库)

投递消息机制

Confirm确认消息

  • 消息确认,是指生产者消息投递后,如果Broker收到消息,则会给生产者一个应答
  • 生产者进行接收应答,用来确定这条消息是否正常的发送到Broker,这种方式也是消息的可靠性投递的核心保障
    在这里插入图片描述
    生成者发送消息与监听confirm是异步的。

如何实现Confirm确认消息?

  1. 在channel上开启确认模式:channel.confirmSelect()
  2. 在channel上添加监听:addConfirmListener,监听成功和失败的返回结果,根据具体返回结果对消息进行重发或日志记录等
public class Producer {

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Connection connection = ConnectionUtil.getConn();
        //1. 通过connection创建一个Channel
        Channel channel = connection.createChannel();
        //2.指定消息确认模式
        channel.confirmSelect();
        String exchangeName = "test_confirm_exchange";
        String routingKey = "confirm.save";

        //3. 通过Channel发送数据
        String message = "Hello from Producer";
        channel.basicPublish(exchangeName,routingKey,null,message.getBytes());

        //4. 添加一个确认监听
        channel.addConfirmListener(new ConfirmListener() {
            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                //成功的情况 deliveryTag:消息的唯一标签;
                System.out.println("——get ack——");
            }

            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                //失败的情况
                System.out.println("——have no  ack——");
            }
        });

        // 关闭掉就没confirm了
        // CloseTool.closeElegantly(channel,connection);

    }
}

public class Consumer {

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConn();

        //1. 通过connection创建一个Channel
        Channel channel = connection.createChannel();

        String exchangeName = "test_confirm_exchange";
        String routingKey = "confirm.save";
        String queueName = "test_confirm_queue";
        //2. 声明一个exchange
        channel.exchangeDeclare(exchangeName,"topic",true);
        //3. 声明一个队列
        channel.queueDeclare(queueName,true,false,false,null);
        //4. 绑定
        channel.queueBind(queueName,exchangeName,routingKey);
        //5. 创建消费者
        QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
        //6. 设置Channel
        channel.basicConsume(queueName,true,queueingConsumer);
        //7. 获取消息
        while (true) {
            //nextDelivery 会阻塞直到有消息过来
            QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println("收到:" + message);
        }
    }
}

return返回消息

  • Return Listener用于处理一些不可路由消息
  • 生产者指定Exchange和RoutingKey,将消息投递到某个队列,然后消费者监听队列,进行消息处理
  • 但在某些情况下,在发送消息时,若当前的exchange不存在或指定的路由key路由失败,这时,如果需要监听这种不可达的消息,则要使用return listener

return 消息机制

在基础API中有一个关键的配置项:

  • Mandatory : 若为true,则监听器会接收到路由不可达的消息,然后进行后粗处理 ;若为false,则broker端自动删除该消息
    在这里插入图片描述
    发送端发送了一条消息,但是没有发现Exchange,则可通过return listener监听这些消息
public class Producer {
    public static final String MQ_HOST = "192.168.222.101";
    public static final String MQ_VHOST = "/";
    public static final int MQ_PORT = 5672;

    public static void main(String[] args) throws IOException, TimeoutException {
        //1. 创建一个ConnectionFactory
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost(MQ_HOST);//配置host
        connectionFactory.setPort(MQ_PORT);//配置port
        connectionFactory.setVirtualHost(MQ_VHOST);//配置vHost

        //2. 通过连接工厂创建连接
        Connection connection = connectionFactory.newConnection();
        //3. 通过connection创建一个Channel
        Channel channel = connection.createChannel();
        String exchange = "test_return_exchange";
        String routingKey = "return.save";
        String routingKeyError = "abc.save";


        //4. 通过Channel发送数据
        String message = "Hello Return Message";

        channel.addReturnListener((replyCode, replyText, exchange1, routingKey1, properties, body) -> {
            System.out.println("——handle return——");
            System.out.println("replyCode:" + replyCode);
            System.out.println("replyText:" + replyText);
            System.out.println("exchange1:" + exchange1);
            System.out.println("routingKey1:" + routingKey1);
            System.out.println("properties:" + properties);
            System.out.println("body:" + new String(body));
        });

        //mandatory : true
        //channel.basicPublish(exchange,routingKey,true,null,message.getBytes());
        channel.basicPublish(exchange,routingKeyError,true,null,message.getBytes());


    }
}

public class Consumer {

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConn();

        //1. 通过connection创建一个Channel
        Channel channel = connection.createChannel();

        String exchange = "test_return_exchange";
        String routingKey = "return.#";
        String queueName = "test_return_queue";

        //2. 声明一个exchange
        channel.exchangeDeclare(exchange,"topic",true,false,null);
        //3. 声明一个队列
        channel.queueDeclare(queueName,true,false,false,null);
        //4. 绑定
        channel.queueBind(queueName,exchange,routingKey);
        //5. 创建消费者
        QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
        //6. 设置Channel
        channel.basicConsume(queueName,true,queueingConsumer);
        //7. 获取消息
        while (true) {
            //nextDelivery 会阻塞直到有消息过来
            QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println("收到:" + message);
        }


    }
}

当生产端执行channel.basicPublish(exchange,routingKey,true,null,message.getBytes());消息能发送成功,也可以从消费端看到打印

当执行channel.basicPublish(exchange,routingKeyError,true,null,message.getBytes());消息发送失败,因为路由失败了嘛,生产端能看到如下打印:

——handle return——
replyCode:312
replyText:NO_ROUTE
exchange1:test_return_exchange
routingKey1:abc.save
properties:#contentHeader<basic>(content-type=null, content-encoding=null, headers=null, delivery-mode=null, priority=null, correlation-id=null, reply-to=null, expiration=null, message-id=null, timestamp=null, type=null, user-id=null, app-id=null, cluster-id=null)
body:Hello Return Message

若生产端将mandatory设为false,则ReturnListener不会进行回调

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值