rocketmq事务消息
一. 问题由来
首先要弄懂一个概念:
在事务中,一个方法执行结束(最后一行执行完了),此时事务不一定执行成功,直到所有的事务提交mysql,mysql事务执行成功了,才算事务执行成功
因此,在业务中,尝尝会遇到如下情况:
-
本地业务事务执行成功了,消息发送失败了
-
本地事务执行失败了,但是消息却发送成功了
这两种情况都会造成不同系统之间消息的不一致性,比如订单系统下单成功了,库存系统却没有减库存。或者下单失败了,库存却减了,这种都是不符合业务需求的
二. 事务消息
事务消息就是为了解决在分布式系统下事务的一致性。
rocketmq的事务消息解决了本地事务和消息发送的原子性,即本地事务成功,消息一定发送成功,本地事务失败,消息一定不被消费。
注意:rocketMq事务消息并不能保证消费的成功,消费的成功性由消费重试相应的功能去实现。
三. 实现原理
3.1 基本思想
rocketMq借助prepare消息这个概念,实现了事务消息的功能。
-
prepare消息(half消息),是一种预发送的消息,这种类型的消息是放在专门的topic中,不会被消费者所消费。通过本地事务执行的结果来决定是否将该消息转移到真实topic中
-
事务状态回查机制。当broker收不到生产者的事务状态或者受到UNKNOW类型的状态时,每隔1min会回查生产者,查看本地事务状态,根据返回值,决定COMMIT还是ROLLBACK还是继续等待下次回查。
默认会回查5次,可以配置
rocketMq通过保证prepare消息一定会发送成功(因为是和本地事务一起的),保证了消息内容先预存在broker上指定topic,然后再去根据本地事务的执行状态,决定是把这条预发送的消息转移到真实topic供消费者消费,还是说直接不处理丢弃该消息
3.2 基本流程
-
生产者开启本地事务,执行相应业务逻辑
-
发送prepare事务消息,此消息一定会成功(失败则本地事务也会直接异常失败)
-
执行本地事务监听器,根据返回状态决定prepare消息如何处理
-
如果消息状态为UNKNOWN或者未收到消息状态,开启事务回查
3.3 源码剖析
主要看事务消息发送的逻辑,broker端的处理不作分析
核心方法为:sendMessageInTransaction
- 发送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消息一定可以发送成功,失败则抛出异常,会使本地方法也失败
- 调用listener的
executeLocalTransaction
方法
else if (transactionListener != null) {
log.debug("Used new transaction API");
//执行本地事务,获取本地事务状态,至此,完成了一次事务消息的逻辑
localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
}
这个方法是本地实现的,个人感觉没有多大的用处,因为执行这个方法的时候,并不能判断本地事务是否执行成功,只能返回UNKNOWN,而且这个方法会try catch,这就导致了在这里插入本地事务标记也不合适,因为插入标记失败了,本地事务并不能回滚,不是原子性的
因此,直接返回UNKNOWN
- 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方法,才能知道结果,才能被消费。
不过,起码基于这样的机制,能够实现本地事务和消息发送的原子性。