相关概念
- 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失效)。
添加方式
- 声明交换器时添加(优先级高)
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","");
- 通过策略添加
(略)
队列Queue
定义队列
Queue.DeclareOk queueDeclare(String queueName,boolean durable,boolean exclusive,boolean autoDelete,Map<String,Object> args);
- queueName:队列名;
- durable:是否持久化,重启服务后队列不丢失(无法保证消息不丢失);
- exclusive:是否排他,排他队列只对创建它的链接可见,断开后自动删除(持久化也会被删除,排他等级最高),适用于同一个客户端即发也收的场景;
- autoDelete:是否自动删除,至少曾经有一个消费者链接到该队列,然后所有消费者都断开后,队列自动删除;
- args:其他参数;
生产者也消费者都可以声明队列,但是消费者不能订阅其他队列,否则无法再声明队列。
TTL
- 设置队列中消息的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效果。
- 设置队列的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,消息成为私信的情况包括:
- 消息被拒绝消费,消费者调用basic.Reject/basic.Nack,并设置requeue=false;
- 消息过期;
- 队列达到最大长度;
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中。
延迟队列
消息不会立即被消费,会延迟一会再消费,常见场景:
- 订单系统中,有30m的支付时间,过期不支付订单取消;
- 定时任务;
RabbitMQ不直接支持延迟队列,可以使用DDL和DLX实现。
- 采用direct交换器,不同的队列采用不同的bindingKey与交换器绑定,每一个队列对应一个延迟时间,如bindingKey=queue_10s,表示10s过期的队列;
- 每一个队列设置一个DLX,当消息过期时,把消息丢给DLX;
- 消费者直接绑定到DLQ,不要绑定到第一步中的队列;
这样消息根据routingKey发送到不同的队列,过期后被丢到对应的DLX,消费者直接消费即可。
优先级队列
正常队列的消息都是FIFO,优先级相同,都时0,如果要设置消息的优先级,需要两步:
- 把队列配置成优先级队列,支持0-255,但建议0-10,太高对性能有影响;
- 发送消息时,设置消息优先级,不能高出队列的优先级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) | |
| deliveryMode | 2表示持久化,枚举使用:MessageProperties.PERSISTENT_TEXT_PLAIN |
| priority | |
| correlationId | |
| replyTo | |
| expiration | 消息过期时间(ms),但在投递时才判断是否过期 |
| messageId | |
| timestamp | |
| type | |
| userId | |
| appId | |
| clusterId |
使用方法:
- new AMQP.BasicProperties.Builder().contentType("text/plain").userId("dony").build();
- new AMQP.BasicProperties().setUserId("dony");
- 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,有三种重载:
- void basicQos(int prefetchCount);
- void basicQos(int prefetchCount,boolean global);
- 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:恰好一次,每次肯定会传输一次(且仅一次),目前不支持;
一般消息会保证 最少一次,保证消息不丢失,但可能会重复,这时需要在消费者采用幂等等手段解决重复消费问题,同时要考虑以下内容:
- 生产者需要开启确认机制,保证消息确认达到队列;
- 采用mandatory或AE确保无法路由的消息不会丢失;
- 消息和队列都要持久化,确保消息不丢失;
- 消费者使用手动Ack确认消息,安全可靠;
最多一次不用考虑这么多,闭着眼生产,闭着眼消费即可,丢就丢吧。
关闭连接
channel.close();
connection.close();
其实connection关闭时,会自动关闭channel,但手动关闭channel是个好习惯