我尝试使用Spring将事务性消息写入HA RabbitMQ集群,并在单个事务中将实体写入H2数据库。
我的代码适用于标准情况,我遇到的问题是在测试错误情况时引起的。如果我断开程序连接到的兔节点的网络电缆,在提交阶段会抛出异常,我希望回滚,但是消息以某种方式被写入兔队列并且我的实体被持久化到数据库?
以下是我收到的例外情况:
org.springframework.amqp.AmqpException: failed to commit RabbitMQ transaction
at org.springframework.amqp.rabbit.connection.RabbitResourceHolder.commitAll(RabbitResourceHolder.java:154)
at org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils$RabbitResourceSynchronization.processResourceAfterCommit(ConnectionFactoryUtils.java:256)
at org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils$RabbitResourceSynchronization.processResourceAfterCommit(ConnectionFactoryUtils.java:236)
at org.springframework.transaction.support.ResourceHolderSynchronization.afterCommit(ResourceHolderSynchronization.java:85)
at org.springframework.transaction.support.TransactionSynchronizationUtils.invokeAfterCommit(TransactionSynchronizationUtils.java:133)
at org.springframework.transaction.support.TransactionSynchronizationUtils.triggerAfterCommit(TransactionSynchronizationUtils.java:121)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.triggerAfterCommit(AbstractPlatformTransactionManager.java:954)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:799)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:726)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:515)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:291)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207)
at com.sun.proxy.$Proxy22.sendMessage(Unknown Source)
at com.test.transactional.RabbitSender.run(RabbitSender.java:32)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
Caused by: java.io.IOException
at com.rabbitmq.client.impl.AMQChannel.wrap(AMQChannel.java:106)
at com.rabbitmq.client.impl.AMQChannel.wrap(AMQChannel.java:102)
at com.rabbitmq.client.impl.AMQChannel.exnWrappingRpc(AMQChannel.java:124)
at com.rabbitmq.client.impl.ChannelN.txCommit(ChannelN.java:1163)
at com.rabbitmq.client.impl.ChannelN.txCommit(ChannelN.java:61)
at sun.reflect.GeneratedMethodAccessor5.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.springframework.amqp.rabbit.connection.CachingConnectionFactory$CachedChannelInvocationHandler.invoke(CachingConnectionFactory.java:704)
at com.sun.proxy.$Proxy24.txCommit(Unknown Source)
at org.springframework.amqp.rabbit.connection.RabbitResourceHolder.commitAll(RabbitResourceHolder.java:151)
... 18 more
Caused by: com.rabbitmq.client.ShutdownSignalException: connection error
at com.rabbitmq.utility.ValueOrException.getValue(ValueOrException.java:67)
at com.rabbitmq.utility.BlockingValueOrException.uninterruptibleGetValue(BlockingValueOrException.java:33)
at com.rabbitmq.client.impl.AMQChannel$BlockingRpcContinuation.getReply(AMQChannel.java:361)
at com.rabbitmq.client.impl.AMQChannel.privateRpc(AMQChannel.java:226)
at com.rabbitmq.client.impl.AMQChannel.exnWrappingRpc(AMQChannel.java:118)
... 26 more
Caused by: java.io.EOFException
at java.io.DataInputStream.readUnsignedByte(DataInputStream.java:290)
at com.rabbitmq.client.impl.Frame.readFrom(Frame.java:95)
at com.rabbitmq.client.impl.SocketFrameHandler.readFrame(SocketFrameHandler.java:139)
at com.rabbitmq.client.impl.AMQConnection$MainLoop.run(AMQConnection.java:536)
这是我发送消息和编写数据库实体的代码: (这在X消息循环中重复调用。如果收到异常,循环不会尝试重新发送消息)
@Service
public class SenderServiceImpl implements SenderService {
private static final Logger LOG = LoggerFactory.getLogger(SenderServiceImpl.class);
private DatabaseService databaseService;
private ConnectionFactory connectionFactory;
private RabbitTemplate template;
private AtomicLong messageId;
private final String startTime;
private final String exchange;
private final String routingKey;
@Autowired
public SenderServiceImpl( DatabaseService databaseService,
ConnectionFactory connectionFactory,
@Value("${rabbitmq.exchange}") String exchange,
@Value("${rabbitmq.routingKey}") String routingKey) {
this.databaseService = databaseService;
this.connectionFactory = connectionFactory;
this.exchange = exchange;
this.routingKey = routingKey;
this.startTime = "" + new Date().getTime();
this.messageId = new AtomicLong(0);
}
@PostConstruct
public void init() {
template = new RabbitTemplate(connectionFactory);
template.setChannelTransacted( true );
}
@Transactional(rollbackFor=AmqpException.class)
public void sendMessage( String messageString ) throws AmqpException {
String messageCorrelationId = startTime + "-" + messageId.incrementAndGet();
MessageProperties properties = new MessageProperties();
properties.setMessageId( messageCorrelationId );
Message message = new Message(messageString.getBytes(), properties );
template.send( exchange, routingKey, message, new CorrelationData( messageCorrelationId ) );
databaseService.saveMessage( messageString, messageCorrelationId );
}
}
这里提供完整的信息是我的DatabaseService实现
@Service
public class DatabaseServiceImpl implements DatabaseService {
private TestDao testDao;
@Autowired
public DatabaseServiceImpl( TestDao testDao ) {
this.testDao = testDao;
}
@Override
@Transactional
public void saveMessage( String message, String messageId ) {
testDao.saveMessage( message, messageId );
}
}
和TestDao类
@Repository
public class TestDao {
@PersistenceContext
private EntityManager entityManager;
public void saveMessage( String message, String messageId ) {
TestEntity entity = new TestEntity();
entity.setMessage( message );
entity.setMessageId( messageId );
entityManager.persist( entity );
}
}
我的applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- Configuration for Component Scan -->
<context:component-scan base-package="com.test.transactional" />
<context:property-placeholder location="classpath*:rabbitmq.properties" />
<!-- RabbitMQ Connection Factory -->
<rabbit:connection-factory id="connectionFactory"
addresses="${rabbitmq.addresses}"
username="${rabbitmq.username}"
password="${rabbitmq.password}" />
<!-- Transaction Manager-->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<!-- JPA Adapter -->
<bean id="jpaAdapter" class="org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter">
<property name="databasePlatform" value="org.eclipse.persistence.platform.database.H2Platform" />
<property name="showSql" value="true" />
</bean>
<!-- EntityManager -->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceXmlLocation" value="classpath*:META-INF/persistence.xml" />
<property name="persistenceUnitName" value="TestPU" />
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
</property>
<property name="jpaPropertyMap">
<map>
<entry key="eclipselink.weaving" value="false"/>
</map>
</property>
</bean>
<!-- This instructs spring to check for the @Transactional annotation -->
<tx:annotation-driven />
<!-- bean post-processor for JPA annotations -->
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>
</beans>
此测试应用程序通过HAProxy服务器连接到RabbitMQ。群集配置有3个兔节点。
回顾一下这个问题。如果我尝试发送30,000条消息,并从我连接的兔节点拉出网线。应用程序将在20秒内抛出几个AmqpExceptions,因此HAProxy会注意到服务器已关闭。
应用程序不会尝试重新发送任何失败的消息,但我的接收应用程序将收到完整的30,000条消息,而我的数据库将包含完整的30000个实体?
任何人都可以帮我找出原因吗?
试图进一步澄清这里的测试功能是盲目地循环尝试发送消息,无论成功与否。
for(int i = 0; i < MESSAGE_LIMIT; i++) {
try {
senderService.sendMessage( "Message:" + i );
} catch (Throwable th) {
LOG.error("Unable to send message.", th);
}
}
作为测试设置的一个示例,初始状态将发送应用程序通过HAProxy连接到Rabbit Node 1。
然后我们从HAProxy切断到Rabbit Node 1的网络连接。 HAProxy大约需要20秒才意识到它无法连接到Rabbit Node。在此期间出现AmqpException - 我希望这些事务能够回滚。
State after cutting connection
在20秒之后,HAProxy将连接发送到剩余的2个兔子节点之一,并且该过程正常继续。