使用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的代码就明白了。

相关文章
相关标签/搜索