rocketMq事务消息理解(结合springboot)

rocketmq事务消息

一. 问题由来

首先要弄懂一个概念:

在事务中,一个方法执行结束(最后一行执行完了),此时事务不一定执行成功,直到所有的事务提交mysql,mysql事务执行成功了,才算事务执行成功

因此,在业务中,尝尝会遇到如下情况:

  1. 本地业务事务执行成功了,消息发送失败了

  2. 本地事务执行失败了,但是消息却发送成功了

这两种情况都会造成不同系统之间消息的不一致性,比如订单系统下单成功了,库存系统却没有减库存。或者下单失败了,库存却减了,这种都是不符合业务需求的

二. 事务消息

事务消息就是为了解决在分布式系统下事务的一致性。

rocketmq的事务消息解决了本地事务消息发送的原子性,即本地事务成功,消息一定发送成功,本地事务失败,消息一定不被消费。

注意:rocketMq事务消息并不能保证消费的成功,消费的成功性由消费重试相应的功能去实现。

三. 实现原理

3.1 基本思想

rocketMq借助prepare消息这个概念,实现了事务消息的功能。

  1. prepare消息(half消息),是一种预发送的消息,这种类型的消息是放在专门的topic中,不会被消费者所消费。通过本地事务执行的结果来决定是否将该消息转移到真实topic中

  2. 事务状态回查机制。当broker收不到生产者的事务状态或者受到UNKNOW类型的状态时,每隔1min会回查生产者,查看本地事务状态,根据返回值,决定COMMIT还是ROLLBACK还是继续等待下次回查。

默认会回查5次,可以配置

rocketMq通过保证prepare消息一定会发送成功(因为是和本地事务一起的),保证了消息内容先预存在broker上指定topic,然后再去根据本地事务的执行状态,决定是把这条预发送的消息转移到真实topic供消费者消费,还是说直接不处理丢弃该消息

3.2 基本流程

  1. 生产者开启本地事务,执行相应业务逻辑

  2. 发送prepare事务消息,此消息一定会成功(失败则本地事务也会直接异常失败)

  3. 执行本地事务监听器,根据返回状态决定prepare消息如何处理

  4. 如果消息状态为UNKNOWN或者未收到消息状态,开启事务回查

3.3 源码剖析

主要看事务消息发送的逻辑,broker端的处理不作分析

核心方法为:sendMessageInTransaction

  1. 发送prepare消息
// ignore DelayTimeLevel parameter
//不支持延迟消息
if (msg.getDelayTimeLevel() != 0) {
   MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL);
}    

这里可以看出来事务消息不支持延迟

MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
        

放入prepare消息专有属性,其中TRAN_MSG是标记消息属于事务消息,PGROUP则放入该消息所属的生产者组,方便事务回查

try {
//发送消息,失败抛出异常,保证了prepare消息一定可以发送成功(杜绝网络,硬件等异常情况)
   sendResult = this.send(msg);
} catch (Exception e) {
   throw new MQClientException("send message Exception", e);
}

这一步确保了prepare消息一定可以发送成功,失败则抛出异常,会使本地方法也失败

  1. 调用listener的executeLocalTransaction方法
else if (transactionListener != null) {
   log.debug("Used new transaction API");
//执行本地事务,获取本地事务状态,至此,完成了一次事务消息的逻辑
   localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
}

这个方法是本地实现的,个人感觉没有多大的用处,因为执行这个方法的时候,并不能判断本地事务是否执行成功,只能返回UNKNOWN,而且这个方法会try catch,这就导致了在这里插入本地事务标记也不合适,因为插入标记失败了,本地事务并不能回滚,不是原子性的

因此,直接返回UNKNOWN

  1. broker端事务回查

这里源码就不分析了,主要就是回调producer的checkLocalTransaction

四. 代码实战

基于springboot实现

<!-- rocketmq -->
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

4.1 消息模型

@Data
@Accessors(chain = true)
public class MessageModel implements Serializable {

    private static final long serialVersionUID = -7376674233827653417L;

    private String messageId;

    public MessageModel() {
        this.messageId = IdGenerator.uuid2();
    }

    public String getHashKey() {
        return null;
    }


}

这里设计了一个消息模型,自定义的业务消息,每个消息都继承该类,构造一个业务上唯一的消息id

4.2 消息标记表

由于事务回查时会通过某个标记去判断本地事务是否执行成功,因此我这里设计一张消息表,用来在本地事务执行的同时插入一条记录,如果该条记录成功了,那么也就能证明本地事务执行成功了

CREATE TABLE `mq_msg` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `tag` varchar(50) NOT NULL COMMENT '消息tag',
  `message_id` varchar(50) NOT NULL COMMENT '消息ID',
  `message` text NOT NULL COMMENT '消息JSON',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_message_id` (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

通过messageId的唯一性,来判断所属事务是否成功

4.2 事务监听类

@RocketMQTransactionListener
@Component
@Slf4j
public class MyTransactionListener implements RocketMQLocalTransactionListener {

    @Autowired
    private MqMsgMapper mqMsgMapper;

    @Autowired
    private RocketMQMessageConverter rocketMQMessageConverter;

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
        //不能在这里面执行标记插入,因为这里的异常都会被拦截,标记是否插入判断不出来
        return RocketMQLocalTransactionState.UNKNOWN;
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        MessageModel msg =
                (MessageModel) rocketMQMessageConverter.getMessageConverter().fromMessage(message, MessageModel.class);
        log.info("开始执行本地事务:messageId = {}", msg.getMessageId());
        MqMsg mqMsg = mqMsgMapper.selectByMessageId(msg.getMessageId());
        if (mqMsg != null) {
            log.info("本地事务执行成功: messageId = {}", msg.getMessageId());
            return RocketMQLocalTransactionState.COMMIT;
        }
        return RocketMQLocalTransactionState.UNKNOWN;
    }
}

executeLocalTransaction做不了任何判断,因为本身异常都是被拦截的,并且和业务事务在同一个事务内,对数据库的判断无从得知

而且执行到这里的时候,本地事务还没开始提交(这个方法执行和本地事务是同一个大方法内的),根本无从判断本地事务是否成功

check方法主要就是检查业务中插入的消息记录是否存在,如果存在说明事务成功了,如果不存在,就只能返回UNDNKOWN,因为不确定是否成功还是失败

4.3 事务生产者

@Service
public class TransactionProducer extends AbstractProducer implements MqMsgProducer {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Autowired
    private MqMsgService mqMsgService;

    public TransactionProducer(RocketMQTemplate rocketMQTemplate) {
        super(rocketMQTemplate, "transaction", "TAG_A");
    }


    @Override
    public void sendTracMessage(MessageModel message, Object arg) {
        mqMsgService.save(message.getMessageId(), Constants.MQ_MSG_TAH_TRANSACTION, JsonUtils.objectToJson(message));
        sendTransactionMessage(message, arg);
    }
}

实现了发送事务消息的方法,内部还是调用了rocketMqTemplate的方法

protected void sendTransactionMessage(MessageModel message, Object arg) {
   this.rocketMQTemplate.sendMessageInTransaction(destination(topic, tag), new GenericMessage<>(message), arg);
}

这里主要就是把我自定义的MessageModel转成rocketMq需要的类型org.springframework.messaging.Message

4.4 消费者

@Slf4j
@Service
@RocketMQMessageListener(topic = "transaction",
        selectorExpression = "TAG_A",
        consumerGroup = "traction_consumer")
public class TestConsumer extends AbstractConsumer<ProductMessage> {

    public TestConsumer(RocketMQMessageConverter rocketMQMessageConverter) {
        super(rocketMQMessageConverter, ProductMessage.class);
    }

    @Override
    protected void processMessage(ProductMessage message) {
        System.out.println(message);
        log.info("消费成功");
    }
}

普通的消费者测试,我这里是自定义包装了一层AbstractConsumer,直接用springboot mq提供的也可以

4.5 测试类

@Override
    @SneakyThrows
    @Transactional
    public void testTransaction() {
        Product product = new Product();
        product.setName("事务测试");
        product.setDetail("事务测试详情");
        product.setPrice(123);
        product.setSubTitle("事务副标题");
        productMapper.insert(product);
        ProductMessage message = BeanUtils.map(product, ProductMessage.class);
        message.setId(product.getId());
        transactionProducer.sendTracMessage(message, null);
    }

先往数据库插入一条数据,然后发送事务消息

4.6 结果

2020-12-31 15:35:01.209 -- [pool-1-thread-1] INFO  c.m.springbootpractice.mq.MyTransactionListener - 开始执行本地事务
2020-12-31 15:35:01.483 -- [pool-1-thread-1] INFO  c.m.springbootpractice.mq.MyTransactionListener - 本地事务执行成功
ProductMessage(id=32, name=事务测试, price=123, subTitle=事务副标题, detail=事务测试详情)
2020-12-31 15:35:01.501 -- [ConsumeMessageThread_1] INFO  c.m.springbootpractice.mq.consumer.TestConsumer - 消费成功

可以看到消费并不是立马执行的,而是等到broker回调check之后,才进行消费的,只有验证本地事务成功了,消息才会被消费者消费。

五. 总结

就探究的结果而言,感觉rocketMq事务消息在executeLocalTransaction这个方法的定位上有点不明所以。感觉可有可无,反而增加理解难度

同时,这样的设计,导致了消费不能立马执行,因为check方法是1min执行一次的,这样就会使消费有延迟。即使本地事务执行成功了,因为无法在事务内部知道结果,因此至少要回调一次check方法,才能知道结果,才能被消费。

不过,起码基于这样的机制,能够实现本地事务和消息发送的原子性。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在Spring Boot中使用RocketMQ保证消息顺序的代码示例: 1. 消息生产者 ```java import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.common.message.Message; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class OrderProducer { @Autowired private DefaultMQProducer defaultMQProducer; public void sendMsg(String msg, String tag, String key) throws Exception { Message message = new Message("OrderTopic", tag, key, msg.getBytes()); defaultMQProducer.send(message, (mqs, msg1, arg) -> { Integer id = (Integer) arg; int index = id % mqs.size(); return mqs.get(index); }, 1); } } ``` 在消息生产者中,我们使用DefaultMQProducer发送消息,通过设置MessageQueueSelector接口实现类,将消息发送到同一个Message Queue中。在MessageQueueSelector接口实现类中,我们可以根据指定的key值,将同一批次的消息发送到同一个Message Queue中。 2. 消息消费者 ```java import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderlyContext; import org.apache.rocketmq.common.message.MessageExt; import org.springframework.stereotype.Component; import java.util.List; @Component public class OrderConsumer implements MessageListenerOrderly { @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { for (MessageExt messageExt : msgs) { String msg = new String(messageExt.getBody()); System.out.println("消费者接收到消息:" + msg); } return ConsumeOrderlyStatus.SUCCESS; } } ``` 在消息消费者中,我们实现了MessageListenerOrderly接口,保证同一个消费者实例中的消息按照顺序进行消费。同时,我们将consumeOrderly属性设置为true,保证同一个消费者实例中的消息按照顺序进行消费。 在使用RocketMQ保证消息顺序时,需要注意消息的路由策略和消费者实例的数量。如果需要保证消息的顺序,可以使用同一批次消息的key相同的方式,将消息路由到同一个Message Queue中,然后由同一个消费者消费这些消息。如果需要多个消费者同时处理同一个主题的消息,可以使用MessageListenerOrderly接口作为消息监听器,保证同一个消费者按照顺序消费同一个Message Queue中的消息

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值