消息队列 RabbitMQ——备份交换机 与 死信队列
1.概念
a.备份交换机(alternate-exchange):备份交换器是为了实现没有路由到队列的消息,声明交换机的时候添加属性alternate-exchange,声明一个备用交换机,一般声明为fanout类型,这样交换机收到路由不到队列的消息就会发送到备用交换机绑定的队列中。
b.死信队列(dead-letter-exchange):当消息在一个队列中变成死信 (dead message) 之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX。
如何成为死信(dead-letter):
1)消息被否定确认,使用 channel.basicNack 或 channel.basicReject ,并且此时requeue 属性被设置为false
2)消息在队列的存活时间超过设置的TTL时间
3)消息队列的消息数量已经超过最大队列长度
2.使用
a.创建配置文件 rabbit.properties
#RabbitMQ服务器地址,默认值"localhost" rabbit.ip=localhost #RabbitMQ服务端口,默认值为5672 rabbit.port=5672 #访问RabbitMQ服务器的账户,默认是guest rabbit.username=guest #访问RabbitMQ服务器的密码,默认是guest rabbit.password=guest
b.创建连接工具类
public class ConnectionManager { private static Logger logger = LoggerFactory.getLogger(ConnectionManager.class); private static String RABBIT_HOST = "localhost"; private static int RABBIT_PORT = 5672; private static String RABBIT_USERNAME = "guest"; private static String RABBIT_PASSWORD = "guest"; private static Connection connection = null; /** * 加载配置文件 */ static { BufferedReader reader = null; try { Properties properties = new Properties(); String filePath = System.getProperty("user.dir") + "/config/rabbit.properties"; File file = new File(filePath); reader = new BufferedReader(new InputStreamReader(new FileInputStream(file),"UTF-8")); properties.load(reader); //设置连接参数 String rabbitHost = properties.getProperty("rabbit.ip"); if(StringUtils.hasText(rabbitHost)){ RABBIT_HOST = rabbitHost; } String rabbitPort = properties.getProperty("rabbit.port"); if(StringUtils.hasText(rabbitPort)){ RABBIT_PORT = Integer.valueOf(rabbitPort); } String rabbitUserName = properties.getProperty("rabbit.username"); if(StringUtils.hasText(rabbitUserName)){ RABBIT_USERNAME = rabbitUserName; } String rabbitPassWord = properties.getProperty("rabbit.password"); if(StringUtils.hasText(rabbitPassWord)){ RABBIT_PASSWORD = rabbitPassWord; } } catch (Exception e) { logger.info("加载配置rabbit.properties失败.", e); } finally { if (reader != null) { try { reader.close(); } catch (Exception e) { } } } } /** * 获取连接 * @return */ public static Connection getConnection() { if(connection == null) { ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost(RABBIT_HOST); connectionFactory.setPort(RABBIT_PORT); connectionFactory.setUsername(RABBIT_USERNAME); connectionFactory.setPassword(RABBIT_PASSWORD); try { connection = connectionFactory.newConnection(); } catch (Exception e) { logger.error("获取MQ连接失败", e); } } return connection; } }
c.创建生产者基类
public abstract class BasicProducer { protected static Logger logger = LoggerFactory.getLogger(BasicProducer.class); //交换机名称 protected String exchangeName; protected abstract void setParam(); /** * 设置参数 * @param exchangeName */ protected void setParam(String exchangeName){ this.exchangeName = exchangeName; }; /** * 初始化 */ public boolean init(){ this.setParam(); Channel channel = null; try { channel = ConnectionManager.getConnection().createChannel(); //声明一个备份交换机 String backupExchange = "backup." + this.exchangeName; channel.exchangeDeclare(backupExchange, "fanout", true, false, null); // 消息没有被路由的之后存入的队列 String backupQueue = backupExchange + ".queue"; channel.queueDeclare(backupQueue, true, false, false, null); channel.queueBind(backupQueue, backupExchange, ""); //声明exchange交换机(参数1:交换机名称,参数2:交换机类型,参数3:交换机持久性,参数4:交换机在不被使用时是否删除,参数5:交换机的其他属性) Map<String, Object> arguments = new HashMap<>(); arguments.put("alternate-exchange", backupExchange); channel.exchangeDeclare(this.exchangeName, "direct", true,false, arguments); return true; }catch (Exception e){ logger.error("发送MQ消息失败", e); }finally { if(channel != null){ try { channel.close(); } catch (Exception e) { logger.error("关闭MQ信道失败", e); } } } return false; } /** * 发送消息 * @param routing * @param msg */ public void sendMsg(String routing, String msg){ Channel channel = null; try { channel = ConnectionManager.getConnection().createChannel(); //发送消息(参数1:交换器,参数2:路由键,参数3:消息的其他参数,参数4:消息体) channel.basicPublish(this.exchangeName, routing, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes()); //MessageProperties.PERSISTENT_TEXT_PLAIN 表示持久化 }catch (Exception e){ logger.error("发送MQ消息失败", e); }finally { if(channel != null){ try { channel.close(); } catch (Exception e) { logger.error("关闭MQ信道失败", e); } } } } }
d.创建消费者基类
public abstract class BasicConsumer { protected static Logger logger = LoggerFactory.getLogger(BasicConsumer.class); //交换机名称 protected String exchangeName; //队列名称 protected String queueName; //路由键 protected String routing; //消息处理handler protected BasicHandler handler; //是否自动确认 protected boolean autoAck; //信道 protected Channel channel; protected abstract void setParam(); /** * 设置参数 * @param exchangeName */ protected void setParam(String exchangeName, String queueName, String routing, BasicHandler handler, boolean autoAck){ this.exchangeName = exchangeName; this.queueName = queueName; this.routing = routing; this.handler = handler; this.autoAck = autoAck; }; /** * 初始化 */ public void init(){ this.setParam(); try { this.channel = ConnectionManager.getConnection().createChannel(); //声明一个死信队列 String dlxExchange = "dlx." + this.exchangeName; this.channel.exchangeDeclare(dlxExchange, "topic", true, false, null); String dlxQueue = "dlx." + this.queueName; this.channel.queueDeclare(dlxQueue, true, false, false, null); String dlxRouting = "dlx." + this.routing; this.channel.queueBind(dlxQueue, dlxExchange, dlxRouting); // 声明队列 Map<String,Object> arguments = new HashMap<>(); arguments.put("x-dead-letter-exchange", dlxExchange); arguments.put("x-dead-letter-routing-key", dlxRouting); arguments.put("x-message-ttl", 5000); //消息过期时间,5秒后放入死信队列 this.channel.queueDeclare(this.queueName, true, false, false, arguments); //绑定队列到交换机(参数1:队列的名称,参数2:交换机的名称,参数3:routingKey) this.channel.queueBind(this.queueName, this.exchangeName, this.routing); // 同一时刻服务器只会发一条消息给消费者 this.channel.basicQos(1); // 定义队列的消费者 Consumer consumer = new DefaultConsumer(this.channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { super.handleDelivery(consumerTag, envelope, properties, body); String msg = new String(body); long deliveryTag = envelope.getDeliveryTag(); try { handler.handleMsg(deliveryTag, msg); }catch (Exception e){ logger.error("处理消息[" + deliveryTag + "]出错", e); } } }; this.channel.basicConsume(this.queueName, this.autoAck, consumer); }catch (Exception e){ logger.error("连接MQ服务失败", e); } } /** * 消息确认 */ public void ack(long deliveryTag){ try{ this.channel.basicAck(deliveryTag, false); }catch (Exception e){ logger.error("确认消息[" + deliveryTag + "]出错", e); } } /** * 消息否认(不返还队列,放入死信队列中) */ public void nack(long deliveryTag){ try { this.channel.basicNack(deliveryTag, false, false); }catch (Exception e){ logger.error("否认消息[" + deliveryTag + "]出错", e); } } /** * 消息否认(返还队列) */ public void nack4Requeue(long deliveryTag){ try { this.channel.basicNack(deliveryTag, false, true); }catch (Exception e){ logger.error("否认消息[" + deliveryTag + "]出错", e); } } }
e.创建消息处理函数式接口
@FunctionalInterface public interface BasicHandler { /** * 处理消息 */ void handleMsg(long deliveryTag, String msg); }
3.测试
a.创建生产者实例继承基类
public class Producer extends BasicProducer { //单例 private static Producer instance = new Producer(); private Producer(){} public static Producer getInstance(){ return instance; } @Override protected void setParam() { this.setParam("common.direct.exchange"); } }
b.创建两个消费者实例继承基类
public class Consumer extends BasicConsumer { //单例 private static Consumer instance = new Consumer(); private Consumer(){} public static Consumer getInstance(){ return instance; } @Override protected void setParam() { BasicHandler handler = (deliveryTag, msg) -> { logger.info("[Consumer1]接受消息:" + msg); }; this.setParam("common.direct.exchange", "common.direct.queue.no1", "consumer.no1", handler, true); } }
public class Consumer2 extends BasicConsumer { //单例 private static Consumer2 instance = new Consumer2(); private Consumer2(){} public static Consumer2 getInstance(){ return instance; } @Override protected void setParam() { BasicHandler handler = (deliveryTag, msg) -> { logger.info("[Consumer2]接受消息:" + msg); this.nack(deliveryTag); }; this.setParam("common.direct.exchange", "common.direct.queue.no2", "consumer.no2", handler, false); } }
c.测试
public static void main(String[] args) { Producer producer = Producer.getInstance(); producer.init(); Consumer consumer = Consumer.getInstance(); consumer.init(); Consumer2 consumer2 = Consumer2.getInstance(); consumer2.init(); producer.sendMsg("consumer.no1", "消息1"); producer.sendMsg("consumer.no2", "消息2"); producer.sendMsg("consumer.no3", "消息3"); }
d.讲解:
1)由生产者发送了3条消息,但路由键"consumer.no3"没有队列与之对应,因此第三条消息被放入了备份交换机的队列中。
2)消费者2设置为手动提交,接收到消息试返回"nack",并且没有requeue返还到队列,因此该消息被放入了死信队列中。