rabbitMq事务消息方案

项目地址

可靠mq项目:reliable-mq
支持消息可靠发送、幂等消费、顺序消费和可靠消费

问题

一般发送mq的场景为:本地修改业务数据,数据修改成功,调用RabbitTemplate.send方法发送mq消息。类似下面的伪代码:

public class DemoService() {
     
	public Demo doSomething() {
     
		// 1、在一个事务中完成数据库数据的变更
		Object result = demoManager.changeDataIntransaction();
		// 2、做一些数据转换,得到返回值
		Demo demo = convertToDemo(result); 
		// 3、发送mq
		rabbitTemplate.send(message);
		return demo;
	}
	......
}

像这样的代码,就保证不了mq消息毕竟会被发送。
1、rabbitTemplate.send方法直接抛出异常,如超时
2、convertToDemo方法抛出异常
3、rabbitTemplate.send调用成功,消息到了rabbitMq服务器,但是在mq落盘持久化之前,rabbitMq服务器挂了

实现可靠发送,就是要做到,业务数据修改成功了,mq消息就一定要被发送rabbitMq服务器,并且不会丢失(持久化的队列和消息是基础,默认是持久化的

方案

方案概述

1、在提交本地事务之前,将要发送的mq消息保存到“发送任务表”中,状态为“发送中”。

2、事务提交之后,再调用RabbitTemplate.send方法发送mq消息。

3、利用rabbitMq的确认机制,添加消息发送回调监听:
成功到达exchange,删除步骤1中保存的mq消息;
到达exchange失败,发送失败次数加1,如果到达最大重试次数,状态改为“失败”,未到达最大重试次数则修改下次重试时间。

4、利用定时任务,定时查询状态为“发送中”的mq消息。重新调用RabbitTemplate.send方法发送mq消息。

消息发送失败后,就会一直重复步骤3和4,直到mq消息被成功发送到exchange或者到达最大重试次数。

流程图

rabbitMq事务消息方案_第1张图片

实现

业务数据和发送任务在同一个事务中落库

java基本都用spring,事务管理也基本都交给spring来管理。
在spring-tx包中,有这样一个接口org.springframework.transaction.suppor.TransactionSynchronization,代码如下

public interface TransactionSynchronization extends Flushable {
     
    int STATUS_COMMITTED = 0;
    int STATUS_ROLLED_BACK = 1;
    int STATUS_UNKNOWN = 2;
 
 	// 事务挂起触发
    default void suspend() {
     }
    // 事务挂起后恢复才触发
    default void resume() {
     }
    // 刷回话触发
    default void flush() {
     }
    // 事务提交前触发
    default void beforeCommit(boolean readOnly) {
     }
    // 事务提交/回滚前触发
    default void beforeCompletion() {
     }
    // 事务提交后触发
    default void afterCommit() {
     }
    // 事务不管是提交还是回滚,都会触发该方法
    default void afterCompletion(int status) {
     }
}

通过TransactionSynchronizationManager.registerSynchronization方法注册一个事务同步器,那么我们就能在事务提交之前、提交之后和事务回滚的时候做一些处理。
1、在beforeCommit方法,把“发送任务”保存到“发送任务表”,并把待发送的任务保存到线程变量中
2、在afterCommit方法,调用rabbitTemlate.send方法,将线程变量中的mq消息真正地去发出去
3、在afterCompletion方法判断事务是否回滚,如果回滚,清理线程变量中保存的mq消息

具体代码可以看reliable-mq项目的com.zidongxiangxi.reliablemq.producer.transaction.listener.DatabaseRabbitProducerTransactionListener类。

注册TransactionSynchronization之后,上面的伪代码改成这样:

public class DemoService() {
     
	@Autowired
	private RabbitMqProduceClient rabbitMqProduceClient;
	@Autowired
	private TransactionTemplate transactionTemplate;
	
	public Demo doSomething() {
     
		Object result = transactionTemplate.execute((transactionStatus) -> {
     
			// 1、在一个事务中完成数据库数据的变更
			Object tmp= demoManager.changeDataIntransaction();
			// 2、发送mq
			rabbitMqProduceClient.sendToExchange(exchange, msgBody);
			return tmp;
		}
		// 3、做一些数据转换,得到返回值
		return convertToDemo(result); 
	}
	......
}

即利用TransactionTemplate将业务数据变更和rabbitMqProduceClient.sendToExchange方法在同一个事物中执行(RabbitMqProduceClient是reliable-mq项目中的定义的mq发送接口)。

消息发送确认

rabbitMq本身提供了消息发送确认机制,在RabbitTemplate内部定义了一个接口ConfirmCallback,代码如下:

	/**
	 * A callback for publisher confirmations.
	 *
	 */
	@FunctionalInterface
	public interface ConfirmCallback {
     

		/**
		 * Confirmation callback.
		 * @param correlationData correlation data for the callback.
		 * @param ack true for ack, false for nack
		 * @param cause An optional cause, for nack, when available, otherwise null.
		 */
		void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String cause);
	}

correlationData是用于识别是哪个消息的确认,我们可以在correlationData中存消息的消息id
ack是确认结果,true表示发送成功,false则发送失败
cause,当ack为false,表示失败的原因

我们只需要实现该接口,在ack为true时,删除“发送任务表”中对应的发送任务,ack为false时,修改“发送任务表”中对应任务的状态即可。
具体代码可以看reliable-mq项目的com.zidongxiangxi.reliablemq.producer.callback.RabbitConfirmCallback类。

单单将RabbitConfirmCallback设置到RabbitTemplate,并不会生效,看RabbitTemplate的invokeAction和determineConfirmsReturnsCapability方法:

@Nullable
private <T> T invokeAction(ChannelCallback<T> action, ConnectionFactory connectionFactory, Channel channel) throws Exception {
      // NOSONAR see the callback
		if (this.confirmsOrReturnsCapable == null) {
     
			determineConfirmsReturnsCapability(connectionFactory);
		}
		if (this.confirmsOrReturnsCapable) {
     
			addListener(channel);
		}
		if (logger.isDebugEnabled()) {
     
			logger.debug(
					"Executing callback " + action.getClass().getSimpleName() + " on RabbitMQ Channel: " + channel);
		}
		return action.doInRabbit(channel);
	}

public void determineConfirmsReturnsCapability(ConnectionFactory connectionFactory) {
     
		this.publisherConfirms = connectionFactory.isPublisherConfirms();
		this.confirmsOrReturnsCapable =
				this.publisherConfirms || connectionFactory.isPublisherReturns();
	}

要confirmsOrReturnsCapable 为true,才会去注册监听,而只有当connectionFactory的isPublisherConfirms和isPublisherReturns有一个为true,confirmsOrReturnsCapable 才会为true

因此,我们还得设置org.springframework.amqp.rabbit.connection.ConnectionFactory的属性
1、如果是用starter,则直接添加配置spring.rabbitmq.publisher-confirms为true,如下:

spring:
	rabbitmq:
		publisher-confirms: true

用starter这种方式,有缺陷,就是只能监听一个virtual-host下的mq消息。如果要监听多个,就不行了。注意!!!

2、要监听多个virtual-host下的mq消息,就需要自己手动定义ConnectionFactory,因为是手动定义,所以可以直接设置publisherConfirms为true。

在reliable-mq中,只要开启了可靠发送功能,就会利用spring的BeanPostProcessor接口,对RabbitTemplate的所有bean设置confirmCallback属性,对ConnectionFactorty的所有bean设置publisherConfirms属性,确保发送确认机制生效。
具体代码可以看com.zidongxiangxi.reliablemq.starter.producer.rabbit.processor.RabbitConnectionFactoryBeanPostProcessor和com.zidongxiangxi.reliablemq.starter.producer.rabbit.processor.RabbitTemplateBeanPostProcessor类。

定时任务

需要引入定时任务,定时查询“待发送”状态的发送任务,重新发送mq消息。
分布式定时任务推荐使用xxl-job,maven依赖如下:

        <dependency>
            <groupId>com.xuxueli</groupId>
            <artifactId>xxl-job-core</artifactId>
            <version>${
     xxl.job.version}</version>
        </dependency>

具体代码可以看reliable-mq项目的com.zidongxiangxi.reliablemq.producer.scheduler.RabbitRetrySendJob类。

表结构

CREATE TABLE `rabbit_producer_record` (
  `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `type` TINYINT(3) NOT NULL DEFAULT 0 COMMENT '消息类型,0=即时消息,1=顺序消息',
  `application` VARCHAR(50) NOT NULL COMMENT '应用名称',
  `virtual_host` VARCHAR(255) NOT NULL COMMENT '虚拟主机',
  `exchange` VARCHAR(255) NULL COMMENT '交换器',
  `routing_key` VARCHAR(255) NULL COMMENT '路由key',
  `message_id` VARCHAR(50) NOT NULL COMMENT '消息id',
  `body` TEXT NOT NULL COMMENT '消息内容',
  `group_name` VARCHAR(50) DEFAULT NULL COMMENT '消息分组,同分组内,消息序号按发送顺序递增',
  `send_status` TINYINT(3) NOT NULL DEFAULT 0 COMMENT '发送状态, 0=预提交,1=发送中,2=发送是啊比',
  `retry_times` SMALLINT(6) NOT NULL DEFAULT 0 COMMENT '重试次数',
  `max_retry_times` SMALLINT(6) NOT NULL COMMENT '最大重试次数',
  `next_retry_time` DATETIME DEFAULT NULL COMMENT '下一次重试时间',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP  COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_message_app` (`message_id`, `application`)
) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4 COMMENT = 'rabbitMq发送表';

group_name字段可以先忽略,是用来实现顺序消息功能的,在讲解顺序消费的时候,会解释它的作用。

你可能感兴趣的:(rabbit,spring,boot,java,后端,rabbitmq)