可靠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或者到达最大重试次数。
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字段可以先忽略,是用来实现顺序消息功能的,在讲解顺序消费的时候,会解释它的作用。