背景:公司的老系统是SSM框架编写的,因为需要加入rabbitmq,但网上关于rabbitmq与SSM的整合非常少,所以本文章就是为了记录一下这段时间整合时所遇到的问题。若你的项目是Springboot的,你也可以借鉴我所写的代码。
这篇文章将会解决 Rabbitmq 技术落地时的一系列问题,若有部分问题没有出现,希望大家可以留言,一起解决!
生产者的消息可靠性投递,是为了防止消费者发送消息到mq的这段距离出现网络问题而导致发送失败。如下图所示:
对此,rabbitmq出给了三种解决方法:transaction事物机制、confirm模式、return模式。
发送消息前,开启事务(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事务就会回滚(channel.txRollback()),回滚后消息不会发送到mq中,如果发送成功则提交事务(channel.txCommit()),提交后,消息才会发送到mq。这种方式是很灵活的,可以根据不同的业务场景做操作,但缺点就是这些操作会侵入代码。
以上说的是Rabbitmq自带的事物,这种事物是编程式的,但正常来说,我们的业务方法(Service)都是响应式的,即打上@Transactional 注解,这种写法显然没有那么容易与我们的业务方法相结合。我们是希望业务方法执行成功才发送消息,若执行失败了消息就不发送。那有没有一种方法,可以结合业务方法的事物呢?答案是有的,我们把发送消息的操作同样交由Spring去处理就可以了。详见如下代码实现:
public class TransactionalUtil {
public static void doAfterTransaction(DoTransactionalCompletion doTransactionalCompletion) {
//判断当前上下文有没有事物激活
if (TransactionSynchronizationManager.isActualTransactionActive()) {
//如果有的话,就将回调接口注册入事物管理器中。事物执行成功后,回调接口内的方法将会被调用
TransactionSynchronizationManager.registerSynchronization(doTransactionalCompletion);
}
}
//eg:
@Transactional
public void doTx() {
// start tx
TransactionalUtil.doAfterTransaction(new DoTransactionalCompletion(() -> {
// send MQ... RPC...
}));
// end tx
}
}
class DoTransactionalCompletion implements TransactionSynchronization {
private Runnable runnable;
public DoTransactionalCompletion(Runnable runnable) {
this.runnable = runnable;
}
/**
* 回调函数。本地事物执行完之后要做的事情
*
* @param status
*/
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_COMMITTED) {
this.runnable.run();
}
}
}
参考极海的Spring事物拓展:https://www.bilibili.com/video/BV168411h7PU/
当消息从生产者发送到mq时,不论发送是否成功都会返回一个confirmCallback,当发送成功时这个confirmCallback为true,当发送失败时这个confirmCallback为false。相当于是生产者每次发送消息,无论成功与否,都会收到一个通知。优势是一次配置,可全局生效。
消息从交换机发送到队列时,若投递失败则会返回一个returnCallback。
在本次项目中,我们就选中了confirm模式。特别注意:confirm模式与return模式只能二选一!
配置:
<rabbit:connection-factory id="defaultConnectionFactory"
addresses="${rabbitmq.addresses}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtualHost}"
publisher-confirms="true"/>
<rabbit:template id="defaultRabbitTemplate" retry-template="defaultRetryTemplate"
message-converter="jackson2JsonMessageConverter" connection-factory="defaultConnectionFactory"
mandatory="true" confirm-callback="RabbitmqMessageConfirm"/>
<bean id="RabbitmqMessageConfirm" class="com.hand.htms.config.RabbitmqMessageConfirm"/>
业务处理类,可以看到,我们消息发送成功后,会记录到日志内;若发送失败了,则会有钉钉通知,这时候就需要人工接入修复了。
public class RabbitmqMessageConfirm implements RabbitTemplate.ConfirmCallback {
private Logger logger = LoggerFactory.getLogger(RabbitmqMessageConfirm.class);
@Autowired
public RabbitTemplate rabbitTemplate;
@Autowired
public DingtalkUtils dingtalkUtils;
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
logger.info("生产者消息发送至Rabbitmq成功");
} else {
String message = "生产者有一条消息发送至Rabbitmq失败,但数据已保存到报文表,且生产者会重发5次,请后续人工查看报文表并确认数据是否成功重发至Rabbitmq \n\r失败原因:" + cause;
logger.warn(message);
dingtalkUtils.sendDingTalkInterfaceExceptionNotifier(new Exception(cause), message);
}
}
}
发送失败后,我们可以配置重发机制:
<bean id="defaultRetryTemplate" class="org.springframework.retry.support.RetryTemplate">
<property name="backOffPolicy">
<bean class="org.springframework.retry.backoff.ExponentialBackOffPolicy">
<property name="initialInterval" value="500" />
<property name="multiplier" value="10.0" />
<property name="maxInterval" value="10000" />
bean>
property>
<property name="retryPolicy">
<bean class="org.springframework.retry.policy.SimpleRetryPolicy">
<property name="maxAttempts" value="5"/>
bean>
property>
bean>
若想要满足消息在rabbitmq中持久化,只需要满足两个条件即可。第一:交换机和队列是持久化的。第二:消息是可持久化的。
我们在声明交换机和队列的时候,默认就是持久化声明的(SSM配置下)
交换机:
队列:
若你使用的是 RabbitTemplate
进行进行消息发送,则默认也是消息持久化的,源码观看可点击这个连接:http://www.qb5200.com/article/472439.html
rabbitmq 是一次性将 大量消息 打入消费者的,消费者接受到这些消息之后,是 立马标记为已确认 的。若这个时候,消费者宕机了;那么所有的消息将会丢失。但我们不想失去任何消息。如果一个消费者死了,我们希望把任务交给另一个消费者。
为了解决这个问题,我们需要设置,第一:每次只拿1条消息消费;第二:我们手动进行消息确认,只有消息完成了才进行确认。
通过设置服务质量,我们可以限定每次只消费1条消息
channel.basicQos(1);
在消费者监听器容器中,设置参数 acknowledge 为 manual 模式即可
<rabbit:listener-container connection-factory="defaultConnectionFactory" acknowledge="manual" requeue-rejected="false" >
<rabbit:listener ref="AppOutDoorListener" queue-names="app_outdoor"/>
<rabbit:listener ref="AppDeadListener" queue-names="app_dead_queue"/>
rabbit:listener-container>
消息手动确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); //消息确认
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false); //消息拒收
重复消费的原因:正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除;但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。
网上防止重复消息的方法有很多如这篇文章写到,可以用redis实现。https://blog.csdn.net/javaxuanshou/article/details/110228784
但由于我们的系统是将消息存储在数据库中的,我们可以直接从数据库中找到这条消息,然后判断他的状态即可。若消息之前是消费过的,则不可能为“N”状态。(未消费的状态是 “N”,消费成功是“F”,消费失败是“E”)
InoutSendxnyBase sendxnyBase = new InoutSendxnyBase();
sendxnyBase.setId(id);
sendxnyBase = inoutSendxnyBaseService.selectByPrimaryKey(iRequest, sendxnyBase);
//如果状态是F或者E,则不执行,防止消息重复消费
if ("F".equals(sendxnyBase.getStatus()) || "E".equals(sendxnyBase.getStatus())) {
channel.basicAck(deliveryTag, false);
return;
}
在我们系统中,实现的是重试 5 次,每次重试间隔0.5s,若5次均失败了,会直接进入死信队列中。
<rabbit:queue name="app_dead_queue" auto-declare="true"/>
<rabbit:queue name="app_outdoor" auto-declare="true">
<rabbit:queue-arguments>
<entry key="x-dead-letter-exchange" value="app_dead_exchange"/>
<entry key="x-dead-letter-routing-key" value="app_outdoor_dead_key"/>
rabbit:queue-arguments>
rabbit:queue>
<rabbit:direct-exchange name="app_dead_exchange">
<rabbit:bindings>
<rabbit:binding queue="app_dead_queue" key="app_outdoor_dead_key"/>
rabbit:bindings>
rabbit:direct-exchange>
<bean id="AppOutDoorListener" class="com.hand.tms.miniapp.listener.AppOutDoorListener"/>
<bean id="AppDeadListener" class="com.hand.tms.miniapp.listener.AppDeadListener"/>
<rabbit:listener-container connection-factory="defaultConnectionFactory" acknowledge="manual" requeue-rejected="false" >
<rabbit:listener ref="AppOutDoorListener" queue-names="app_outdoor"/>
<rabbit:listener ref="AppDeadListener" queue-names="app_dead_queue"/>
rabbit:listener-container>
public class AppOutDoorListener implements ChannelAwareMessageListener {
private Logger logger = LoggerFactory.getLogger(AppOutDoorListener.class);
@Autowired
private IInoutSendxnyBaseService inoutSendxnyBaseService;
@Autowired
private IGuangxinService guangxinService;
@Autowired
private RedisTemplate redisTemplate;
@Override
public void onMessage(Message message, Channel channel) throws Exception {
//重试次数
int retryCount = 0;
long deliveryTag = message.getMessageProperties().getDeliveryTag();
Long id = null; //消息的id
IRequest iRequest = new ServiceRequest();
String errLog = null; //错误日志
channel.basicQos(1); //一次只接受一条未确认的消息
while (retryCount < 5) {
retryCount++;
try {
//业务操作START
String content = new String(message.getBody());
id = new Long(content);
logger.info("InoutSendxnyBase的id是{}", id);
//根据id找 InoutSendxnyBase
InoutSendxnyBase sendxnyBase = new InoutSendxnyBase();
sendxnyBase.setId(id);
sendxnyBase = inoutSendxnyBaseService.selectByPrimaryKey(iRequest, sendxnyBase);
if (StrUtil.isEmpty(sendxnyBase.getStatus())) {
throw new Exception("数据库未找到InoutSendxnyBase");
}
//如果状态是F或者E,则不执行,防止消息重复消费
if ("F".equals(sendxnyBase.getStatus()) || "E".equals(sendxnyBase.getStatus())) {
channel.basicAck(deliveryTag, false);
return;
}
//业务操作
*****
//业务操作完成后,报文状态更新到数据库
sendxnyBase.setStatus("F");
sendxnyBase.setLastUpdateDate(new Date());
inoutSendxnyBaseService.updateByPrimaryKeySelective(iRequest, sendxnyBase);
//业务操作END
//若没有报错,消息确认
channel.basicAck(deliveryTag, false);
return;
} catch (Exception e) {
errLog = "id为" + id + "的InoutSendxnyBase,出错的原因是:" + e.toString();
logger.warn(errLog);
String errorTip = "第" + retryCount + "次消费失败" +
((retryCount < QueueConstant.MAX_RETRIES) ? "," + QueueConstant.RETRY_INTERVAL + "s后重试" : ",进入死信队列");
logger.warn(errorTip);
//间隔时间,重试
Thread.sleep(0.5 * 1000);
}
}
//重试到达5次,消息拒接,进入死信
if (retryCount == 5) {
// 使用redis存储错误信息
String key = "InoutSendxnyBase:" + id;
redisTemplate.opsForValue().set(key, errLog, 30, TimeUnit.MINUTES);
channel.basicNack(deliveryTag, false, false);
}
}
}
public class AppDeadListener implements ChannelAwareMessageListener {
private Logger logger = LoggerFactory.getLogger(AppDeadListener.class);
@Autowired
private IInoutSendxnyBaseService inoutSendxnyBaseService;
@Autowired
private DingtalkUtils dingtalkUtils;
@Autowired
private RedisTemplate redisTemplate;
@Override
public void onMessage(Message message, Channel channel) throws Exception {
try {
channel.basicQos(1); //一次只接受一条未确认的消息
String content = new String(message.getBody());
Long id = new Long(content);
logger.info("进入死信队列的InoutSendxnyBase的id是{}", id);
IRequest iRequest = new ServiceRequest();
//找到id对应的 InoutSendxnyBase 报文
InoutSendxnyBase sendxnyBase = new InoutSendxnyBase();
sendxnyBase.setId(id);
sendxnyBase = inoutSendxnyBaseService.selectByPrimaryKey(iRequest, sendxnyBase);
//报文请求错误,状态、错误信息保存到数据库中
String key = "InoutSendxnyBase:" + id;
String errLog = (String) redisTemplate.opsForValue().get(key);
sendxnyBase.setStatus("E");
sendxnyBase.setAttribute1(errLog);
sendxnyBase.setLastUpdateDate(new Date());
inoutSendxnyBaseService.updateByPrimaryKeySelective(iRequest, sendxnyBase);
dingtalkUtils.sendDingTalkInterfaceExceptionNotifier(new Exception(errLog), "业务队列出现异常,已进入死信队列");
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
logger.warn("死信队列出现异常,异常信息是:{}", e.getMessage());
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
}
配置文件:
<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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit-1.7.xsd">
<context:property-placeholder location="classpath:config.properties"/>
<rabbit:connection-factory id="defaultConnectionFactory"
addresses="${rabbitmq.addresses}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtualHost}"
publisher-confirms="true"/>
<rabbit:template id="defaultRabbitTemplate" retry-template="defaultRetryTemplate"
message-converter="jackson2JsonMessageConverter" connection-factory="defaultConnectionFactory"
mandatory="true" confirm-callback="RabbitmqMessageConfirm"/>
<rabbit:admin id="defaultRabbitAdmin" connection-factory="defaultConnectionFactory"/>
<rabbit:direct-exchange name="defaultDirectExchange" id="defaultDirectExchange"/>
<rabbit:topic-exchange name="defaultTopicExchange" id="defaultTopicExchange"/>
<bean id="defaultRetryTemplate" class="org.springframework.retry.support.RetryTemplate">
<property name="backOffPolicy">
<bean class="org.springframework.retry.backoff.ExponentialBackOffPolicy">
<property name="initialInterval" value="500" />
<property name="multiplier" value="10.0" />
<property name="maxInterval" value="10000" />
bean>
property>
<property name="retryPolicy">
<bean class="org.springframework.retry.policy.SimpleRetryPolicy">
<property name="maxAttempts" value="5"/>
bean>
property>
bean>
<bean class="com.hand.hap.message.impl.rabbitmq.ListenerContainerFactory"/>
<bean id="jackson2JsonMessageConverter" class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter">
<property name="jsonObjectMapper" ref="objectMapper"/>
bean>
<bean id="RabbitmqMessageConfirm" class="com.hand.htms.config.RabbitmqMessageConfirm"/>
<rabbit:queue name="app_dead_queue" auto-declare="true"/>
<rabbit:queue name="app_outdoor" auto-declare="true">
<rabbit:queue-arguments>
<entry key="x-dead-letter-exchange" value="app_dead_exchange"/>
<entry key="x-dead-letter-routing-key" value="app_outdoor_dead_key"/>
rabbit:queue-arguments>
rabbit:queue>
<rabbit:direct-exchange name="app_dead_exchange">
<rabbit:bindings>
<rabbit:binding queue="app_dead_queue" key="app_outdoor_dead_key"/>
rabbit:bindings>
rabbit:direct-exchange>
<bean id="AppOutDoorListener" class="com.hand.tms.miniapp.listener.AppOutDoorListener"/>
<bean id="AppDeadListener" class="com.hand.tms.miniapp.listener.AppDeadListener"/>
<rabbit:listener-container connection-factory="defaultConnectionFactory" acknowledge="manual" requeue-rejected="false" >
<rabbit:listener ref="AppOutDoorListener" queue-names="app_outdoor"/>
<rabbit:listener ref="AppDeadListener" queue-names="app_dead_queue"/>
rabbit:listener-container>
beans>
生产者:我这里异步延时1s再发送到mq,为了防止事物还没提交,而消费者消费过快导致消息失败
poolTaskExecutor.execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//放在此处发送消息,是为了防止回滚而导致将脏数据存入消息队列
for (InoutSendxnyBase inoutSendxnyBase : inoutSendxnyBases) {
logger.info("在异步线程池中发送消息至rabbitmq,InoutSendxnyBase的id是{}", inoutSendxnyBase.getId());
//取插入数据后的id 放入消息队列中,延迟1s
rabbitTemplate.convertAndSend(QueueConstant.appOutDoorQueue,inoutSendxnyBase.getId());
}
});
消费者:
public class AppOutDoorListener implements ChannelAwareMessageListener {
private Logger logger = LoggerFactory.getLogger(AppOutDoorListener.class);
@Autowired
private IInoutSendxnyBaseService inoutSendxnyBaseService;
@Autowired
private IGuangxinService guangxinService;
@Autowired
private RedisTemplate redisTemplate;
@Override
public void onMessage(Message message, Channel channel) throws Exception {
//重试次数
int retryCount = 0;
long deliveryTag = message.getMessageProperties().getDeliveryTag();
Long id = null; //消息的id
IRequest iRequest = new ServiceRequest();
String errLog = null; //错误日志
channel.basicQos(1); //一次只接受一条未确认的消息
while (retryCount < 5) {
retryCount++;
try {
//业务操作START
String content = new String(message.getBody());
id = new Long(content);
logger.info("InoutSendxnyBase的id是{}", id);
//根据id找 InoutSendxnyBase
InoutSendxnyBase sendxnyBase = new InoutSendxnyBase();
sendxnyBase.setId(id);
sendxnyBase = inoutSendxnyBaseService.selectByPrimaryKey(iRequest, sendxnyBase);
if (StrUtil.isEmpty(sendxnyBase.getStatus())) {
throw new Exception("数据库未找到InoutSendxnyBase");
}
//如果状态是F或者E,则不执行,防止消息重复消费
if ("F".equals(sendxnyBase.getStatus()) || "E".equals(sendxnyBase.getStatus())) {
channel.basicAck(deliveryTag, false);
return;
}
//业务操作
*****
//业务操作成功后,报文状态更新到数据库
sendxnyBase.setStatus("F");
sendxnyBase.setLastUpdateDate(new Date());
inoutSendxnyBaseService.updateByPrimaryKeySelective(iRequest, sendxnyBase);
//业务操作END
//若没有报错,消息确认
channel.basicAck(deliveryTag, false);
return;
} catch (Exception e) {
errLog = "id为" + id + "的InoutSendxnyBase,出错的原因是:" + e.toString();
logger.warn(errLog);
String errorTip = "第" + retryCount + "次消费失败" +
((retryCount < QueueConstant.MAX_RETRIES) ? "," + QueueConstant.RETRY_INTERVAL + "s后重试" : ",进入死信队列");
logger.warn(errorTip);
//间隔时间,重试
Thread.sleep((0.5 * 1000);
}
}
//重试到达5次,消息拒接,进入死信
if (retryCount == 5) {
// 使用redis存储错误信息
String key = "InoutSendxnyBase:" + id;
redisTemplate.opsForValue().set(key, errLog, 30, TimeUnit.MINUTES);
channel.basicNack(deliveryTag, false, false);
}
}
}
死信队列:
public class AppDeadListener implements ChannelAwareMessageListener {
private Logger logger = LoggerFactory.getLogger(AppDeadListener.class);
@Autowired
private IInoutSendxnyBaseService inoutSendxnyBaseService;
@Autowired
private DingtalkUtils dingtalkUtils;
@Autowired
private RedisTemplate redisTemplate;
@Override
public void onMessage(Message message, Channel channel) throws Exception {
try {
channel.basicQos(1); //一次只接受一条未确认的消息
String content = new String(message.getBody());
Long id = new Long(content);
logger.info("进入死信队列的InoutSendxnyBase的id是{}", id);
IRequest iRequest = new ServiceRequest();
//找到id对应的 InoutSendxnyBase 报文
InoutSendxnyBase sendxnyBase = new InoutSendxnyBase();
sendxnyBase.setId(id);
sendxnyBase = inoutSendxnyBaseService.selectByPrimaryKey(iRequest, sendxnyBase);
//报文请求错误,状态、错误信息保存到数据库中
String key = "InoutSendxnyBase:" + id;
String errLog = (String) redisTemplate.opsForValue().get(key);
sendxnyBase.setStatus("E");
sendxnyBase.setAttribute1(errLog);
sendxnyBase.setLastUpdateDate(new Date());
inoutSendxnyBaseService.updateByPrimaryKeySelective(iRequest, sendxnyBase);
dingtalkUtils.sendDingTalkInterfaceExceptionNotifier(new Exception(errLog), "业务队列出现异常,已进入死信队列");
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
logger.warn("死信队列出现异常,异常信息是:{}", e.getMessage());
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
}