最近的项目中使用到了rabbitMQ,巧合的是fanout,direct,topic这三种模式都接触到了,记录一下。
是什么?
RabbitMQ是由Erlang语言编写的实现了高级消息队列协议(AMQP)的开源消息代理软件(也可称为 面向消息的中间件)。支持Windows、Linux/Unix、MAC OS X操作系统和包括JAVA在内的多种编程语言。
AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受 客户端/中间件 不同产品,不同的开发语言等条件的限制
······然后还是说说和之前用的消息队列的区别吧,上个项目用过kafka。
关于这两种MQ的比较,网上查到的要点:
- RabbitMq比kafka成熟,在可用性上,稳定性上,可靠性上,RabbitMq超过kafka
- Kafka设计的初衷就是处理日志的,可以看做是一个日志系统,针对性很强,所以它并没有具备一个成熟MQ应该具备的特性
- Kafka的性能(吞吐量、tps)比RabbitMq要强,这篇文章的作者认为,两者在这方面没有可比性。
干什么?
MQ就是消息中间件嘛,消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题实现高性能,高可用,可伸缩和最终一致性架构 使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ。
怎么用?
标题上线,RabbitMQ中,所有生产者提交的消息都由Exchange来接受,然后Exchange按照特定的策略转发到Queue进行存储
RabbitMQ提供了四种Exchange:fanout,direct,topic,header
性能排序:fanout > direct > topic。比例大约为11:10:6
恰好前三个我都用到了,都分别写一下
对于rabbitMQ中使用的方法和参数详解阅读 [snowcoal.com/article/598…](Rabbitmp(java)对列 Client api介绍)
fanout模式
任何发送到Fanout Exchange的消息都会被转发到与该Exchange绑定(Binding)的所有Queue上。
1.可以理解为路由表的模式
2.这种模式不需要RouteKey
3.这种模式需要提前将Exchange与Queue进行绑定,一个Exchange可以绑定多个Queue,一个Queue可以同多个Exchange进行绑定。
4.如果接受到消息的Exchange没有与任何Queue绑定,则消息会被抛弃。
生产者
Channel channel = null;
try {
// 连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 创建连接
Connection connection = factory.newConnection();
// 获取通道
channel = connection.createChannel();
// 声明队列
channel.queueDeclare(queueName, true, false, false, null);
// 发布消息
channel.basicQos(1);
// 每次分发一个任务,MessageProperties.PERSISTENT_TEXT_PLAIN消息持久化
channel.basicPublish("", queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
} catch (Exception e) {
System.out.println(e.getStackTrace().toString());
}
消费者
// rabbitMQ连接参数
boolean DURABLE_TRUE = true; // 持久化
String queueName = ""; // 消费的队列 = this.getName();
try {
// 连接服务器
Connection connection = MQCommons.getNewConnection();
Channel channel = connection.createChannel();
// 随机生成一个队列
channel.queueDeclare(queueName, DURABLE_TRUE, false, false, null);
// 每次只消费一个
channel.basicQos(1);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
long deliveryTag = envelope.getDeliveryTag();
String message = new String(body, "UTF-8");
// 处理消息
this.getChannel().basicAck(deliveryTag, false);
}
};
// 需明确回复
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, consumer);
} catch (Exception e) {
System.out.println("消费者线程停止");
e.printStackTrace();
}
direct模式
任何发送到Direct Exchange的消息都会被转发到RouteKey中指定的Queue。
1.一般情况可以使用rabbitMQ自带的Exchange:”"(该Exchange的名字为空字符串,下文称其为default Exchange)。
2.这种模式下不需要将Exchange进行任何绑定(binding)操作
3.消息传递时需要一个“RouteKey”,可以简单的理解为要发送到的队列名字。
4.如果vhost中不存在RouteKey中指定的队列名,则该消息会被抛弃。
生产者
public void send(String message, String exchangeName, String routeKey) {
Channel channel = null;
try {
// 连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 创建连接
Connection connection = factory.newConnection();
// 获取通道
channel = connection.createChannel();
// 交换机声明 true:持久化
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT,true,false,null);
// 声明队列
channel.queueDeclare(queueName, true, false, false, null);
// 发布消息
channel.basicQos(1);
// 发布消息 MessageProperties.PERSISTENT_TEXT_PLAIN消息持久化
channel.basicPublish(exchangeName, routeKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
} catch (Exception e) {
e.printStackTrace();
}
}
消费者
// rabbitMQ连接参数
threadName = getName();
try {
// 连接服务器
Connection connection = MQCommons.getNewConnection();
Channel channel = connection.createChannel();
//交换机声明(参数为:交换机名称;交换机类型)
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT,true,false,null);
// 声明队列
channel.queueDeclare(queueName, DURABLE_TRUE, false, false, null);
// 队列与交换机绑定(参数为:队列名称;交换机名称;密匙-routingKey)
channel.queueBind(queueName, exchangeName, routingKey);
// 每次分发一个任务,MessageProperties.PERSISTENT_TEXT_PLAIN消息持久化
channel.basicQos(1);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
long deliveryTag = envelope.getDeliveryTag();
String message = new String(body, "UTF-8");
log.info("message内容={}", message);
// 处理消息
dealMessage(message);
// 应答:已处理(不做判断是否异常,不仍会消息队列)
getChannel().basicAck(deliveryTag, false);
}
};
// 需明确回复
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, consumer);
} catch (Exception e) {
log.info("消费线程处理异常", e);
}
topic模式
任何发送到Topic Exchange的消息都会被转发到所有关心RouteKey中指定话题的Queue上
1.这种模式较为复杂,简单来说,就是每个队列都有其关心的主题,所有的消息都带有一个“标题”(RouteKey),Exchange会将消息转发到所有关注主题能与RouteKey模糊匹配的队列。
2.这种模式需要RouteKey,也许要提前绑定Exchange与Queue。
3.在进行绑定时,要提供一个该队列关心的主题,如“#.log.#”表示该队列关心所有涉及log的消息(一个RouteKey为”MQ.log.error”的消息会被转发到该队列)。
4.“#”表示0个或若干个关键字,“”表示一个关键字。如“log.”能与“log.warn”匹配,无法与“log.warn.timeout”匹配;但是“log.#”能与上述两者匹配。
5.同样,如果Exchange没有发现能够与RouteKey匹配的Queue,则会抛弃此消息。
生产者
public void sendTopic(String message, String exchangeName, String routeKey) {
log.info("交换机={},routeKey={},消息={}", exchangeName, routeKey, message);
try {
//交换机声明 true:持久化
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, true, false, null);
channel.queueBind("queueName", exchangeName, routeKey);
// 发布消息 MessageProperties.PERSISTENT_TEXT_PLAIN消息持久化
channel.basicPublish(exchangeName, routeKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
} catch (Exception e) {
log.error("发送消息队列消息异常", e);
}
}
消费者
基本同上面的direct消费者
交换机绑定改一下
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC,true,false,null);
补充- rabbitmq实现延时队列(死信队列)
采用rabbitmq的消息时效ttl的特性实现延时重试机制。
前面对exchange,queue,routingKey有过介绍。延时队列的实现就是把消息放到死信交换机的死信队列,设置超时时间,等超时未处理,消息再次根据routingKey返回原有队列。消费者判断消费次数重复消费消息。
队列中的消息在以下三种情况下会变成死信 (1)消息被拒绝(basic.reject 或者 basic.nack),并且requeue=false; (2)消息的过期时间到期了; (3)队列长度限制超过了。 当队列中的消息成为死信以后,如果队列设置了DLX那么消息会被发送到DLX。通过x-dead-letter-exchange设置DLX,通过这个x-dead-letter-routing-key设置消息发送到DLX所用的routing-key,如果不设置默认使用消息本身的routing-key
参考实现:my.oschina.net/xiaominmin/…
文中提供了两种方式,我采用的第一种
spring的简单实现:www.cnblogs.com/lori/archiv…
public void sendDelay(String message,String exchangeName,String queueName,String routeKey,int ttl,BuiltinExchangeType exchangeType,String deathExchange){
log.info("延时队列,交换机={},队列名={},routeKey={},deathExchange={},消息={},队列类型={},TTL={}", exchangeName, queueName, routeKey, deathExchange, message, exchangeType, ttl);
try{
Map<String, Object> arguments = new HashMap<String, Object>();
//设置死信交换机
arguments.put("x-dead-letter-exchange", deathExchange);
//延时30秒
arguments.put("x-message-ttl", ttl);
//设置死信routingKey
arguments.put("x-dead-letter-routing-key", routeKey);
channel.exchangeDeclare(exchangeName, exchangeType,true,false,null);
channel.queueDeclare(queueName, DURABLE_TRUE, false, false, arguments);
channel.queueBind(queueName, exchangeName, routeKey);
channel.basicPublish(exchangeName, routeKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
}catch (Exception e){
log.error("发送消息异常",e);
}
}
业务场景: 消息消费过程中可能因为一些原因导致消费出错,比如请求其他系统接口超时,或者第三方系统挂掉,这种情况如果立即重试大部分情况还是一样的处理结果。最开始我想的解决方案,是把错误消息存库,然后设置定时任务隔一段时间重试,最后达到最大重试次数后记为任务失败。但是定时任务的方式是十分消耗系统资源的,此时发现rabbitMQ中可以使用延时队列实现定时任务这个功能。
实现代码:
先写着些,肯定有人觉得我这么写麻烦,我这个项目没有用springboot,如果是springboot感觉会简单的多。别人的springboot项目 www.jianshu.com/p/0d400d309…,有空我也用springboot试一下。
分享一个好用的软件 这是谷歌的http请求的模拟插件类似于postman,优点就是小,方便,当然经常用的话还是推荐postman,七天有效再要留言。
链接:pan.baidu.com/s/1zYQSdhH5…
提取码:mubu