使用Spring/Spring Boot集成JMS的陷阱

Githubhtml

本文旨在指出Spring/Spring Boot中集成JMS的一些性能陷阱,在另外一篇文章Spring JMS各組件詳解裏有Spring JMS組件介紹及如何正確使用的內容。java

JmsTemplate性能問題

Spring提供了JmsTemplate用來簡化JMS的操做,可是JmsTemplate的性能是有很大問題的,主要體如今如下幾個方面:git

頻繁建立connection,session,producer

根據Artemis官方文檔的說法,JmsTemplate是一種anti-pattern,若是在使用JmsTemplate的狀況下以爲很慢,那麼就不要怪Artemis。github

而且ConnectionSessionProducerConsumer這些對象自己設計上是能夠複用的。所以JmsTemplate的作法會大大下降性能,而且將大部分的時間都花在反覆重建這些對象上。spring

Spring提供的Workaroundapache

可讓JmsTemplate使用SingleConnectionFactory避免頻繁建立Connection的問題。或者其子類CachingConnectionFactory避免頻繁建立ConnectionSessionProducerConsumer的問題。api

頻繁建立臨時Queue

JmsTemplate#sendAndReceive方法能夠用來模擬RPC調用過程,內部原理是:session

  1. A程序建立一個臨時Queue做爲接受相應的Queue
  2. send一個Message到Queue,並在這個臨時Queue上等待結果
  3. B程序consume了這個Message把結果send到那個臨時Queue
  4. A接受到結果,把這個臨時Queue幹掉

固然整個過程當中還包括前面提到的建立ConnectionSessionProducerConsumer的動做。多線程

不出所料,Artemis官方文檔也提到了,頻繁建立臨時Queue是一種anti-pattern,會大大影響性能。併發

@JmsListener性能問題

使用sync方法consume消息

使用@JmsListener註解能夠很方便的用來consume消息,但它是同步而非異步,這個和官方文檔所說的偏偏相反(關於sync/async consume消息的資料)。

Spring Boot的JmsAnnotationDrivenConfiguration默認使用DefaultJmsListenerContainerFactory生成DefaultMessageListenerContainer ,而它的內部原理是使用TaskExecutor發起多個線程同時從Queue中拉取消息,這也就是爲何Spring官方文檔裏說若是監聽的是Topicconcurrency > 1,那麼可能會收到重複消息的緣由。

DefaultMessageListenerContainer的javadoc中說道:

Actual MessageListener execution happens in asynchronous work units which are created through Spring's TaskExecutor abstraction

這裏就有個矛盾,若是使用async的方式consume消息,那麼只需給consumer設置MessageListener就好了,何須使用TaskExecutor呢?

一看代碼果真不出所料:

  1. DefaultMessageListenerContainer#runcallinvokeListener
  2. 而後callAbstractPollingMessageListenerContainer#receiveAndExecute
  3. 而後calldoReceiveAndExecute
  4. 而後callreceiveMessage
  5. 而後callreceiveFromConsumer
  6. 而後callJmsDestinationAccessor#receiveFromConsumer這個方法調用了MessageConsumer#consume

也就是說Spring只是使用一個或多個線程在不停的同步的consume消息而已。

雖然可使用concurrency參數提升併發,可是多線程從Queue/Topic中consume消息的性能比javax.jms.MessageConsumer#setMessageListener的方法要低上不少。

有興趣的同窗可使用Artemis Example(下載地址)裏的JMS Perf代碼作一下測試,它的測試代碼用的是javax.jms.MessageConsumer#setMessageListener的方式來consume消息的,在配置正確的狀況下能夠達到接近10w/s。至於使用MessageConsumer.receive的性能測試則留給同窗本身作了。

爲@JmsListener建立的session默認transacted=true

仍是以前提到的JmsAnnotationDrivenConfiguration,使用的DefaultJmsListenerContainerFactoryConfigurer默認是把session設置爲transacted的。

根據測算,當一個session是transacted的時候其性能會相差20%,有興趣的同窗可使用Artemis Example(下載地址)裏的JMS Perf代碼作一下測試。

下載以後找到examples/jms/perf目錄,看這個目錄下的readme.html得到執行方法,在執行以前修改src/main/resources/perf.properties文件,下面是部分參數的解釋:

  1. num-messages,測試多少個消息
  2. num-warmup-messages,熱身用的消息數,熱身過以後性能會提高
  3. durable,對應DeliveryMode
  4. transacted,是否transacted
  5. batch-size,批量commit的大小
  6. drain-queue,是否測試前先把隊列裏已有的消息都清空

可使用如下兩套配置對比transacted的性能差異:

配置一,transacted=false:

num-messages=100000
num-warmup-messages=1000
message-size=1024
durable=false
transacted=false
batch-size=1
drain-queue=true
destination-lookup=perfQueue
connection-factory-lookup=/ConnectionFactory
throttle-rate=-1
dups-ok-acknowledge=false
disable-message-id=true
disable-message-timestamp=true

配置二,transacted=true:

num-messages=100000
num-warmup-messages=1000
message-size=1024
durable=false
transacted=true
batch-size=1
drain-queue=true
destination-lookup=perfQueue
connection-factory-lookup=/ConnectionFactory
throttle-rate=-1
dups-ok-acknowledge=false
disable-message-id=true
disable-message-timestamp=true

@JmsListener建立的session默認加入了事務控制

關於加入事務控制是否會有性能問題沒有實際測試過,不過值得注意的這是Spring Boot的默認行爲。

相關鏈接:代碼1, 代碼2代碼3Javadoc

Spring Boot配置

ConnectionFactory全局只有一個實例

Spring將JMS的集成變得很是簡單,只需提供幾個配置參數就能夠了,可是隻能提供一種默認配置,無論業務場景如何都使用同一種配置是很是有問題的。

spring-boot提供瞭如下幾種方式(文檔)集成JMS:

  1. JNDI
  2. Artemis, native模式和embedded模式
  3. ActiveMQ

這幾種模式的缺點都是隻能配置一個ConnectionFactory,這對於簡單應用來講沒問題,對於複雜應用來講就顯得不夠用了,這讓你喪失了針對不一樣場景配置不一樣ConnectionFactory的機會。
並且Artemis的native模式只支持host:port,不支持更豐富參數的url模式。

@JmsListener的默認配置也就只提供一種

看過DefaultJmsListenerContainerFactoryConfigurerJmsAnnotationDrivenConfiguration的代碼就明白了。

相關文章
相關標籤/搜索