Spring整合rabbitmq實(shí)踐(一):基礎(chǔ)
Spring整合rabbitmq實(shí)踐(三):源碼
3. 擴(kuò)展實(shí)踐
3.1. MessageConverter
前面提到只要在RabbitTemplate中配置了MessageConverter,在發(fā)送和接收消息的時候就能自動完成Message和自定義java對象的自動轉(zhuǎn)換。
MessageConverter接口只有兩個方法:
public interface MessageConverter {
/**
* Convert a Java object to a Message.
* @param object the object to convert
* @param messageProperties The message properties.
* @return the Message
* @throws MessageConversionException in case of conversion failure
*/
Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException;
/**
* Convert from a Message to a Java object.
* @param message the message to convert
* @return the converted Java object
* @throws MessageConversionException in case of conversion failure
*/
Object fromMessage(Message message) throws MessageConversionException;
}
即使不手動配置MessageConverter,也會有一個默認(rèn)的SimpleMessageConverter,
它會直接將java對象序列化。
官方文檔不建議使用這個MessageConverter,因?yàn)镾impleMessageConverter是將java對象在producer端序列化,然后在consumer端反序列化,這會將producer和consumer緊密地耦合在一起,并且僅限于java平臺。
推薦用JsonMessageConverter、Jackson2JsonMessageConverter,這兩個是都將java對象轉(zhuǎn)化為json再轉(zhuǎn)為byte[]來構(gòu)造Message對象,前一個用的是jackson json lib,后一個用的是jackson 2 json lib。
@Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
@Bean
@Autowired
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory,
MessageConverter messageConverter) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMessageConverter(messageConverter);
return template;
}
還有一些其它的MessageConverter實(shí)現(xiàn)類,當(dāng)然如果有需要也可以自己實(shí)現(xiàn)。
3.2. Exception Handling
有兩個error handler類可以對@RabbitListener注解的方法中拋出的異常進(jìn)行處理。
一個是RabbitListenerErrorHandler接口,并將其設(shè)置到@RabbitListener注解中,如下:
@Bean
public RabbitListenerErrorHandler rabbitListenerErrorHandler(){
return new RabbitListenerErrorHandler() {
@Override
public Object handleError(Message amqpMessage, org.springframework.messaging.Message<?> message, ListenerExecutionFailedException exception) throws Exception {
System.out.println(message);
throw exception;
}
};
}
@RabbitListener(queues = "test_queue_1", errorHandler = "rabbitListenerErrorHandler")
public void listen(Message message){
...
}
另一個是ErrorHandler接口,并將其設(shè)置到RabbitListenerContainerFactory中。
public interface ErrorHandler {
/**
* Handle the given error, possibly rethrowing it as a fatal exception.
*/
void handleError(Throwable t);
}
@RabbitListener注解的方法中拋出的異常,首先會進(jìn)入RabbitListenerErrorHandler,這里如果沒有能力處理這個異常,需要將其重新拋出(否則不會進(jìn)入ErrorHandler),然后異常將會進(jìn)入ErrorHandler,一旦異常進(jìn)入ErrorHandler就意味著消息消費(fèi)失敗了(所以不需要重新拋出異常)。
RabbitListenerErrorHandler沒有默認(rèn)配置,ErrorHandler有一個默認(rèn)的ConditionalRejectingErrorHandler類,它的處理方式是打印日志,然后辨別異常類型,如果屬于以下幾種異常,
o.s.amqp...MessageConversionException
o.s.messaging...MessageConversionException
o.s.messaging...MethodArgumentNotValidException
o.s.messaging...MethodArgumentTypeMismatchException
java.lang.NoSuchMethodException
java.lang.ClassCastException
則包裝成AmqpRejectAndDontRequeueException拋出,這個異常的作用是,忽略defaultRequeueRejected(前文已經(jīng)講過)的設(shè)置,強(qiáng)制讓rabbitmq丟棄此條處理失敗消息,不放回queue。
這樣處理是因?yàn)檫@些異常是不可挽回的,就算再重新執(zhí)行也一樣會拋異常,如果放回到queue就會陷入“消費(fèi)失敗-放回queue-消費(fèi)失敗...”的死循環(huán)。不過這是1.3.2版本之后新增的功能,之前的版本如果設(shè)置放回queue會陷入死循環(huán),需要自己實(shí)現(xiàn)ErrorHandler來處理。
3.3. Transactions
rabbitmq和spring-amqp官方文檔對事務(wù)的描述都非常少,簡單介紹一下了解到的信息。
rabbitmq官方文檔對amqp事務(wù)的整體定位是這樣的:
Overall the behaviour of the AMQP tx class, and more so its implementation on RabbitMQ, is closer to providing a 'batching' feature than ACID capabilities known from the database world.
amqp事務(wù)僅僅適用于publish和ack,rabbitmq增加了reject的事務(wù)。其它操作都不具備事務(wù)特性。也就是說,rabbitmq本身的事務(wù)可以保證producer端發(fā)出的消息成功被broker收到(不能保證一定會進(jìn)入queue),consumer端發(fā)出的確認(rèn)信息成功被broker收到,其它諸如consumer端具體的消費(fèi)邏輯之類如果想要獲得事務(wù)功能,需要引入外部事務(wù)。
引入rabbitmq事務(wù)很簡單,將RabbitTemplate或者RabbitListenerContainerFactory的channelTransacted屬性設(shè)為true即可,示例:
@Autowired
@Bean
public AmqpTemplate amqpTemplate(ConnectionFactory amqpConnectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(amqpConnectionFactory);
rabbitTemplate.setChannelTransacted(true);
return rabbitTemplate;
}
這樣,獲得的Channnel就有了事務(wù)功能。
也可以直接操作Channel:
Channel channel = cachingConnectionFactory.createConnection().createChannel(true);
try {
//channel.txSelect();上面createChannel已經(jīng)設(shè)為true了,這句可以去掉
channel.basicPublish("xxx", "xxx", new AMQP.BasicProperties(), JSON.toJSONString(event).getBytes());
channel.txCommit();
} catch (IOException e) {
try {
channel.txRollback();
} catch (IOException e1) {
}
} finally {
try {
channel.close()
} catch (Exception e) {
}
}
需要注意的是,直接通過Connection獲取的Channel需要手動close:
Channels used within the framework (e.g. RabbitTemplate) will be reliably returned to the cache. If you create channels outside of the framework, (e.g. by accessing the connection(s) directly and invoking createChannel()), you must return them (by closing) reliably, perhaps in a finally block, to avoid running out of channels.
對于producer端,同樣的發(fā)送一條消息到一個不存在的exchange:
amqpTemplate.convertAndSend("notExistExchange", "routingKey", object);
如果關(guān)閉事務(wù),如上文提到過,CachingConnectionFactory會打出一條錯誤日志,但程序會正常運(yùn)行。
如果打開事務(wù),由于消息沒有到達(dá)broker,這里會拋出異常。
對于consumer端,當(dāng)consumer正在處理一條消息時:
如果broker掛掉,程序會不斷嘗試重連,當(dāng)broker恢復(fù)時,會重新收到這條消息;
如果程序掛掉,broker發(fā)現(xiàn)還沒有收到consumer的確認(rèn)信息但consumer沒了,會將這條消息恢復(fù);
長時間沒有收到consumer端的確認(rèn)信息,也會將消息從unacked狀態(tài)變成ready狀態(tài);
如果程序處理消息期間拋異常,broker會收到一個nack或者reject,也會將這條消息恢復(fù)。
所以,rabbitmq是可以將沒有成功消費(fèi)的消息恢復(fù)的,個人覺得consumer端使用rabbitmq事務(wù)的意義并不是很大,也許可以用于consumer端消息去重:
consumer處理成功向rabbitmq發(fā)出了ack,consumer默認(rèn)rabbitmq收到了這個ack所以consumer認(rèn)為這條消息處理結(jié)束,但實(shí)際可能rabbitmq沒有收到ack又將這條消息放回queue然后重新發(fā)給consumer導(dǎo)致消息重復(fù)處理。如果開啟了事務(wù),能保證rabbitmq一定能收到確認(rèn)信息,否則事務(wù)提交失敗。
另外,需要注意的是,開啟事務(wù)會大幅降低消息發(fā)送及接收效率,因?yàn)楫?dāng)已經(jīng)有一個事務(wù)存在時,后面的消息是不能被發(fā)送或者接收(對同一個consumer而言)的,所以以上兩種場景都不推薦使用事務(wù)來解決。
3.4. Listeners
@Bean
public ChannelListener channelListener() {
return new ChannelListener() {
@Override
public void onCreate(Channel channel, boolean transactional) {
logger.info("channel number:{}, nextPublishSqlNo:{}",
channel.getChannelNumber(),
channel.getNextPublishSeqNo());
}
@Override
public void onShutDown(ShutdownSignalException signal) {
logger.error("channel shutdown, reason:{}, errorLevel:{}",
signal.getReason().protocolMethodName(),
signal.isHardError() ? "connection" : "channel");
}
};
}
ChannelListener接口,監(jiān)聽Channel的創(chuàng)建和異常關(guān)閉。
@Bean
public BlockedListener blockedListener() {
return new BlockedListener() {
@Override
public void handleBlocked(String reason) throws IOException {
logger.info("connection blocked, reason:{}", reason);
}
@Override
public void handleUnblocked() throws IOException {
logger.info("connection unblocked");
}
};
}
BlockedListener監(jiān)聽Connection的block和unblock。
@Bean
public ConnectionListener connectionListener() {
return new ConnectionListener() {
@Override
public void onCreate(Connection connection) {
logger.info("connection created.");
}
public void onClose(Connection connection) {
logger.info("connection closed.");
}
public void onShutDown(ShutdownSignalException signal) {
logger.error("connection shutdown, reason:{}, errorLevel:{}",
signal.getReason().protocolMethodName(),
signal.isHardError() ? "connection" : "channel");
}
};
}
ConnectionListener監(jiān)聽Connection的創(chuàng)建、關(guān)閉和異常終止。
@Bean
public RecoveryListener recoveryListener() {
return new RecoveryListener() {
@Override
public void handleRecovery(Recoverable recoverable) {
logger.info("automatic recovery completed");
}
@Override
public void handleRecoveryStarted(Recoverable recoverable) {
logger.info("automatic recovery started");
}
};
}
RecoveryListener監(jiān)聽開始自動恢復(fù)Connection、自動恢復(fù)連接完成。
ConnectionListener、ChannelListener、RecoveryListener設(shè)置到ConnectionFactory即可。
@Autowired
@Bean
public CachingConnectionFactory cachingConnectionFactory(ConnectionListener connectionListener,
ChannelListener channelListener,
RecoveryListener recoveryListener) {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setAddresses(mqConfigBean.getAddresses());
connectionFactory.setUsername(mqConfigBean.getUsername());
connectionFactory.setPassword(mqConfigBean.getPassword());
connectionFactory.setVirtualHost(mqConfigBean.getVirtualHost());
connectionFactory.addConnectionListener(connectionListener);
connectionFactory.addChannelListener(channelListener);
connectionFactory.setRecoveryListener(recoveryListener);
connectionFactory.setChannelCacheSize(3);
return connectionFactory;
}
ConnectionListener、ChannelListener可以正常觸發(fā),RecoveryListener暫時還沒發(fā)現(xiàn)怎么觸發(fā)。BlockedListener還沒有發(fā)現(xiàn)應(yīng)該設(shè)置在哪里,ConnectionFactory沒有這個設(shè)置。
通過ConnectionListener和ChannelListener可以debug看出Connection和Channel都是有緩存的,因?yàn)閛nCreate()方法不會每次都調(diào)用。并且Connection和Channel的創(chuàng)建都是lazy的,程序啟動時不會創(chuàng)建Connection和Channel,在第一次用到的時候才會創(chuàng)建。
3.5. 多個@RabbitListener消費(fèi)一個queue
一個服務(wù)中可以有多個@RabbitListener注解的方法消費(fèi)一個queue,如下:
@RabbitListener(queues = "queueName")
public void listener1(Message message) {
...
}
@RabbitListener(queues = "queueName")
public void listener2(Message message) {
...
}
這樣寫使用的仍是同一個Connection,一條消息也不會被兩個方法都調(diào)用,如果RabbitListenerContainerFactory中設(shè)置concurrentConsumer為3,意味著每個方法產(chǎn)生3個consumer,一共會有6個consumer對這個queue進(jìn)行消費(fèi)。
也可以分布在不同的應(yīng)用程序中,那樣會在不同的Connection中。
一個服務(wù)中有如上的兩個方法消費(fèi)同一個queue,另一個服務(wù)中有一個方法消費(fèi)同一個queue,產(chǎn)生的結(jié)果如下:
可以看到,有兩個消費(fèi)者Connection,一個有3個Channel,一個有6個Channel。
共產(chǎn)生了9個consumer。
3.6. publisher confirm and return
為了能讓producer端知道消息是否成功進(jìn)入了queue,并且避免使用事務(wù)大幅降低消息發(fā)送效率,可以用confirm和return機(jī)制來代替事務(wù)。
首先實(shí)現(xiàn)兩個Callback,ReturnCallback和ConfirmCallback,需要哪個實(shí)現(xiàn)哪個,不一定都需要。
public RabbitTemplate.ReturnCallback returnCallback() {
return new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
logger.info("return call back");
}
};
}
public RabbitTemplate.ConfirmCallback confirmCallback() {
return new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
logger.info("confirm call back");
}
};
}
然后將這兩個Callback設(shè)置到RabbitTemplate中,將mandatory屬性設(shè)為true(ReturnCallback需要,ConfirmCallback不需要):
rabbitTemplate.setReturnCallback(returnCallback);
rabbitTemplate.setConfirmCallback(confirmCallback);
rabbitTemplate.setMandatory(true);
然后在ConnectionFactory中將這Confirm和Return機(jī)制打開:
connectionFactory.setPublisherReturns(true);
connectionFactory.setPublisherConfirms(true);
這樣就完成了。
ConfirmCallback和ReturnCallback的調(diào)用條件:
ConfirmCallback - 每一條發(fā)出的消息都會調(diào)用ConfirmCallback;
ReturnCallback - 只有在消息進(jìn)入exchange但沒有進(jìn)入queue時才會調(diào)用。
相關(guān)方法入?yún)ⅲ?/p>
correlationData - RabbitTemplate的send系列方法中有帶這個參數(shù)的,如果傳了這個參數(shù),會在回調(diào)時拿到;
ack - 消息進(jìn)入exchange,為true,未能進(jìn)入exchange,為false,由于Connection中斷發(fā)出的消息進(jìn)入exchange但沒有收到confirm信息的情況,也會是false;
cause - 消息發(fā)送失敗時的失敗原因信息。
另外,關(guān)于confirm和return官方文檔上有下面這段信息,有必要了解一下:
When a rabbit template send operation completes, the channel is closed; this would preclude the reception of confirms or returns in the case when the connection factory cache is full (when there is space in the cache, the channel is not physically closed and the returns/confirms will proceed as normal). When the cache is full, the framework defers the close for up to 5 seconds, in order to allow time for the confirms/returns to be received. When using confirms, the channel will be closed when the last confirm is received. When using only returns, the channel will remain open for the full 5 seconds. It is generally recommended to set the connection factory’s channelCacheSize to a large enough value so that the channel on which a message is published is returned to the cache instead of being closed. You can monitor channel usage using the RabbitMQ management plugin; if you see channels being opened/closed rapidly you should consider increasing the cache size to reduce overhead on the server.
是說異步的接收confirm和return時仍然需要走原來發(fā)送消息用到的那個Channel,如果那個Channel被關(guān)閉了,是收不到confirm/return信息的。好在根據(jù)以上說明,Channel會等到最后一個confirm接收到時才會close,所以應(yīng)該也不用擔(dān)心Channel被關(guān)閉而接收不到confirm的問題。
3.7. retry
Starting with version 1.3 you can now configure the RabbitTemplate to use a RetryTemplate to help with handling problems with broker connectivity.
重試機(jī)制主要是解決網(wǎng)絡(luò)不穩(wěn)導(dǎo)致連接中斷的問題。所以其實(shí)并不是重新發(fā)送消息,而是重新建立。
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy(Integer.MAX_VALUE);
retryTemplate.setRetryPolicy(simpleRetryPolicy);
return retryTemplate;
}
如上,配置一個RetryTemplate,再設(shè)置到AmqpTemplate即可。
RetryTemplate與spring-amqp及rabbitmq都沒有關(guān)系,這是spring-retry中的類。以上示例中使用了最簡單的重試策略,不斷重試,直到Integer.MAX_VALUE次為止。
對producer端而言,如果Connection正常,但發(fā)送消息失敗是不會重試的,如指定的exchange不存在的情況:
第1條發(fā)送完畢
收到第1條confirm,ack:false, correlationData:null
17:26:09.544 [AMQP Connection 127.0.0.1:5672] ERROR [CachingConnectionFactory.java:1344] - Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'xxx' in vhost 'vhost', class-id=60, method-id=40)
第2條發(fā)送完畢
收到第2條confirm,ack:false, correlationData:null
17:26:10.552 [AMQP Connection 127.0.0.1:5672] ERROR [CachingConnectionFactory.java:1344] - Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'xxx' in vhost 'vhost', class-id=60, method-id=40)
第3條發(fā)送完畢
收到第3條confirm,ack:false, correlationData:null
17:26:11.559 [AMQP Connection 127.0.0.1:5672] ERROR [CachingConnectionFactory.java:1344] - Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'xxx' in vhost 'vhost', class-id=60, method-id=40)
由Connection中斷導(dǎo)致的發(fā)送消息失敗,會進(jìn)行重試:
第7條發(fā)送完畢
收到第7條confirm,ack:true, correlationData:null
第8條發(fā)送完畢
收到第8條confirm,ack:true, correlationData:null
第9條發(fā)送完畢
收到第9條confirm,ack:true, correlationData:null
第10條發(fā)送完畢
收到第10條confirm,ack:true, correlationData:null
第11條發(fā)送完畢
收到第11條confirm,ack:true, correlationData:null
17:01:44.000 [AMQP Connection 127.0.0.1:5672] ERROR [CachingConnectionFactory.java:1344] - Channel shutdown: connection error; protocol method: #method<connection.close>(reply-code=320, reply-text=CONNECTION_FORCED - broker forced connection closure with reason 'shutdown', class-id=0, method-id=0)
17:01:44.005 [AMQP Connection 127.0.0.1:5672] WARN [ForgivingExceptionHandler.java:115] - An unexpected connection driver error occured (Exception message: Connection reset)
17:01:44.602 [http-nio-8080-exec-2] INFO [AbstractConnectionFactory.java:455] - Attempting to connect to: [localhost:5672]
...
17:02:23.076 [http-nio-8080-exec-2] INFO [AbstractConnectionFactory.java:455] - Attempting to connect to: [localhost:5672]
17:02:24.578 [http-nio-8080-exec-2] INFO [AbstractConnectionFactory.java:471] - Created new connection: amqpConnectionFactory#3412a3fd:20/SimpleConnection@41298ed [delegate=amqp://guest@0:0:0:0:0:0:0:1:5672/test, localPort= 55092]
第12條發(fā)送完畢
收到第12條confirm,ack:true, correlationData:null
第13條發(fā)送完畢
收到第13條confirm,ack:true, correlationData:null
第14條發(fā)送完畢
收到第14條confirm,ack:true, correlationData:null
第15條發(fā)送完畢
收到第15條confirm,ack:true, correlationData:null
沒有配置重試,或到達(dá)了重試次數(shù)依然失敗,會拋出異常:
第15條發(fā)送完畢
收到第15條confirm,ack:false, correlationData:null
17:41:13.571 [AMQP Connection 127.0.0.1:5672] ERROR [CachingConnectionFactory.java:1344] - Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'xxx' in vhost 'paas_v3_vhost', class-id=60, method-id=40)
第16條發(fā)送完畢
收到第16條confirm,ack:false, correlationData:null
17:41:14.583 [AMQP Connection 127.0.0.1:5672] ERROR [CachingConnectionFactory.java:1344] - Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'xxx' in vhost 'paas_v3_vhost', class-id=60, method-id=40)
17:41:15.322 [AMQP Connection 127.0.0.1:5672] WARN [ForgivingExceptionHandler.java:115] - An unexpected connection driver error occured (Exception message: Connection reset)
17:41:15.579 [http-nio-8080-exec-1] INFO [AbstractConnectionFactory.java:455] - Attempting to connect to: [localhost:5672]
17:41:17.609 [http-nio-8080-exec-1] ERROR [ExceptionHandler.java:41] - unknown error
org.springframework.amqp.AmqpConnectException: java.net.ConnectException: Connection refused: connect
at org.springframework.amqp.rabbit.support.RabbitExceptionTranslator.convertRabbitAccessException(RabbitExceptionTranslator.java:62)
at org.springframework.amqp.rabbit.connection.AbstractConnectionFactory.createBareConnection(AbstractConnectionFactory.java:484)
at org.springframework.amqp.rabbit.connection.CachingConnectionFactory.createConnection(CachingConnectionFactory.java:626)
at org.springframework.amqp.rabbit.connection.CachingConnectionFactory.createBareChannel(CachingConnectionFactory.java:576)
對consumer端,如果采用的是@RabbitListener或其它類似異步接收消息的方式,則沒必要配置重試。consumer端有ack機(jī)制,Connection中斷導(dǎo)致rabbitmq收不到ack信息,消息會重新入隊(duì)(可能會導(dǎo)致同一條消息重復(fù)消費(fèi))。
對于直接調(diào)用RabbitTemplate的receive系列方法獲取消息的消費(fèi)方式,則同消息發(fā)送端,沒有retry或retry次數(shù)到達(dá),則拋異常。
3.8. 發(fā)送端的消息丟失
這里討論兩種情況可能產(chǎn)生的消息丟失:
(1).rabbitmq沒掛,只是短暫的網(wǎng)絡(luò)異常,連接可以恢復(fù),消息發(fā)送出去但沒有到exchange。
(2).rabbitmq掛了且長時間無法恢復(fù),消息沒有發(fā)出去;
3.8.1. 可恢復(fù)的Connection中斷
在配置了retry的情況下,Connection中斷,會根據(jù)配置的retry策略嘗試重連,即使重新連上了,消息依然可能會丟失。
本地測試,單線程間隔1毫秒循環(huán)發(fā)送1萬條消息,模擬一個不斷有消息發(fā)出的場景,在發(fā)送過程中手動關(guān)閉Rabbitmq服務(wù)再重新啟動,模擬Connection短暫中斷的場景。因?yàn)槊恳粭l消息都帶有唯一的messageId(實(shí)際上是“線程名-序號”的形式),所以能輕易地從消費(fèi)端讀出所有消息之后找到丟失的消息。
測試結(jié)果:發(fā)送1萬條消息,實(shí)際收到9999條,丟失1條。
發(fā)送端通過ConfirmCallback打印出所有ack=false的消息:
----------打印ack=false的消息----------
size:4
pool-5-thread-1-5881
pool-5-thread-1-5882
pool-5-thread-1-5883
pool-5-thread-1-5884
消費(fèi)端讀出所有消息后,找出丟失的消息:
--------total:10000---------
----------contain size: 9999----------
----------absent size: 1----------
pool-5-thread-1-5883
可以看到,ack=false的消息有4條,但實(shí)際上只丟了一條。因?yàn)橄⒌陌l(fā)送和Confirm是異步進(jìn)行的,如果在消息發(fā)送出去之后,異步的confirm回來之前,Connection中斷,那么ConfirmCallback會立即被調(diào)用,并且ack=false,原因是Channel被關(guān)閉了。
單線程情況下應(yīng)該最多只會丟失一條,也有可能不會丟。
多線程的情況下丟消息的現(xiàn)象就很嚴(yán)重了。本地測試5個線程發(fā)消息的情況,一共50000條消息,丟失了1500多條。但其實(shí)如果把這5個線程分到5個請求,一個請求只跑一個線程,情況會好很多,類似于上面單線程的情況。
解決方案
最完美的解決方案是事務(wù),但不推薦,為了rabbitmq的效率,退而求其次,采用confirm機(jī)制。
從上面的測試可以看到,在ConfirmCallback中ack=false的消息未必真的沒有到達(dá)exchange,但沒有到達(dá)exchange的消息ack一定是false,所以只需要將ack=false的消息重新發(fā)送一遍即可。(這種方案會導(dǎo)致消息重復(fù)發(fā)送,后面再解決這一問題)
實(shí)現(xiàn)方案各種各樣,這里分享一下自己遇到的問題 。
ConfirmCallback的回調(diào)方法中沒有Message對象
你可能會想從ConfirmCallback中拿到Message對象,當(dāng)ack=false的時候?qū)⑦@個Message再重新發(fā)出去,但方法入?yún)⒅袥]有Message對象。
@Component
public class ReissueMessageConfirmCallback implements RabbitTemplate.ConfirmCallback {
private static final Logger logger = LoggerFactory.getLogger(ReissueMessageConfirmCallback.class);
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause){
if (correlationData instanceof MessageCorrelationData) {
MessageCorrelationData messageCorrelationData = (MessageCorrelationData) correlationData;
logger.info("------------messageId: " + messageCorrelationData.getMessage().getMessageProperties().getMessageId() +
", ack: " + ack + ", cause:" + cause + "--------------");
if (!ack) {
SendFailedMessageHolder.add(messageCorrelationData);
}
}
}
}
注意到入?yún)⒅杏幸粋€CorrelationData對象,同時在RabbitTemplate中有相應(yīng)的send方法:
@Override
public void send(final String exchange, final String routingKey,
final Message message, final CorrelationData correlationData)
throws AmqpException {
}
這個方法AmqpTemplate中是沒有的,是RabbitTemplate擴(kuò)展的。
所以,雖然ConfirmCallback不能直接拿到Message,但可以拿到CorrelationData,于是問題就解決了。
直接在ConfirmCallback中調(diào)用RabbitTemplate發(fā)送消息導(dǎo)致死鎖
現(xiàn)在我們可以通過CorrelationData在ConfirmCallback中拿到Message對象了,我們也有辦法拿到RabbitTemplate,為了避免bean的循環(huán)依賴,我是這樣做的:
@Autowired
@Bean
public RabbitTemplate amqpTemplate(ConnectionFactory amqpConnectionFactory,
RetryTemplate retryTemplate,
MessageConverter messageConverter,
//RabbitTemplate.ConfirmCallback confirmCallback,
RabbitTemplate.ReturnCallback returnCallback
){
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(amqpConnectionFactory);
rabbitTemplate.setRetryTemplate(retryTemplate);
rabbitTemplate.setMessageConverter(messageConverter);
//rabbitTemplate.setChannelTransacted(true);
rabbitTemplate.setReturnCallback(returnCallback);
rabbitTemplate.setConfirmCallback(new ReissueMessageConfirmCallback(rabbitTemplate));
rabbitTemplate.setMandatory(true);
return rabbitTemplate;
}
ReissueMessageConfirmCallback是自己寫的一個實(shí)現(xiàn)類,將RabbitTemplate bean自己設(shè)置進(jìn)去。然后我們在ConfirmCallback中發(fā)送消息:
@Component
public class ReissueMessageConfirmCallback implements RabbitTemplate.ConfirmCallback {
private static final Logger logger = LoggerFactory.getLogger(ReissueMessageConfirmCallback.class);
private RabbitTemplate rabbitTemplate;
public ReissueMessageConfirmCallback(RabbitTemplate rabbitTemplate){
this.rabbitTemplate = rabbitTemplate;
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause){
if (correlationData instanceof MessageCorrelationData) {
MessageCorrelationData messageCorrelationData = (MessageCorrelationData) correlationData;
String exchange = messageCorrelationData.getExchange();
String routingKey = messageCorrelationData.getRoutingKey();
Message message = messageCorrelationData.getMessage();
if (!ack) {
rabbitTemplate.send(exchange, routingKey, message, messageCorrelationData);
}
}
}
}
MessageCorrelationData是自己寫的CorrelationData擴(kuò)展類,增加了Message、exchange、routingKey屬性。
在請求主線程發(fā)送1萬條消息的過程中,將rabbitmq關(guān)閉,這時請求主線程和ConfirmCallback線程都在等待Connection恢復(fù),然后重新啟動rabbitmq,當(dāng)程序重新建立Connection之后,這兩個線程會死鎖。
可行的方案:定時任務(wù)重發(fā)
@Component
public class ReissueMessageSchedule implements InitializingBean {
@Autowired
private RabbitTemplate rabbitTemplate;
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
public void start(){
scheduledExecutorService.scheduleWithFixedDelay(new ReissueTask(rabbitTemplate), 10, 10, TimeUnit.SECONDS);
}
@Override
public void afterPropertiesSet(){
this.start();
}
}
public class ReissueTask implements Runnable {
private static final Logger logger = LoggerFactory.getLogger(ReissueTask.class);
private RabbitTemplate rabbitTemplate;
public ReissueTask(RabbitTemplate rabbitTemplate){
this.rabbitTemplate = rabbitTemplate;
}
@Override
public void run() {
List<MessageCorrelationData> messageCorrelationDataList = new ArrayList<>(SendFailedMessageHolder.getAll());
logger.info("------------------獲取到" + messageCorrelationDataList.size() + "條ack=false的消息,準(zhǔn)備重發(fā)------------------");
SendFailedMessageHolder.clear();
int i = 1;
for (MessageCorrelationData messageCorrelationData : messageCorrelationDataList) {
Message message = messageCorrelationData.getMessage();
String messageId = message.getMessageProperties().getMessageId();
logger.info("------------------重發(fā)第" + i + "條消息,id: " + messageId + "------------------");
i++;
message.getMessageProperties().setMessageId(messageId + "-重發(fā)");
rabbitTemplate.send(messageCorrelationData.getExchange(), messageCorrelationData.getRoutingKey(),
messageCorrelationData.getMessage(), messageCorrelationData);
}
logger.info("------------------重發(fā)完成------------------");
}
}
重發(fā)的消息會在原消息id后面跟上“重發(fā)”二字。
本地測試打印出的相關(guān)信息:
發(fā)送端:
15:07:36.063 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:29] - ------------------獲取到13條發(fā)送失敗的消息,準(zhǔn)備重發(fā)------------------
15:07:36.063 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第1條消息,id: reactor-http-nio-3-7439------------------
15:07:38.030 [pool-3-thread-1] INFO o.s.a.r.c.CachingConnectionFactory [AbstractConnectionFactory.java:455] - Attempting to connect to: [localhost:5672]
15:07:40.036 [reactor-http-nio-3] INFO o.s.a.r.c.CachingConnectionFactory [AbstractConnectionFactory.java:455] - Attempting to connect to: [localhost:5672]
...
15:08:14.188 [pool-3-thread-1] INFO o.s.a.r.c.CachingConnectionFactory [AbstractConnectionFactory.java:455] - Attempting to connect to: [localhost:5672]
15:08:16.190 [reactor-http-nio-3] INFO o.s.a.r.c.CachingConnectionFactory [AbstractConnectionFactory.java:455] - Attempting to connect to: [localhost:5672]
15:08:16.710 [reactor-http-nio-3] INFO o.s.a.r.c.CachingConnectionFactory [AbstractConnectionFactory.java:471] - Created new connection: amqpConnectionFactory#2127e66e:25/SimpleConnection@ee0d88b [delegate=amqp://guest@127.0.0.1:5672/test, localPort= 57212]
15:08:16.716 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第2條消息,id: reactor-http-nio-3-7440------------------
15:08:16.716 [reactor-http-nio-3] INFO c.l.l.r.p.c.RabbitmqController [RabbitmqController.java:102] - send message, id: reactor-http-nio-3-7452
send message: reactor-http-nio-3-7452
15:08:16.717 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第3條消息,id: reactor-http-nio-3-7441------------------
15:08:16.718 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第4條消息,id: reactor-http-nio-3-7442------------------
15:08:16.718 [reactor-http-nio-3] INFO c.l.l.r.p.c.RabbitmqController [RabbitmqController.java:102] - send message, id: reactor-http-nio-3-7453
send message: reactor-http-nio-3-7453
15:08:16.718 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第5條消息,id: reactor-http-nio-3-7443------------------
15:08:16.719 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第6條消息,id: reactor-http-nio-3-7444------------------
15:08:16.719 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第7條消息,id: reactor-http-nio-3-7445------------------
15:08:16.719 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第8條消息,id: reactor-http-nio-3-7446------------------
15:08:16.720 [reactor-http-nio-3] INFO c.l.l.r.p.c.RabbitmqController [RabbitmqController.java:102] - send message, id: reactor-http-nio-3-7454
send message: reactor-http-nio-3-7454
15:08:16.720 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第9條消息,id: reactor-http-nio-3-7447------------------
15:08:16.720 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第10條消息,id: reactor-http-nio-3-7448------------------
15:08:16.720 [AMQP Connection 127.0.0.1:5672] INFO c.l.l.r.p.r.ReissueMessageConfirmCallback [ReissueMessageConfirmCallback.java:21] - ------------messageId: reactor-http-nio-3-7451, ack: true, cause:null--------------
15:08:16.721 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第11條消息,id: reactor-http-nio-3-7449------------------
15:08:16.721 [reactor-http-nio-3] INFO c.l.l.r.p.c.RabbitmqController [RabbitmqController.java:102] - send message, id: reactor-http-nio-3-7455
send message: reactor-http-nio-3-7455
15:08:16.721 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第12條消息,id: reactor-http-nio-3-7450------------------
15:08:16.722 [reactor-http-nio-3] INFO c.l.l.r.p.c.RabbitmqController [RabbitmqController.java:102] - send message, id: reactor-http-nio-3-7456
send message: reactor-http-nio-3-7456
15:08:16.723 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第13條消息,id: reactor-http-nio-3-7451------------------
15:08:16.723 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:41] - ------------------重發(fā)完成------------------
reactor-http-nio-3是請求主線程,pool-3-thread-1是執(zhí)行重發(fā)消息定時任務(wù)的線程。
從以上日志信息可以看出,當(dāng)rabbitmq關(guān)閉的時候,主線程與重發(fā)線程都在嘗試重連,直到rabbitmq重啟完成恢復(fù)Connection。
重發(fā)的消息有13條:reactor-http-nio-3-7439 ~ reactor-http-nio-3-7451。
再看消費(fèi)端整理并打印出來的接收到的所有消息:
--------should receive:10000---------
----------actually receive: 10013----------
----------absent messages:0---------
----------resend messages: 13----------
reactor-http-nio-3-7439-重發(fā)
reactor-http-nio-3-7440-重發(fā)
reactor-http-nio-3-7441-重發(fā)
reactor-http-nio-3-7442-重發(fā)
reactor-http-nio-3-7443-重發(fā)
reactor-http-nio-3-7444-重發(fā)
reactor-http-nio-3-7446-重發(fā)
reactor-http-nio-3-7447-重發(fā)
reactor-http-nio-3-7445-重發(fā)
reactor-http-nio-3-7449-重發(fā)
reactor-http-nio-3-7448-重發(fā)
reactor-http-nio-3-7450-重發(fā)
reactor-http-nio-3-7451-重發(fā)
可以看到,我們正確收到了上面那重發(fā)的13條消息。不過這次運(yùn)氣比較好,沒有消息遺漏。
同時,這里注意到一件事,消費(fèi)端代碼沒有對重發(fā)的消息做排序,收到的重發(fā)消息的順序與發(fā)送端重發(fā)消息的順序是不匹配的,所以rabbitmq可能不保證先發(fā)出的消息一定先被接收。
下面是5個線程同時發(fā)送消息的測試結(jié)果:
發(fā)送端:
15:42:40.602 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:29] - ------------------獲取到642條發(fā)送失敗的消息,準(zhǔn)備重發(fā)------------------
15:42:40.602 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第1條消息,id: pool-5-thread-4-6951------------------
...
省略重連過程
...
15:43:07.628 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第2條消息,id: pool-5-thread-5-6605------------------
...
省略中間600多條消息的重發(fā)
...
15:43:07.794 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第641條消息,id: pool-5-thread-1-6704------------------
15:43:07.794 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:35] - ------------------重發(fā)第642條消息,id: pool-5-thread-4-7088------------------
15:43:07.794 [pool-3-thread-1] INFO c.l.l.r.p.schedule.task.ReissueTask [ReissueTask.java:41] - ------------------重發(fā)完成------------------
消費(fèi)端:
--------should receive:50000---------
----------actually receive: 50014----------
----------absent messages:628---------
pool-5-thread-1-6583
pool-5-thread-1-6584
...
pool-5-thread-1-6705
pool-5-thread-2-6538
...
pool-5-thread-2-6653
pool-5-thread-3-6093
...
pool-5-thread-3-6218
pool-5-thread-4-6955
...
pool-5-thread-4-7087
pool-5-thread-5-6605
...
pool-5-thread-5-6733
pool-5-thread-5-6734
----------resend messages: 642----------
pool-5-thread-1-6580-重發(fā)
pool-5-thread-1-6581-重發(fā)
...
pool-5-thread-1-6705-重發(fā)
pool-5-thread-1-6706-重發(fā)
pool-5-thread-2-6537-重發(fā)
...
pool-5-thread-2-6654-重發(fā)
pool-5-thread-3-6093-重發(fā)
...
pool-5-thread-3-6219-重發(fā)
pool-5-thread-4-6951-重發(fā)
...
pool-5-thread-4-7088-重發(fā)
pool-5-thread-5-6604-重發(fā)
...
pool-5-thread-5-6734-重發(fā)
pool-5-thread-5-6735-重發(fā)
可以看到,丟失的消息被完美地包含在重發(fā)的消息里面了。
3.8.2. 長時間無法恢復(fù)的Connection中斷
上面討論了retry之后可以恢復(fù)Connection的情況,也有可能長時間retry之后依然不能恢復(fù)Connection,如rabbitmq掛掉的情況,不能一直retry下去阻塞接口調(diào)用。
這種情況是沒有confirm的,因?yàn)橄⒍紱]有發(fā)出去。所以處理就更簡單了:
try {
rabbitTemplate.send(messageCorrelationData.getExchange(), messageCorrelationData.getRoutingKey(),
messageCorrelationData.getMessage(), messageCorrelationData);
}catch (AmqpConnectException e) {
SendFailedMessageHolder.add(messageCorrelationData);
}
retry失敗或者沒有retry機(jī)制都會拋出AmqpConnectException,catch之后將消息保存起來即可。
3.9. 消費(fèi)端的消息去重
如果發(fā)送端采用confirm機(jī)制來做丟失消息的重發(fā),上面提到,可能會出現(xiàn)沒有丟失的消息也被重發(fā)了,導(dǎo)致消息重復(fù)。
這個問題很容易解決,MessageProperties中是有messageId屬性的,每條消息設(shè)置一個唯一的messageId即可。
Message message = messageConverter.toMessage(messageId, new MessageProperties());
message.getMessageProperties().setMessageId(messageId);
3.10. 消息發(fā)送和接收使用不同的Connection
當(dāng)一個服務(wù)同時作為消息發(fā)送端和接收端時,建議使用不同的Connection以避免一方出現(xiàn)故障影響到另一方。
并不需要做很多事情,只需RabbitTemplate配置中加一個屬性設(shè)置即可:
rabbitTemplate.setUsePublisherConnection(true);
RabbitTemplate在創(chuàng)建Connection時,會根據(jù)這個boolean參數(shù)選擇使用ConnectionFactory本身或者ConnectionFactory中的publisherConnectionFactory(也是一個ConnectionFactory)來創(chuàng)建,相關(guān)源碼如下:
/**
* Create a connection with this connection factory and/or its publisher factory.
* @param connectionFactory the connection factory.
* @param publisherConnectionIfPossible true to use the publisher factory, if present.
* @return the connection.
* @since 2.0.2
*/
public static Connection createConnection(final ConnectionFactory connectionFactory,
final boolean publisherConnectionIfPossible) {
if (publisherConnectionIfPossible) {
ConnectionFactory publisherFactory = connectionFactory.getPublisherConnectionFactory();
if (publisherFactory != null) {
return publisherFactory.createConnection();
}
}
return connectionFactory.createConnection();
}
3.11. 消息過期
在發(fā)送端,可通過如下方式設(shè)置消息過期時間:
message.getMessageProperties().setExpiration("30000");
這樣,這條消息的有效期是30秒,30秒沒有被消費(fèi)掉會被丟棄。
3.12. dead letter exchange
這個與spring-amqp無關(guān),是rabbitmq的設(shè)置。
將一個queue設(shè)置了x-dead-letter-exchange及x-dead-letter-routing-key兩個參數(shù)后,這個queue里丟棄的消息將會進(jìn)入dead letter exchange,并route到相應(yīng)的queue里去。
這里,被丟棄的消息包括:
The message is rejected (basic.reject or basic.nack) with requeue=false,
The TTL for the message expires; or
The queue length limit is exceeded.