摘要

一般来说,如果为JmsTemplate做了事务配置,那么它将会与当前线程的数据库事务挂钩,并且仅在数据库事务的afterCommit动作中提交。

但是,如果一个MessageListener在接收Jms消息的同时,也使用JmsTemplate发送了Jms消息;那么它发送的Jms消息将与数据库事务无关(即使为JmsTemplate做了事务配置),而是与Listener接收消息保持在同一个事务中。


问题

问题是一位同事发现的。

账务系统的垫付功能存在REST和MessageListener两个入口;两个入口中调用的是同一套代码和业务逻辑。但是,REST入口中发送的Jms消息会随着数据库事务回滚而回滚;MessageListener中却不会回滚。相关流程图说明如下。

我们期望的结果是:在还款操作中发送的Jms消息,随还款操作的数据库事务回滚而取消(红色底色部分的操作);而垫付操作中发送的Jms消息,则应随垫付操作的数据库事务提交而提交(绿色底色部分的操作)。这一点在REST入口的相关日志和数据中得到了验证。但是,从MessageListener入口调用此服务时,却出现了问题:虽然还款服务的数据库事务确实回滚了,但是其中的Jms消息却成功发送了出来(参见红色字体部分)


分析

首先,REST入口的操作、结果是正确的。这说明,当数据库事务回滚时,Jms消息确实没有提交。那么,可以肯定一点:一定是MessageListener后续处理中做了提交消息这个动作。

经过一系列的Debug和逐行执行、分析,我找到了这段代码。

MessageListener接收到消息后,会进入org.springframework.jms.listener.AbstractPollingMessageListenerContainer.receiveAndExecute( Object invoker,  Session session,  MessageConsumer consumer)方法中。由于没有配置transactionManager,我们会通过doReceiveAndExecute(invoker, session, consumer, null)来调用org.springframework.jms.listener.AbstractPollingMessageListenerContainer.doReceiveAndExecute( Object invoker,  Session session,  MessageConsumer consumer,  TransactionStatus status) 方法。

/**

 * Execute the listener for a message received from the given consumer,

 * wrapping the entire operation in an external transaction if demanded.

 * @param session the JMS Session to work on

 * @param consumer the MessageConsumer to work on

 * @return whether a message has been received

 * @throws JMSException if thrown by JMS methods

 * @see #doReceiveAndExecute

 */

protected boolean receiveAndExecute(Object invoker, Session session, MessageConsumer consumer)

        throws JMSException {

    if (this.transactionManager != null) {

        // Execute receive within transaction.

        TransactionStatus status = this.transactionManager.getTransaction(this.transactionDefinition);

        boolean messageReceived;

        try {

            messageReceived = doReceiveAndExecute(invoker, session, consumer, status);

        }

        catch (JMSException ex) {

            rollbackOnException(status, ex);

            throw ex;

        }

        catch (RuntimeException ex) {

            rollbackOnException(status, ex);

            throw ex;

        }

        catch (Error err) {

            rollbackOnException(status, err);

            throw err;

        }

        this.transactionManager.commit(status);

        return messageReceived;

    }

    else {

        // Execute receive outside of transaction.

        return doReceiveAndExecute(invoker, session, consumer, null);

    }

}

doReceiveAndExecute方法又会调用org.springframework.jms.listener.AbstractMessageListenerContainer.doExecuteListener( Session session,  Message message)方法,以便于执行我们编写的业务代码,并处理Jms相关的事务。如果业务代码中没有抛出异常,那么就会进入org.springframework.jms.listener.AbstractMessageListenerContainer.commitIfNecessary( Session session,  Message message)方法中。

/**

 * Execute the specified listener,

 * committing or rolling back the transaction afterwards (if necessary).

 * @param session the JMS Session to operate on

 * @param message the received JMS Message

 * @throws JMSException if thrown by JMS API methods

 * @see #invokeListener

 * @see #commitIfNecessary

 * @see #rollbackOnExceptionIfNecessary

 * @see #convertJmsAccessException

 */

protected void doExecuteListener(Session session, Message message) throws JMSException {

    if (!isAcceptMessagesWhileStopping() && !isRunning()) {

        if (logger.isWarnEnabled()) {

            logger.warn("Rejecting received message because of the listener container " +

                    "having been stopped in the meantime: " + message);

        }

        rollbackIfNecessary(session);

        throw new MessageRejectedWhileStoppingException();

    }

    try {

        invokeListener(session, message);

    }

    catch (JMSException ex) {

        rollbackOnExceptionIfNecessary(session, ex);

        throw ex;

    }

    catch (RuntimeException ex) {

        rollbackOnExceptionIfNecessary(session, ex);

        throw ex;

    }

    catch (Error err) {

        rollbackOnExceptionIfNecessary(session, err);

        throw err;

    }

    commitIfNecessary(session, message);

}

commitIfNecessary方法几经辗转,最终会调用到org.apache.activemq.ActiveMQConnection.syncSendPacket( Command command) 方法。这个方法的作用,是将当前Connection中的数据同步到MQ服务端。也就是在这个方法执行完毕之后,不应当发送的消息被发送了出去。

public Response syncSendPacket(Command command) throws JMSException {

    if (isClosed()) {

        throw new ConnectionClosedException();

    else {

        try {

            Response response = (Response)this.transport.request(command);

            if (response.isException()) {

                ExceptionResponse er = (ExceptionResponse)response;

                if (er.getException() instanceof JMSException) {

                    throw (JMSException)er.getException();

                else {

                    if (isClosed()||closing.get()) {

                        LOG.debug("Received an exception but connection is closing");

                    }

                    JMSException jmsEx = null;

                    try {

                        jmsEx = JMSExceptionSupport.create(er.getException());

                    catch(Throwable e) {

                        LOG.error("Caught an exception trying to create a JMSException for " +er.getException(),e);

                    }

                    if (er.getException() instanceof SecurityException && command instanceof ConnectionInfo){

                        forceCloseOnSecurityException(er.getException());

                    }

                    if (jmsEx !=null) {

                        throw jmsEx;

                    }

                }

            }

            return response;

        catch (IOException e) {

            throw JMSExceptionSupport.create(e);

        }

    }

这就是消息被错误发送的原因:MessageListener在接收消息的时候,获取了一个Connection;后续发送消息时,用的是同一个Connection。因此,尽管中间的数据库事务回滚了,但由于这个Connection最终要提交(MessageListner中没有抛出异常),用这个Connection发送的所有消息最终都被提交到了MQ上。


解决方案

方案一:使用JmsTransactionManager来管理Jms事务

可以通过以下配置,为MessageListner注入JmsTransactionManager:

<bean id="jmsTransactionManager"

    class="org.springframework.jms.connection.JmsTransactionManager">

    <property name="connectionFactory" ref="jmsConnectionFactory" />

</bean

<jms:listener-container destination-type="queue"

    transaction-manager="jmsTransactionManager" concurrency="4"

    acknowledge="transacted" connection-factory="jmsConnectionFactory">

    <jms:listener destination="queue.thread.autopay" ref="autoPayListener" />

</jms:listener-container>

但是在测试后发现,这样配置并没有用。我分析,JmsTransactionManager并不能为@Transactional(propagation = Propagation.REQUIRES_NEW)注解创建新的JmsConnection,因而,发送消息使用的仍然是接收消息时创建的connection。由于MessageListener中并没有抛出异常,JmsTransactionManager仍然会提交这个connection中的数据,并最终导致消息提交。

方案二:手动将发送消息的操作放到数据库事务的AfterCommit操作中

现有代码中,我们是在事务体内执行JmsTemplate.send()操作;在事务的AfterCommit操作中执行Session.commit()。

如果我们将JmsTemplate.send()操作放到AfterCommit操作中,那么就可以确保只在数据库事务提交后,才会提交Jms消息了。

此方案验证可行。验证代码如下:

public void send(Event event, List<TransferParam> transferParams) {

    TransactionSynchronizationManager

        .registerSynchronization(new TransactionSynchronizationAdapter() {

            @Override

            public void afterCommit() {

                System.out.println(this.getClass() + " - " + event + "--"

                    + transferParams);

                try {

                    event.getTychoOperType().ifPresent(

                        (value) -> {

                            TychoProductor4Account.this.doSend(event,

                                transferParams);

                        });

                catch (Exception e) {

                    System.out.println(e.getMessage());

                    TychoProductor4Account.LOGGER.error("发送数据到tycho异常:{}",

                        e);

                }

            }

        });

}

方案三:手动在数据库事务的RollBack操作中回滚Jms消息

暂未找到实现方式。

方案四:尝试为发送消息创建并使用新的Connection

代码流程中之所以会使用同一个Connection,是因为接收、发送消息时,都是从线程上下文中尝试获取JmsResourceHolder,并从其中获取连接的。

那么,简单做法就是在接收到消息后,开启一个子线程;复杂做法则是为JmsTransactionManager编写识别@Transactional(propagation = Propagation.REQUIRES_NEW)注解的功能。

开启子线程的方案可行。验证代码如下:

Future<Event> actualResult = this.keplerRestExecutor.submit(() -> {

            Event4Reserve event4Reserve = new Event4Reserve();

            event4Reserve.setRecordId(recordId);

            event4Reserve.setUserId(ThreadConsts.SYSTEM_USER_ID);

            AutoPayListener4BaeEvent.LOGGER.debug("event4Reserve={}",

                event4Reserve);

            Event result = this.bizAccountEventService.handle(event4Reserve);

            AutoPayListener4BaeEvent.LOGGER.info("result={}", result);

            return result;

        });

        try {

            actualResult.get();

        catch (InterruptedException e) {

            AutoPayListener4BaeEvent.LOGGER.error("线程被中断!", e);

            throw new RuntimeException("垫付线程中断!", e);

        catch (ExecutionException e) {

            AutoPayListener4BaeEvent.LOGGER.error("执行过程出错!", e);

            Throwable real = e.getCause();

            if (real instanceof RuntimeException) {

                throw (RuntimeException) real;

            else {

                throw new RuntimeException(real);

            }

        }

方案五:使用org.springframework.jms.connection.CachingConnectionFactory

已验证,方案无效。

测试配置如下:

<bean id="jmsTransactionManager"

    class="org.springframework.jms.connection.JmsTransactionManager">

    <property name="connectionFactory" ref="jmsConnectionFactory" />

</bean>

<bean id="jmsConnectionFactory"

    class="org.springframework.jms.connection.CachingConnectionFactory">

    <property name="targetConnectionFactory" ref="targetActiveMqConnectionFactory" />

    <property name="sessionCacheSize" value="10" />

</bean>

<amq:connectionFactory id="targetActiveMqConnectionFactory"

    brokerURL="${jms.url.failover}">

    <amq:redeliveryPolicyMap>

        <amq:redeliveryPolicyMap>

            <amq:defaultEntry>

                <!-- 5次,每次30秒 -->

                <amq:redeliveryPolicy maximumRedeliveries="5"

                    initialRedeliveryDelay="30000" />

            </amq:defaultEntry>

            <amq:redeliveryPolicyEntries>

                <!-- 5次,每次10秒 -->

                <amq:redeliveryPolicy queue="queue.thread.autopay"

                    maximumRedeliveries="5" initialRedeliveryDelay="10000" />

            </amq:redeliveryPolicyEntries>

            <amq:redeliveryPolicyEntries>

                <!-- 银联实时划扣超时限制 ,5次,每次90秒 -->

                <amq:redeliveryPolicy queue="queue.thread.instantUnionpay"

                    maximumRedeliveries="5" initialRedeliveryDelay="90000" />

            </amq:redeliveryPolicyEntries>

        </amq:redeliveryPolicyMap>

    </amq:redeliveryPolicyMap>

</amq:connectionFactory>

<jms:listener-container destination-type="queue"

    transaction-manager="jmsTransactionManager" concurrency="4"

    acknowledge="transacted" connection-factory="jmsConnectionFactory">

    <jms:listener destination="queue.thread.autopay" ref="autoPayListener" />

</jms:listener-container>

方案六:为jmsTemplate和MessageListener配置不同的ConnectionFactory

验证可行。测试配置如下:

<bean id="newJmsTemplate" class="org.springframework.jms.core.JmsTemplate">

    <property name="connectionFactory" ref="targetActiveMqConnectionFactory" />

    <property name="sessionTransacted" value="true" />

    <property name="explicitQosEnabled" value="${activemq.explicitQosEnabled}" />

    <property name="timeToLive" value="86400000" />

</bean>

<amq:connectionFactory id="targetActiveMqConnectionFactory"

    brokerURL="${jms.url.failover}">

</amq:connectionFactory>

<jms:listener-container destination-type="queue"

    concurrency="4" acknowledge="transacted" connection-factory="jmsConnectionFactory">

    <jms:listener destination="queue.thread.autopay" ref="autoPayListener" />

</jms:listener-container>

<amq:connectionFactory id="jmsConnectionFactory"

    brokerURL="${jms.url.failover}">

</amq:connectionFactory>


后续工作

基本已经验证完毕。

Jms的MessageListener中的Jms事务的更多相关文章

  1. Welogic中的JMS

    WebLogic JMS 概述 企业消息传递系统使得应用程序能够通过消息的交换与其他系统之间进行通信.消息是包含协调不同应用程序之间通信所需信息的请求.报告和/或事 件.消息提供了提取级别,使您能够从 ...

  2. JMS 在 SpringBoot 中的使用

    当前环境 Mac OS 10.11.x docker 1.12.1 JDK 1.8 SpringBoot 1.5 前言 基于之前一篇“一个故事告诉你什么是消息队列”,了解了消息队列的使用场景以及相关的 ...

  3. JMS介绍:我对JMS的理解和认识

    [ZT]JMS介绍:我对JMS的理解和认识 转自:http://blog.csdn.net/KimmKing/archive/2011/06/30/6577021.aspx,感谢作者KimmKing ...

  4. Jms学习篇一:JMS介绍

    1.JMS介绍: JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API JMS是一种与厂商无关的 API,用来访问 ...

  5. Spring 中的 JDBC 事务

    Spring 对 JDBC 的支持 JdbcTemplate 简介 •为了使 JDBC 更加易于使用, Spring 在 JDBC API 上定义了一个抽象层, 以此建立一个 JDBC 存取框架. • ...

  6. 实例详解 EJB 中的六大事务传播属性--转

    前言 事务 (Transaction) 是访问并可能更新数据库中各种数据项的一个程序执行单元 (unit).在关系数据库中,一个事务可以是一条或一组 SQL 语句,甚至整个程序.它有通常被称为 ACI ...

  7. 领域模型中分散的事务如何集中统一处理(C#解决方案)

    领域模型中分散的事务如何集中统一处理(C#解决方案)   开篇 什么是事务,事务的应用场景 做项目时,经常会遇到一些需求,比如注册用户时,要求同时存入用户的基本信息和初始化该用户的帐户,如果在这两个环 ...

  8. Django中的原子事务相关注意事项

    Django中的原子事务支持(transaction.atomic)方式函数装饰器或者with语句,这种方式特别是前者和spring里面的AOP事务支持方式基本等同,当然其实质方式都是原始的try.. ...

  9. spring中注解式事务不生效的问题

    常用的解决方法可以百度,我针对我的问题描述一下 Mysql中InnoDB引擎才支持事务, MyISAM不支持事务. 当你尝试了各种方法解决spring中注解式事务不生效时, 一定要查看一下数据库中表的 ...

随机推荐

  1. DBUtils学习总结

    这几天闲着无聊,就看了一下DBUtils这个数据库组件.中间有了一些想法,现在记录下来. 文章主要分几部分 1 最简单同时也是最经常使用的一些范例 2 学习源码前的一些知识储备 3 我自己写的mydb ...

  2. Android官方技术文档翻译——IntelliJ 项目迁移

    本文译自Android官方技术文档<Migrating from IntelliJ Projects>,原文地址:http://tools.android.com/tech-docs/ne ...

  3. SwipeRefreshLayout实现上拉下拉刷新

    1:在布局中添加SwipeRefreshLayout和Listview组件 [html] view plain copy <?xml version="1.0" encodi ...

  4. 怎样使用projectproperty sheet(.vsprops)来管理工程

    怎样使用projectproperty sheet(.vsprops)来管理工程 IDE:VS2005 前言 Project Property Sheet的意思是项目属性表,在大型项目中基本上都会使用 ...

  5. inittab文件剖析[CentOS 5.X](第二版)

    inittab文件剖析[CentOS 5.X] grep -v "^#" /etc/inittab | more #过滤掉以#开头的行 inittab文件格式 id:runleve ...

  6. struts2 easyui实现datagrid的crud

    最近两天因为项目需要,接触了easyui,要用它的datagrid实现crud.第一次做,花了一天时间才完成所有功能,昨天做另外一个模块,同样的功能只用了两个小时. 现在把第一次做datagrid时遇 ...

  7. ”()“和”[]“引发的血案——由此引出C++中关键词new

    先来看一个程序吧: #include <iostream> #include <cassert> using namespace std; int main() { ; int ...

  8. C++实现二叉树

    #include <iostream> using namespace std ; class Tree { public : int number ; class Tree *left ...

  9. Python3玩转儿 机器学习(5)

    numpy 的使用 numpy.array基础 import numpy numpy.__version__ #查询当前numpy的版本 '1.14.0' import numpy as np np. ...

  10. Spring3 MVC

    一.前言: 大家好,Spring3 MVC是非常优秀的MVC框架,由其是在3.0版本发布后,现在有越来越多的团队选择了Spring3 MVC了.Spring3 MVC结构简单,应了那句话简单就是美,而 ...