事务消息是RocketMQ区别于其他消息队列的一个很明显的特征。
概念介绍
- 事务消息:消息队列RocketMQ提供了类似X/Open XA的分布式事务功能,通过事务消息达到分布式事务的最终一致
- 半事务消息:暂不能投递的消息,发送方已经成功地将消息发送到了RocketMQ服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记为"暂不能投递"状态,处在该状态的消息即称为半事务消息。处在该状态的消息是不能被消费者发现并消费的
- 消息回查:由于网络、生产者应有重启等原因,导致某条事务消息的二次确认丢失,RocketMQ服务端通过扫描发现某条消息长期处于"半事务消息"状态,需要主动向生产者询问该消息的最终状态(commit或者rollback)。
流程
事务消息发送步骤:
- 发送方将半事务消息发送至服务端
- 服务端将消息持久化之后,想发送方返回ACK确认消息已经发送成功,此时消息仍是半事务消息
- 发送方执行本地事务逻辑
- 发送方根据本地事务执行结果向服务端提交二次确认,服务端收到commit状态则将半事务消息标记为可投递,消费者最终可以收到该消息;服务端收到rollback消息则删除半事务消息,消费者不会接收该消息
实践
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.5.2</version>
</dependency>
使用事务消息时,必须要实现TransactionListener,这可以实现消息回查功能
依然以前面用户下单成功扣库存为例 展示事务消息的实现
@Service
public class TransactionProducer {
//事务消息生产者
private TransactionMQProducer transactionMQProducer;
private ExecutorService executorService;
@Autowired
private TransactionListenerImpl transactionListener;
@PostConstruct
public void init() throws MQClientException {
transactionMQProducer = new TransactionMQProducer();
transactionMQProducer.setProducerGroup(MqConfig.RPODUCER_GROUP);
transactionMQProducer.setNamesrvAddr(MqConfig.NAME_SRV_ADDR);
executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("client-transaction-msg-check-thread");
return thread;
}
});
transactionMQProducer.setExecutorService(executorService);
transactionMQProducer.setTransactionListener(transactionListener);
transactionMQProducer.start();
}
//1、发送事务消息
public boolean sendMessage(Message message) throws MQClientException {
TransactionSendResult transactionSendResult = transactionMQProducer.sendMessageInTransaction(message, null);
System.out.println("message id," + transactionSendResult.getMsgId());
//2、服务端响应事务消息发送是否成功
return true;
}
处理事务回查、事务二次确认的类
@Service
public class TransactionListenerImpl implements TransactionListener {
@Autowired
private OrderService orderService;
private ConcurrentHashMap<String, Boolean> localTrans = new ConcurrentHashMap<>(16);
@Override
@Transactional
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
//3、执行本地事务
orderService.createOrder();
//message的key对于一条消息而言是唯一的
//这里标记创建订单的执行结果
localTrans.put(msg.getKeys(), true);
} catch (Exception e) {
//出现异常 标记创建订单的执行结果
localTrans.put(msg.getKeys(), false);
throw e;
}
//如果返回的状态是Commit/rollback,则不会再消息回查 直接会投递消息或丢弃消息
//这里返回UNKNOW 希望消息回查 本地事务的执行情况
//4、响应本地事务执行情况
return LocalTransactionState.UNKNOW;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
//5&6、消息回查,最终给出本地事务执行情况,commit或rollback
Boolean b = localTrans.get(msg.getKeys());
return b ? LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.ROLLBACK_MESSAGE;
}
}
消费者:
@Service
public class TransactionConsumer {
private DefaultMQPushConsumer defaultMQPushConsumer;
//这个里面是实际消费消息的地方
@Autowired
private MessageListener messageListener;
@Value("${topic:topic4})
private String topic;
@Value("${tag:tagA})
private String tag;
@PostConstruct
public void init() throws MQClientException {
defaultMQPushConsumer = new DefaultMQPushConsumer();
defaultMQPushConsumer.setConsumerGroup(MqConfig.CONSUMER_GROUP);
defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
defaultMQPushConsumer.setNamesrvAddr(MqConfig.NAME_SRV_ADDR);
defaultMQPushConsumer.subscribe(topic, tag);
defaultMQPushConsumer.registerMessageListener(messageListener);
defaultMQPushConsumer.start();
}
MessageListener
@Component
public class MessageListener implements MessageListenerConcurrently {
@Autowired
private WarehouseService warehouseService
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
//处理扣减库存逻辑
String body=new String(msg.getBodys());
warehouseService.reduceProductQuantity();
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}
如果在消息消费的过程中抛出错误,消费者会触发RECONSUME_LATER状态,会重试。所以如果消费者业务有问题,及时更正之后,消息队列重试时依然会得到正常的执行处理,从而保证数据的一致性
MqConfig只是常量的类,涉及的消费者、生产者组 可根据实际情况自己来,至于NameServer地址则需要依赖自己搭建的RocketMQ来定,这里是localhost:9876。所以未给出配置
这里在SpringBoot中启动生产者
@SpringBootApplication
public class WebDemoApplication {
public static void main(String[] args) throws MQClientException, UnsupportedEncodingException {
ConfigurableApplicationContext run = SpringApplication.run(WebDemoApplication.class, args);
TransactionProducer transactionProducer = run.getBean(TransactionProducer.class);
Message message = new Message("topic4", "tagA", "wojiushowo", "test".getBytes(RemotingHelper.DEFAULT_CHARSET));
transactionProducer.sendMessage(message);
}
}
至此一个事务消息的实践即完成。
事务消息的三种状态
- ROLLBACK_MESSAGE 回滚事务
- COMMIT_MESSAGE 提交事务
- UNKNOW 服务端会定时回查生产者消息状态,直到彻底成功或失败
当executeLocalTransaction
方法返回ROLLBACK_MESSAGE时,表示直接回滚事务,当返回COMMIT_MESSAGE,表示提交事务
当返回UNKNOW时,服务端会在一段时间之后回查checkLocalTransaction
,根据checkLocalTransaction
返回状态执行事务的操作(回滚或提交)