如何解决订单数据库同步的消息乱序问题?

895 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

前言

数据组利用canal监听mysql的binlog日志,然后发送到RocketMQ后,通过消费消息来实现业务库数据同步的功能。 实际使用会发现RocketMQ的消息顺序和binlog的顺序不一致,导致数据不正确。原因是属于同一个订单的binlog进入了不同的MessageQueue,进而导致一个订单的binlog被不同机器上的Consumer来获取和处理,那么如果是这样的话,必然会导致这一个订单的binlog会乱序执行。

解决办法

最简单的方式是只用一个MessageQueue,这样自然可以保证所有消息都是有序的。但是会有隐患,要确保消息顺序就必须要等前一个消息ack消费成功之后才能消费后一条消息。就会导致消息挤压的问题。
分析业务场景,其实没有必要要保证所有消息全局有序,只需要保证同一个订单的消息是有序的就可以了。

让属于同一个订单的binlog进入一个MessageQueue。

在send中,有一个参数是shardingKey,将orderId作为shardingKey。select方法会根据shardingKey的hashCode来选择MessageQueue,从而实现将同一个订单的消息进入一个MessageQueue。
消费端需保证同一个订单的消息的消费顺序,即在前一个消息ack消费成功了才能消费后一条消息。具体源码如下:

public SendResult send(final Message message, final String shardingKey) {
    if (UtilAll.isBlank(shardingKey)) {
        throw new ONSClientException("'shardingKey' is blank.");
    }
    message.setShardingKey(shardingKey);
    this.checkONSProducerServiceState(this.defaultMQProducer.getDefaultMQProducerImpl());
    final com.aliyun.openservices.shade.com.alibaba.rocketmq.common.message.Message msgRMQ = ONSUtil.msgConvert(message);
    try {
        com.aliyun.openservices.shade.com.alibaba.rocketmq.client.producer.SendResult sendResultRMQ =
            this.defaultMQProducer.send(msgRMQ, new com.aliyun.openservices.shade.com.alibaba.rocketmq.client.producer.MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, com.aliyun.openservices.shade.com.alibaba.rocketmq.common.message.Message msg,
                    Object shardingKey) {
                    int select = Math.abs(shardingKey.hashCode());
                    if (select < 0) {
                        select = 0;
                    }
                    return mqs.get(select % mqs.size());
                }
            }, shardingKey);
        message.setMsgID(sendResultRMQ.getMsgId());
        SendResult sendResult = new SendResult();
        sendResult.setTopic(message.getTopic());
        sendResult.setMessageId(sendResultRMQ.getMsgId());
        return sendResult;
    } catch (Exception e) {
        throw new ONSClientException("defaultMQProducer send order exception", e);
    }
}