当消费侧出现异常时,若未进行处理,可能会导致消息再次被消费,然后再次出现异常,进入循环 官方文档:https://www.rabbitmq.com/confirms.html 前言 思考:因为在开发项目时,RabbitMQ的消费端出现了异常(工具类操作文件时,未找到文件路径)。由于在此之前并未对该异常进行预判,导致异常出现后,消费端仍然对MQ的消息进行消费,但是出现异常后无法对MQ进行回复,所以造成后果消费端一直消费该条信息,进入死循环! 另需注意:若在消费者上加了声明式事物,基于事务的传播行为,默认为加入当前事务,这样以来,如果内部报错,即便在外层加了try-catch,事务也会进行回滚,因为默认是在最后进行ack的,消息就不会被ack,从而再次被消费。 从而引发了自己的思考:1. 开发时难免会出现异常,这种异常如果事先未预判,那么在程序运行中,消费端该怎么避免以上出现的死循环;2. 如果事先预判到异常,对其进行了抛出或捕获,消费端又该如何表现? 异常 第一种方法,可以对可能发生异常的部分try、catch;只要事先将问题catch住,就证明消费端已经将该问题消费掉,然后该消息就不存在于队列中,不会造成无限报错的情况。这里,你可以在catch中写一些业务,把这个出现异常的“消息”记录到数据库或者怎么怎么处理,反正是相当于被消费掉了。 第二种方法,因为RabbitMQ 默认的异常策略是不断重试,除非抛出了fatal(致命)类型的异常,这种异常类型如下 异常类 MessageConversionException MessageConversionException MethodArgumentNotValidException MethodArgumentTypeMismatchException NoSuchMethodException ClassCastException 所以可以在catch语句块中抛出fatal 类型的异常(虽然不推荐,但也是种方式) 第三种方法,”消费者重试“模式。基本配置同一,只是在catch中显式的抛异常。这样其实就和没有catch差不多,就相当于未知状况下出现了异常。catch是为了解决业务问题,在这里处理自己需要的业务。catch中的throw有什么用呢? throw配合着application.yml中的“开启消费者重试”模式:若异常发生,重试n次(n为yml中的 max-attempts),之后消息就自动进入死信队列(或者如果没配置死信队列,消息被扔掉)。 具体如下,消费者的mq配置类中设置了死信队列(参数只有死信交换机和路由,没有TTL)。 消费者端不做任何异常处理,模拟开发时并不知道会出现异常的情况。(注释掉的,catch里的throw和这个是一样的效果) 但是配置文件中开启“消费者尝试”,并配置最大尝试数。 这样,消费端发现了异常,尝试了规定次数后,这条“问题消息”就会被解决(如果设置了死信队列,就被送到了死信队列;否则直接扔掉)。是开启了“消费者重试尝试”的功劳。如果不开启该模式,那么会无限的循环下去。和 “default-requeue-rejected: true”参数没有任何关系,“消费者重试”模式会覆盖掉default-requeue-rejected(默认为true)。所以,只要是开了该模式,异常就可以被解决。如果只设置 default-requeue-rejected: true(消费者重试未开启,应答方式为默认),那么会无限报错! 第四种,只设置 default-requeue-rejected: false(消费者重试未开启,应答方式为默认),异常只出现一次,然后该“问题消息”被解决(如果设置了死信队列,就被送到了死信队列;否则直接扔掉)。 第五种,在队列中设置了TTL参数!!!那么异常会无脑的跑一会,当消息到了一定时间就会过期,自动进入死信队列。这是TTL的功劳。 目前为止,都是自动(acknowledge-mode默认auto)应答mq,不需要手动应答。 第六种,yml配置文件手动应答,见最后一行的配置。 这时,消费端的监听需要如下这样,参照死信队列的概念,channel.basicReject的requeue参数必须设为false。 如果把requeue的值设为true,那就白玩了,“问题消息”又被你放到了当前队列,下一次消费方又执行这条“问题消息”。可以看出,第六种方案的推行并不依赖于“消费端重试”和TTL,仅仅依照死信队列的定义:利用basicReject拒绝,并把requeue设置为false. 注意:如果是,不管是否设置“消费者重试”模式,配置了default-requeue-rejected: false,且手动应答,异常只会出现一次,但是不会进入死信队列。消息以unack形式存在队列中。 综上所述,我们可以发现消费端异常的几种方案的特点: TTL可以设置消息的过期时间,不管你是不是无脑抛异常,只要过期,就进入死信队列; “消费者重试”模式,只要你抛异常抛到了我的底线(次数达标),那我就把你送走,可能是直接扔了,也可能是扔到死信队列; try、catch,只要你能提前预判,捕获到相应异常,那就平平安安,没有一点波澜; 手动回应,需要提前知道哪里会出错,就在哪里拒绝,而且requeue设成false;还要在哪里不拒绝(普通的消息回应),对mq做出相应正确的反馈 其实从这些特点可以看出,死信的定义就是最好的答案。 死信的产生: 消息被拒绝(basic.reject / basic.nack),并且requeue = false 消息TTL过期 […]
https://mp.weixin.qq.com/s/AIxJsse6wqpps5QEyiPpaw 学习东西要知行合一,如果只是知道理论而没实践过,那么掌握的也不会特别扎实,估计过几天就会忘记,接下来我们一起实践来学习Spring事务的传播属性。 # 传播属性 传播属性定义的是当一个事务方法碰到另一个事务方法时的处理行为,一共有七种行为,定义如下 其实只看概念的话已经很直截了当了说明了每个传播性的作用,此时我们再用具体的例子演示一下每个传播性属性下的行为。 此次演示我们使用的是H2数据库,这个数据库是作用在内存里面的,所以对于我们演示事务效果来说正好,无需我们在进行其他的配置了,我们新建一个表。将下面语句放在schema.sql文件里面即可,SpringBoot程序在启动的时候就会自动为我们在内存里面建立这样的一个表。 演示之前我们会定义两个类FooService和BarService。我们使用BarService 里面的方法进行调用FooService 中的方法。 # 环境准备 在进行事务演示之前,其实可以分为以下几种情况,根据排列组合,我们可以得出以下八种情况 调用者:有无事务 调用者:是否有异常 被调用者:有无事务(这个是通过传播属性进行控制的)所以并不在排列组合中 被调用者:是否有异常 # 异常类 其中的RollbackException是我们自己定义的一个异常类 # 调用者 在BarService中定义两个方法,一个是带着事务的,一个是不带事务的 接下来我们就根据俄上面定义的八种情况进行事务传播属性的学习。 PROPAGATION_REQUIRED 在此传播属性下,被调用方是否新建事务取决去调用者是否带着事务。 想要了解这个传播属性的特性,其实我们演示上面八种情况的两个例子就够了 第一种情况我们在被调用者抛出异常的情况下,如果查询不到插入的数据,那么就说明被调用者在调用者没有事务的情况下自己新建了事务。 第二种情况我们在调用者抛出异常的情况下,如果查询不到插入的数据,那么就说明被调用者在调用者有事务的情况下就加入当前事务了。 我们先来看一下被调用者的类的方法例子。 接下来我们看一下调用者方法的例子 此时我们在程序调用时进行查询 查看打印出来的日志 我们看到我们都没有查到相应的数据,说明数据都回滚了。此时我们应该就理解了那句话支持当前事务,如果没有就新建事务。 PROPAGATION_SUPPORTS 被调用者是否有事务,完全依赖于调用者,调用者有事务则有事务,调用者没事务则没事务。 接下来我们还是用上面的两个例子进行演示 第一种情况:被调用者抛出异常的情况下,如果仍能查询到数据,说明事务没有回滚,说明被调用者没有事务 第二种情况:调用者抛出异常情况下,如果查不到数据,说明两个方法在一个事务中 接下来仍然是例子演示 被调用者,只是将@Transactional 注解中的propagation 属性更换为了Propagation.SUPPORTS 调用者和上面的例子调用一样,我们直接查看执行效果 我们看到了在第一种情况下查到了数据,说明在第一种情况下被调用者是没有事务的。此时我们应该就理解了这句话 支持当前事务,如果没有就不以事务的方式运行。 PROPAGATION_MANDATORY 依然是这两个例子进行演示 第一种情况:因为调用者没有事务,所以此传播属性下应该是抛异常的 第二种情况:被调用者的事务和调用者事务是同样的 接下来是被调用者的代码例子 调用者和上面的例子调用一样,我们直接查看执行效果 我们发现和我们推测一样,说明被调用者是不会自己新建事务的,此时我们应该就理解了这句话支持当前事务,如果当前没事务就抛异常。 PROPAGATION_REQUIRES_NEW 此传播属性下,无论调用者是否有事务,被调用者都会新建一个事务 […]