项目地址
可靠mq项目:reliable-mq
支持消息事务消息发送、幂等消费、顺序消费和可靠消费
前提
顺序消费,依赖消息id,使用reliable-mq的DatabaseRabbitMqProduceClient类发送mq消息,支持指定消息id,如果不指定,则会使用java的UUID自动生成一个。
DatabaseRabbitMqProduceClient类实现了RabbitMqProduceClient接口:
/**
* rabbitMq消息发送接口,支持发送顺序mq
*
* @author chenxudong
* @date 2019/09/17
*/
public interface RabbitMqProduceClient {
/**
* 发送消息到exchange
*
* @param exchange 交换器
* @param msgBody 消息体
* @return 消息id
*/
void sendToExchange(String exchange, Object msgBody);
/**
* 发送消息到queue
*
* @param queue 队列
* @param msgBody 消息体
* @return 消息id
*/
void sendToQueue(String queue, Object msgBody);
/**
* 发送消息
*
* @param exchange 交换器
* @param routingKey 路由key
* @param msgBody 消息体
* @return 消息id
*/
void send(String exchange, String routingKey, Object msgBody);
/**
* 发送消息
*
* @param messageId 消息id
* @param exchange 交换器
* @param routingKey 路由key
* @param msgBody 消息体
* @return 消息id
*/
void send(String messageId, String exchange, String routingKey, Object msgBody);
/**
* 发送消息到exchange
*
* @param groupName 消息分组,同个分组内的消息顺序消费
* @param exchange 交换器
* @param msgBody 消息体
* @return 消息id
*/
void sendToExchangeSequentially(String groupName, String exchange, Object msgBody);
/**
* 发送消息到queue
*
* @param groupName 消息分组,同个分组内的消息顺序消费
* @param queue 队列
* @param msgBody 消息体
* @return 消息id
*/
void sendToQueueSequentially(String groupName, String queue, Object msgBody);
/**
* 发送消息
*
* @param groupName 消息分组,同个分组内的消息顺序消费
* @param exchange 交换器
* @param routingKey 路由key
* @param msgBody 消息体
* @return 消息id
*/
void sendSequentially(String groupName, String exchange, String routingKey, Object msgBody);
/**
* 发送消息
*
* @param messageId 消息id
* @param groupName 消息分组,同个分组内的消息顺序消费
* @param exchange 交换器
* @param routingKey 路由key
* @param msgBody 消息体
* @return 消息id
*/
void sendSequentially(String messageId, String groupName, String exchange, String routingKey, Object msgBody);
}
sendXXXSequentially是发送顺序消息的方法,对比发送普通消息和顺序消息的方法会发现,对了一个分组参数。
顺序消费,只有在指定分组才有意义,例如同一张订单的创建消息和修改消息,才有顺序的必要。
问题
假设服务A是订单服务,订单服务会发送订单创建消息和变更消息。服务B需要接收服务A的消息,同步数据,接收到创建消息,就在本地创建订单;接收到变更消息,就修改本地的订单数据。
如果服务A在极短的时间内,发出创建和变更消息,而服务B部署了多台,可能就会是一台处理创建,一台处理更新。
这种情况下,处理更新的那个服务就会因为查询不到订单,导致更新操作执行不了。
方案
要实现顺序消息,需要依赖消费记录,必须知道自己消费到了哪里,才能判断接收到的mq能不能处理。关于消费记录的,可以看看《rabbitMq幂等消费方案》。
调用RabbitMqProduceClient.sendXXXSequentially发送mq消息的时候,查询该消息在同个分组中的上一个消息的消息id,并设置到mq消息头中(如果是分组内的第一个消息,查询结果为空,不设置消息头)。
消费者接收到mq消息时,尝试从消息头中获取“同分组内上一个消息的消息id”。
如果获取不到“同分组内上一个消息的消息id”,则当普通mq消息处理,结束顺序消息检查。
如果获取到“同分组内上一个消息的消息id”,则查询消费记录,看是否消费过该消息id的消息。消费过,则允许进入mq消息处理逻辑,并插入消费记录(插入消费记录,是幂等操作);未消费过,则休眠指定时间,抛出ImmediateRequeueAmqpException,让消息回到requeue,待下次消费。
流程图
实现
发送的时,如何获取组内的上一个消息id
在《rabbitMq事务消息方案》中增加了发送任务表rabbit_producer_record,如果发送成功后,不清理发送记录,是可以用该表来实现“获取组内的上一个消息id”的目的。但是出于数据量的考虑,发送成功后,还是删除更好。
额外再增加一张表producer_sequence_record来保存顺序消息记录: