RabbitMQ实战

60 阅读12分钟

相关概念

  • producer:生产者,用于发布消息
  • message:包括消息体(payload)+标签(label)
  • consumer:消费者
  • broker:消息中间件服务节点
  • queue:一个队列绑定多个消费者时,消息循环消费,不支持队列层面广播
  • exchange:交换器
  • routingkey:发送消息时,消息中会指定一个routingkey,用来决定消息流向
  • bindingkey:队列和交换器绑定时指定

交换器类型

  • fanout:把消息发送到该交换器绑定的所有队列中
  • direct:把消息发送到bindingkey和routingkey完全一致的队列中
  • topic:模式匹配,routingkey以.分割,在绑定队列时,bindingkey可以使用表示一个单词,#表示多个单词
  • headers:根据消息中headers属性进行匹配

客户端开发

一般过程

ConnectionFactory factory=new ConnectionFactory();
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/");
factory.setHost("http://ip");
factory.setPort(5672);
// 创建链接
Connection connection=factory.newConnection();
// 创建信道
Channel channel=connection.createChannel();

channel.exchangeDeclare("exchangeName","exchangeType",true);
//匿名队列,只对当前Connection可见,在Connection断开后自动删除
String queue=channel.queueDeclare().getQueue();
//全局共享的队列
//String queue=channel.queueDeclare("queueName",true,true,false,false,null);
channel.queueBind(queue,"exchangeName","routingKey");

如果代码中多次定义名称相同的交换器或队列,只要参数相同,即可成功返回,否则会报错。

交换器Exchange

定义交换器

Exchange.DeclareOK exchangeDeclare(String exchange,String type,boolean durable,boolen autoDelete,boolean internal,Map<String,Object> args)
    throws IOException;
  • exchange:交换器名称;
  • type:交换器类型,支持fanout、direct、topic、header,内置BuiltinExchangeType枚举可用;
  • durable:是否持久化,当服务重启后交换器元数据不会丢失;
  • autoDelete:是否自动删除,至少曾经有一个队列与之关联,然后所有队列都解绑后自动删除;
  • internal:是否内置,内置交换器只能通过其他交换器把数据投递过来,无法使用客户端直接投递消息;
  • args:其他结构化参数;

查看同名交换器

//如果存在则返回,否则抛出异常404 change exception,同时关闭channel
Exchange.DeclareOK exchangeDeclarePassive(String name);

删除交换器

//ifunused=false时,强制删除交换器
Exchange.DeleteOK exchangeDelete(String exchange,boolean ifUnused);

备份交换器AE

当投递消息时,如果不设置mandatory,或者设置mandatory=false,消息在路由失败后会丢失,通过添加ReturnListener代码又比较复杂,可以使用AE解决(配置AE后mandatory失效)。

添加方式

  1. 声明交换器时添加(优先级高)
Map<String,Object> args=new HashMap();
args.put("alternate-exchange","myExchange");
//定义交换器,并指定备份交换器
channel.exchangeDeclare("normalExchange","direct",true,false,args);
//定义备份交换器,这里使用fanout,忽略routingKey为了绑定消息一定会存入队列
channel.exchangeDeclare("myExchange","fanout",true,false,null);
//定义队列
channel.queueDeclare("normalQueue",true,false,false,null);
//绑定队列
channel.queueBind("normalQueue","normalExchange","normalKey");
//定义备份交换器对应的队列
channel.queueDeclare("unroutedQueue",true,false,false,null);
//绑定备份交换器和队列
channel.queueBind("unroutedQueue","myExchange","");
  1. 通过策略添加

(略)

队列Queue

定义队列

Queue.DeclareOk queueDeclare(String queueName,boolean durable,boolean exclusive,boolean autoDelete,Map<String,Object> args);
  1. queueName:队列名;
  2. durable:是否持久化,重启服务后队列不丢失(无法保证消息不丢失);
  3. exclusive:是否排他,排他队列只对创建它的链接可见,断开后自动删除(持久化也会被删除,排他等级最高),适用于同一个客户端即发也收的场景;
  4. autoDelete:是否自动删除,至少曾经有一个消费者链接到该队列,然后所有消费者都断开后,队列自动删除;
  5. args:其他参数;

生产者也消费者都可以声明队列,但是消费者不能订阅其他队列,否则无法再声明队列。

TTL

  1. 设置队列中消息的TTL

TTL---Time to live,为队列中的消息统一设置过期时间(如果消息本身也设置了TTL#见生产消息publish章节#,最先过期的为准),超时后的消息变成死信(Dean Message),消息会立即被删除(不会在投递时才判断),如果配置了死信交换器DLX,消息会进去信息队列。

Map<String,Object> args=new HashMap<String,Object>();
args.put("x-message-ttl",6000);//单位ms
channel.queueDeclare("myQueue",true,false,false,args);

正常情况下,消息不会过期;如果x-message-ttl设置为0,消息要么立即消费要么过期,可以替代3.0后的immediate效果。

  1. 设置队列的TTL

x-expires参数设置允许队列空置多久,超过这个时间(ms)队列会被删除。

条件:没有消费者绑定、没有调用Get、没有被重置,这种状态超过x-expires时间后队列自动删除。

Map<String,Object> args=new HashMap<String,Object>();
args.put("x-expires",1800000);//单位ms,30分钟后
channel.queueDeclare("myQueue",true,false,false,args);

DLX-私信交换器

DLX---Dead Letter Exchange,私信交换器,当消息过期后会被发送到DLX,消息成为私信的情况包括:

  1. 消息被拒绝消费,消费者调用basic.Reject/basic.Nack,并设置requeue=false;
  2. 消息过期;
  3. 队列达到最大长度;

DLX和DLQ和普通的交换器和队列一样,只是被用于其他队列的死信而已,使用x-dead-letter-exchange参数设置。

//定义死信交换器
channel.exchangeDeclare("dlx_exchange","direct");
Map<String,Object> args=new HashMap();
args.put("x-dead-letter-exchange","dlx_exchange");
//默认采用原来的路由键,也可以通过以下方式修改路由键
args.put("x-dead-letter-routing-key","dlx-routing-key");
//为队列myQueue添加DLX
channel.queueDeclare("myQueue",false,false,false,args);

使用TTL和DLX完整的示例

//创建交换器
channel.exchangeDeclare("exchange.dlx","direct",true);
channel.exchangeDeclare("exchange.normal","fanout",true);
//设置队列参数
Map<String,Object> args=new HashMap();
args.put("x-message-ttl",10000);//队列中消息10s过期
args.put("x-dead-letter-exchange","exchange.dlx");
args.put("x-dead-letter-routing-key","routingKey");
//创建和绑定队列
channel.queueDeclare("queue.normal",true,false,false,args);
channel.queueBind("queue.normal","exchange.normal","");
//创建和绑定死信队列
channel.queueDeclare("queue.dlx",true,false,false,null);
channel.queueBind("queue.dlx","exchange.dlx","routingKey");
//发送消息
channel.basicPublish("exchange.normal","rk",MessageProperties.PERSISTENT_TEXT_PLAIN,"hello dlx".getBytes());

携带rk的消息被发送到exchange.normal,然后存储到queue.normal中,10s后消息过期,消息被丢弃到exchange.dlx中,根据routingKey消息被存储到queue.dlx中。

延迟队列

消息不会立即被消费,会延迟一会再消费,常见场景:

  1. 订单系统中,有30m的支付时间,过期不支付订单取消;
  2. 定时任务;

RabbitMQ不直接支持延迟队列,可以使用DDL和DLX实现。

  1. 采用direct交换器,不同的队列采用不同的bindingKey与交换器绑定,每一个队列对应一个延迟时间,如bindingKey=queue_10s,表示10s过期的队列;
  2. 每一个队列设置一个DLX,当消息过期时,把消息丢给DLX;
  3. 消费者直接绑定到DLQ,不要绑定到第一步中的队列;

这样消息根据routingKey发送到不同的队列,过期后被丢到对应的DLX,消费者直接消费即可。

优先级队列

正常队列的消息都是FIFO,优先级相同,都时0,如果要设置消息的优先级,需要两步:

  1. 把队列配置成优先级队列,支持0-255,但建议0-10,太高对性能有影响;
  2. 发送消息时,设置消息优先级,不能高出队列的优先级0-10;

在非优先级队列中,设置消息优先级不生效

//配置优先级队列
Map<String,Object> args=new HashMap();
args.put("x-max-priority",10);
channel.queueDeclare("queue.priority",true,false,false,args);

//发送消息时携带优先级-5
AMQP.BasicProperties.Builder builder=new AMQP.BasicProperties.Builder();
builder.priority(5);
AMQP.BasicProperties props=builder.build();
channel.basicPublish("exchange_pri","rkey",props,"hello".getBytes());

如果队列消息消费很快,优先级基本没啥效果,还占用资源;如果队列消费太慢,优先级低的消息可能会一直被阻塞。

查看同名队列

//如果存在则返回,否则抛出异常404 queue exception,同时关闭channel
Queue.DeclareOk queueDeclarePassive(String queueName);

删除队列

// ifUnused=false会强制删除队列
// ifEmpty=true表示只有队列为空才会被删除
Queue.DeleteOk queueDelete(String queueName,boolean ifUnused,boolean ifEmpty);

绑定器QueueBind

绑定队列和交换器

Queue.BindOk queueBind(String queue,String exchange,String routingKey,Map<String,Object> args);

解绑队列和交换器

Queue.UnbindOk queueUnbind(String queue,String exchange,String routingKey,Map<String,Object> args);

绑定器ExchangeBind

// 绑定后消息从source投递到dest,routingKey依然是投递给source时使用的一致
Exchange.BindOk exchangeBind(String dest,String source,String routingKey,Map<String,Object> args);

生产消息publish

void basicPublish(String exchangeName,String routingKey,boolean mandatory,boolean immediate,BasicProperties props,byte[] body);
  • exchangeName:交换器名称;
  • routingKey:路由键;
  • props:消息基本属性集(见下文);
  • mandatory:当消息找到匹配的队列时,是否把消息返回给生产者;
  • immediate:true表示只有队列上存在消费者时才会投递,否则返回;false表示闭着眼投递(3.0版本去掉了该配置,可为队列设置x-message-ttl=0替代);

mandatory介绍

当消息根据交换器类型和路由键没有找到匹配的队列时,是否把消息返回给生产者,true表示返回,false表示直接丢弃,那么生产者如何获取被返回的消息?

channel.addReturnListener(new ReturnListener(){
    public void handleReturn(int replyCode,String replyText,String exchange,String routingKey,AMQP.BasicProperties props,byte[] body) throw IOException{
        // 获取被返回的消息
        String message=new String(body);
    }
});

props介绍

一共包括14个属性成员:

  
属性介绍
contentType 
contentEncoding 
headers(Map) 
deliveryMode2表示持久化,枚举使用:MessageProperties.PERSISTENT_TEXT_PLAIN
priority 
correlationId 
replyTo 
expiration消息过期时间(ms),但在投递时才判断是否过期
messageId 
timestamp 
type 
userId 
appId 
clusterId 

使用方法

  1. new AMQP.BasicProperties.Builder().contentType("text/plain").userId("dony").build();
  2. new AMQP.BasicProperties().setUserId("dony");
  3. MessageProperties.PERSISTENT_TEXT_PLAIN,该类预置了几个常用的;

生产者确认机制

如何保证生产者的消息正确发送到了服务器?

通过事务机制(同步)

事务机制很消耗性能,涉及三个方法:

  • channel.txSelect:把信道设置为事务模式并开启事务;
  • channel.txCommit:提交事务;
  • channel.txRollback:回滚事务;
try{
    channel.txSelect();
    //多条消息,可以把basicPublish放在循环中执行,最后一次性提交
    channel.basicPublish("myEx","rKey",MessageProperties.PERSISTENT_TEXT_PLAIN,"hello".getBytes());
    channel.txCommit();
}catch(Exception ex){
    channel.txRollback();
}

通过发送方确认机制--publisher confirm(异步)

通过把信道设置成 confirm模式,该信道上的消息会被自动编号(ID从1开始),当消息被成功写入队列后,Broker会给生产者发送一个确认消息(Basic.Ack),如果队列和消息是可持久化的,当消息持久化成功后才返回确认消息。

确认消息的deliveryTag包含了发送消息的编号ID。

另外,也可以直接设置确认消息的multiple参数,表示该ID之前的消息都已确认收到。

// 1. 同步方式
try{
    // 开启信道的confirm模式
    channel.confirmSelect();
    // 如果要发送多条,可以把以下内容放入循环中处理
    channel.basicPublish("myEx","rkey",null,"hello".getBytes());
    if(!channel.waitForConfirm()){//这里实际也是同步等待
        //发送失败
    }
    //----多条截至位置-----
}catch(InterruptedException ex){
    // 异常日志
}

异步方式需要使用channel.addConfirmListener回调接口实现:

channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener(){
    public void handleAck(long deliveryTag,boolean multiple){
        //投递成功
    }

    public void handleNack(long delivery,boolean multiple){
        //投递失败
    }
});

消费消息

滑动窗口

当一个队列有多个消费者时,消息会轮询发送到每一个消费者,不是广播!不是广播!

轮询时,无论消费者是否已经把上一条消费完,都会被再次投递一个消息,可以使用滑动窗口解决(只对推模式有效)channel.basicQos,有三种重载:

  1. void basicQos(int prefetchCount);
  2. void basicQos(int prefetchCount,boolean global);
  3. void basicQos(int prefetchSize,int prefetchCount,boolean global);

解释:

  • prefetchCount:当前消费者只允许挤压的数量,当待消费数量到达上限后,队列不会再推送消息给当前消费者;0表示没有上线;
  • prefetchSize:表示消费者允许挤压消息总容量的上线,单位B,0表示没上限;
  • global:针对一个信道(多个消费者)订阅了多个队列的情况,默认false,建议保持默认,true会消耗性能;

推模式

String basicConsume(String quene,boolean autoAck,Map<String,Object> args,Consumer callback);
String basicConsume(String quene,boolean autoAck,String consumerTag,Consumer callback);
String basicConsume(String quene,boolean autoAck,String consumerTag,boolean noLocal,boolean exclusive,Map<String,Object> args,Consumer callback);
  • queue:队列名称
  • autoAck:是否自动确认,如果true,投递后消息会被立即删除,消费失败后消息会丢失
  • consumerTag:消费者标签
  • noLocal:true表示不允许把消息发送给同一个Connection中的消费者
  • exclusive:是否排他
  • args:消费者其他参数
  • callback:处理推送过来的消息
boolean autoAck=false;
channel.basicQos(64);
channel.basicConsume(queueName,autoAck,"myconsumerTag",new DefaultConsumer(channel){
    @Override
    public void handleDelivery(String consumerTag,Envelope envelop,AMQP.BasicProperties props,byte[] body) throw IOException{
        String routingKey=envelope.getRoutingKey();
        String contentType=props.getContentType();
        long deliveryTag=envelope.getDeliveryTag();
        channel.basicAck(deliveryTag,false);
    }
});

在同一个channel中的消费者也需要定义consumerTag标记自身,用于ack时识别消费者。

DefaultConsumer介绍

包括以下方法:

 void handleDelivery(String consumerTag,Envelope envelop,AMQP.BasicProperties props,byte[] body);
// 在其他方法之前调用
void handleConsumeOk(String consumerTag);
//以下两个在取消订阅_channel.basicCancel(consumerTag)_时调用
//触发顺序:handleConsumeOk->handleDelivery->handleCancelOk
void handleCancelOk(String consumerTag);
void handleCancel(String consumerTag);
//当Connection和Channel关闭时调用
void handleShutdownSignal(String consumerTag,ShutdownSignalException sig);
void handleRecoverOk(String consumerTag);

拉模式

GetResponse basicGet(String queue,boolean autoAck);
//例子
GetResponse reps= basicGet("myQueue",false);
channel.basicAck(reps.getEnvelops().getDeliveryTag(),false);

拉模式每次消费一个消息,不建议放在循环中使用,性能很差,持续消费建议采用推模式。

拒绝消费

//拒绝一条消息
//deliveryTag:消息编号
//requeue:是否重新放回队列
void basicReject(long deliveryTag,boolean requeue);

//批量拒绝消息
//multiple:false时和basicReject相同;true表示拒绝deliveryTag之前所有未被当前消费者消费的消息
void basicNack(long deliveryTag,boolean multiple,boolean requeue);

当requeue=false时,消息会删除,这时可以使用死信队列DLQ

重新消费

当消费出现问题时,可以要求broker重新发送该消息,以保证重新消费。

 //requeue:true消息重新被投递,可能会被投递给其他消费者
//requeue:false消息重新被投递,但会再次投递给当前消费者
Basic.RecoverOk basicRecover(boolean requeue);

消息传输保障

消息可靠性是在生产者发送消息到中间件时要考虑的问题,一般分三个层级:

  • At most once:最多一次,消息可能会丢失,但不会重复;
  • At least once:最少一次,消息不会丢失,但可能会重复;
  • Exactly once:恰好一次,每次肯定会传输一次(且仅一次),目前不支持;

一般消息会保证 最少一次,保证消息不丢失,但可能会重复,这时需要在消费者采用幂等等手段解决重复消费问题,同时要考虑以下内容:

  1. 生产者需要开启确认机制,保证消息确认达到队列;
  2. 采用mandatory或AE确保无法路由的消息不会丢失;
  3. 消息和队列都要持久化,确保消息不丢失;
  4. 消费者使用手动Ack确认消息,安全可靠;

最多一次不用考虑这么多,闭着眼生产,闭着眼消费即可,丢就丢吧。

关闭连接

channel.close();
connection.close();

其实connection关闭时,会自动关闭channel,但手动关闭channel是个好习惯