消息队列之rabbitmq的高级特性之消息的可靠投递(confirm和return)以及消费者端的可靠信ACK机制,和消费者端的限流机制

消息队列mq,是作为消息的中间件存在的,它的优势在于
1.解耦合
2.异步提速
3.削峰填谷
解耦:在消息队列没有出现前我们的系统传递消息是由A系统直接传递给系统B,系统A和B直接存在紧密的耦合度,若是其中一个系统发生故障导致整体系统瘫痪,可维护性和容错性较低,不利于程序的拓展.
然而出现了mq,之后系统相互之间的耦合就变成了和消息中间件的耦合了,降低了系统之间的耦合度,独立出来的消息中间件也可以被其他系统所利用,提高了系统的拓展性和可维护性,容错性
异步提速:系统A的消息都存放在消息中间件中,不用去将请求发送给其他系统,减少了系统之间的相互通信,大大提升了速度
削峰填谷:如果系统处理消息的能力在每秒1000,现在每秒出现了5000的请求系统无法处理,使用消息中间件之后,先将消息存放在消息中间件里面,限制消费消息每秒拉取1000个请求给系统处理,(在消费者端为了防止系统处理请求时候,请求数超过了系统的最大的请求处理,我们这里使用了消费端限流)这样提高了系统的稳定性.

**劣势 **
1.系统可用性降低:
系统引入的外部依赖越多,系统稳定性越差。一旦 MQ 宕机,就会对业务造成影响。
解决办法:搭建集群
2.系统复杂度提高:
MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过MQ 进行异步调用。
解决办法:根据业务需求决定
3.一致性问题:
A 系统处理完业务,通过 MQ 给B、C、D三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理失败。
保证消息的一致性
解决办法:
为了确保消息的可靠性,我们在生产者端和消费者端都进行了消息的可靠性的投递机制

  • 在生产者端我们使用了消息的confirm和return
  • 在消费者端我们则使用了Ack机制.
    确保消息的一致性

生产者端

rabbitmq.properties

rabbitmq.host=192.168.88.133
rabbitmq.port=5672
rabbitmq.username=guest
rabbitmq.password=guest
rabbitmq.virtual-host=/

spring-rabbitmq-producer.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/rabbit
       http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"
                               publisher-confirms="true"
                               publisher-returns="true"
    />
    <!--定义管理交换机、队列-->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!--定义rabbitTemplate对象操作可以在代码中方便发送消息-->
    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>

    <!--    消息的可靠性-->
    <rabbit:queue id="test_queue_confirm" name="test_queue_confirm"></rabbit:queue>

    <rabbit:direct-exchange name="test_exchange_confirm" id="test_exchange_confirm" durable="true">
        <rabbit:bindings>
            <rabbit:binding queue="test_queue_confirm" key="confirm"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:direct-exchange>
</beans>

在测试方法中,用于生产者发送消息


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-producer.xml")
public class TestSpringConfirm {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 确认模式开启步骤
     * 1.spring的配置文件中connectionFactory的 publisher-confirms="true"
     * 2.在rabbitTemplate中定义回调函数
     */
    @Test
    public void testConfirm() {
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * confirm(CorrelationData correlationData, boolean ack, @Nullable String cause) 
             * @param correlationData 配置参数
             * @param ack 是否开启ack的机制
             * @param cause 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String  cause) {
                byte[] body = correlationData.getReturnedMessage().getBody();
                System.out.println(new String(body));
                if (ack) {
                    System.out.println("确认方法执行成功了...");
                } else {
                    System.out.println("确认方法执行失败了...");

                }

            }
        });

        CorrelationData correlationData = new CorrelationData();
        Message message = new Message("用于测试confirm-".getBytes(), new MessageProperties());
        correlationData.setReturnedMessage(message);
        rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "用于测试confirm", correlationData);
    }


    /**
     * 回退模式 : 当消息发送给Exchange后,Exchange路由到Queues失败是才会执行ReturnCallBack
     * 步骤:
     * 1.开启回退模式  publisher-returns="true"
     * 2.设置ReturnCallBack
     * 3.设置Exchange处理消息的模式,
     * 如果消息没有到路由的queue,则丢弃消息(默认)
     * 如果消息没有到queue,返回给消息发送方ReturnCallBack
     */
    @Test
    public void testReturnCallBack() {
        //设置交换机处理失败消息的方式
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("回退模式执行了");
                System.out.println(message);
                System.out.println(replyCode);
                System.out.println(replyText);
                System.out.println(exchange);
                System.out.println(routingKey);
            }
        });

        rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "用于测试confirm");

    }
}

publisher confirm
接受成功的情形:
1、消息被路由到所有匹配的队列,如果消息是持久化的,还需要等待消息持久化到磁盘;
2、消息发送到交换机,但未路由到任何队列。
接受失败情形:主要是由于rabbitmq内部错误导致的消息接受失败。
1、生产者发送的交换机不存在;
2、消息由交换机路由到队列出错;
3、持久化消息在进行持久化,写入磁盘是出错;
4、其他rabbitmq的未知错误等等。
解决方法:做一些处理,让消息再次发送。
publisher return
当消息发送给Exchange后,Exchange路由到Queues失败是才会执行ReturnCallBack

消费者端

  • comsumer ACK机制
  • 1.设置手动的签收,acknowledge=“manual” (配置文件中设置)
  • 2.让监听器实现ChannelAwareMessageListener接口
  • 3.如果消息成功处理,则调用channel的basicACK签收
  • 4.如果消息处理失败,则调用channel的basicNack拒绝签收,broker重新发送给consumer
    这里,acknowledge=“manual”
    有三种情况
    1.none:
    自动确认是指,当消息一旦被Consumer接收到,则自动给broker返回确认信息,broker收到确认信息后将相应 message从Broker中移除。
    但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失!!!
    2.auto:
    会根据方法的执行情况来决定是否确认还是拒绝(是否重新入queue)
    1.如果消息成功被消费(成功的意思是在消费的过程中没有抛出异常),则自动确认
    2.当抛出 AmqpRejectAndDontRequeueException 异常的时候,则消息会被拒绝,且 requeue = false(不重新入队列)
    3.当抛出 ImmediateAcknowledgeAmqpException 异常,则消费者会被确认
    4.其他的异常,则消息会被拒绝,且 requeue = true(如果此时只有一个消费者监听该队列,则有发生死循环的风险,多消费端也会造成资源的极大浪费,这个在开发过程中一定要避免的)。可以通过 setDefaultRequeueRejected(默认是true)去设置
    3.manuel:
    手动模式下,broker未正确收到消息的确认或拒绝信息时,会处于unacked状态,
    以下情形会进行重新发送:
    1、当前消费者重启;
    2、有新的消费者加入
    确认消息
    channel.basicAck(deliveryTag,true);
    // deliveryTag:当前需要确认消息的标识; true:是否批量确认
    int batch=100;
    if(deliveryTag%batch==0){
    channel.basicAck(deliveryTag,true);
    }
    拒绝消息
    channel.basicNack(deliveryTag,true,false);
    参数1:当前拒绝消息的标识;
    参数2:是否批量拒绝;
    参数3:requeue是否重入队列 true:重入队列会再次发送;false:作为死信消息(默认删除)

rabbitmq.properties

rabbitmq.host=192.168.88.133
rabbitmq.port=5672
rabbitmq.username=guest
rabbitmq.password=guest
rabbitmq.virtual-host=/

spring-rabbitmq-consumer.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/rabbit
       http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"
    />
    <context:component-scan base-package="com.itheima.rabbitmq.listener"/>


    <!--设置手动的签收,acknowledge="manual"-->
    <rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual" prefetch="1" auto-declare="true">
        <rabbit:listener ref="qosListener" queue-names="test_queue_confirm"/>
    </rabbit:listener-container>
</beans>
/**
 * comsumer ACK机制
 * 1.设置手动的签收,acknowledge="manual"  (配置文件中设置)
 * 2.让监听器实现ChannelAwareMessageListener接口
 * 3.如果消息成功处理,则调用channel的basicACK签收
 * 4.如果消息处理失败,则调用channel的basicNack拒绝签收,broker重新发送给consumer
 */
@Component
public class SpringAckListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
//basicAck(long deliveryTag, boolean multiple)
        try {
            System.out.println(new String(message.getBody()));
            System.out.println("处理业务逻辑");
            //int i = 1 / 0;
             //一次性设置100数据的传输
            if (deliveryTag % 1 == 0) {

                channel.basicAck(deliveryTag, true);

            }
           
        } catch (Exception e) {
            channel.basicNack(deliveryTag, true, true);
        }
    }


}

限流机制

/**
 * Consumer 限流机制
 *  1. 确保ack机制为手动确认。
 *  2. listener-container配置属性
 *      perfetch = 1,表示消费端每次从mq拉去一条消息来消费,直到手动确认消费完毕后,才会继续拉去下一条消息。
 */

@Component
public class QosListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {

        Thread.sleep(1000);
        //1.获取消息
        System.out.println(new String(message.getBody()));

        //2. 处理业务逻辑

        //3. 签收
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);

    }
}

消费者端的测试,保证消费者一直处于监听的状态使用死循环让程序不要停止

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-consumer.xml")
public class TestConsumer {

    @Test
    public void test1(){
        boolean flag = true;
        while (true){

        }
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值