序
最近项目使用开源版本的rocketmq
发现了一些特殊场景,需要自己动手改造一下
- 延时消息 无法随机延时之后消费
- 事务消息 无法在事务完成之后发送消息
因为网上有较多rocketmq的延时消息实现,所以本文主要介绍事务消息相关改造
消息的困扰
项目经常会发生,消息发送之后,事务没有执行完毕,消费者在第一次消费的时候出现无法找到发送消息事务里面新增
、修改
的数据
官方事务消息
发现官方的事务消息与目前项目所需要的“事务消息”有所不同
- 官方的事务消息是发送消息之后立马执行
executeLocalTransaction
,然后获得消息的state
- 如果
executeLocalTransaction
返回的是COMMIT
,则会立马投递消息,此时事务并没执行有完毕,还是会出现上面的问题。 - 而如果在
executeLocalTransaction
中返回UNKNOW
,则意味着消息需要等回查checkLocalTransaction
执行完毕后才能消费,而这个过程默认是需要10秒,简单一点:发送消息后需要等10秒才能消费....
备注
官方的事务消息虽然不是我们想要的,但是有一套完整的事务消息流程
- 保证了已经发送的消息不会丢失
- 保证了发送的消息必须被COMMIT才能被投递和消费
当发生异常情况(网络问题、系统崩溃、进程被kill等等),后期broker都会进行回查
由此催生了改造的想法
改造原理
把COMMIT操作推后到事务结束的时候才提交
而如果事务失败了,则会走checkLocalTransaction
改造TransactionMQProducer
创建一个TransactionMQProducer
继承官方的TransactionMQProducer
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.Validators;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageAccessor;
import org.apache.rocketmq.common.message.MessageConst;
import org.apache.rocketmq.common.protocol.NamespaceUtil;
import org.apache.rocketmq.remoting.RPCHook;
import org.apache.rocketmq.remoting.exception.RemotingException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Lazy;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
public class TransactionMQProducer extends org.apache.rocketmq.client.producer.TransactionMQProducer {
@Lazy
@Resource
private ApplicationContext applicationContext;
public TransactionMQProducer() {
}
public TransactionMQProducer(String producerGroup) {
super(producerGroup);
}
public TransactionMQProducer(String namespace, String producerGroup) {
super(namespace, producerGroup);
}
public TransactionMQProducer(String producerGroup, RPCHook rpcHook) {
super(producerGroup, rpcHook);
}
public TransactionMQProducer(String namespace, String producerGroup, RPCHook rpcHook) {
super(namespace, producerGroup, rpcHook);
}
/**
* 发送事务消息
*
* @param msg 消息
* @param arg 参数
* @return 结果
* @throws MQClientException
*/
public TransactionSendResult sendMessageInTransaction(final Message msg, final Object arg) throws MQClientException {
msg.setTopic(NamespaceUtil.wrapNamespace(this.getNamespace(), msg.getTopic()));
TransactionListener transactionListener = this.defaultMQProducerImpl.getCheckListener();
if (null == transactionListener) {
throw new MQClientException("tranExecutor is null", null);
}
// ignore DelayTimeLevel parameter
if (msg.getDelayTimeLevel() != 0) {
MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL);
}
Validators.checkMessage(msg, this);
SendResult sendResult;
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.getProducerGroup());
try {
sendResult = this.send(msg);
} catch (Exception e) {
throw new MQClientException("send message Exception", e);
}
LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
switch (sendResult.getSendStatus()) {
case SEND_OK:
try {
if (sendResult.getTransactionId() != null) {
msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
}
String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
if (null != transactionId && !"".equals(transactionId)) {
msg.setTransactionId(transactionId);
}
log.debug("Used new transaction API");
localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
if (null == localTransactionState) {
localTransactionState = LocalTransactionState.UNKNOW;
}
} catch (Throwable e) {
log.info("executeLocalTransactionBranch exception", e);
log.info(msg.toString());
throw e;
}
// 把发送成功的消息添加到待处理列表
applicationContext.publishEvent(new TxnMessageEvent(this, sendResult));
break;
case FLUSH_DISK_TIMEOUT:
case FLUSH_SLAVE_TIMEOUT:
case SLAVE_NOT_AVAILABLE:
// 只有在消息发送失败,才会立马rollback
localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
try {
this.defaultMQProducerImpl.endTransaction(sendResult, localTransactionState, null);
} catch (Exception e) {
log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
}
break;
default:
break;
}
TransactionSendResult transactionSendResult = new TransactionSendResult();
transactionSendResult.setSendStatus(sendResult.getSendStatus());
transactionSendResult.setMessageQueue(sendResult.getMessageQueue());
transactionSendResult.setMsgId(sendResult.getMsgId());
transactionSendResult.setQueueOffset(sendResult.getQueueOffset());
transactionSendResult.setTransactionId(sendResult.getTransactionId());
transactionSendResult.setLocalTransactionState(localTransactionState);
return transactionSendResult;
}
/**
* 发送确认消息
*
* @param sendResult 消息结果
* @param localTransactionState 状态
* @param localException 异常
* @throws InterruptedException
* @throws UnknownHostException
* @throws RemotingException
* @throws MQBrokerException
*/
public void endTransaction(
final SendResult sendResult,
final LocalTransactionState localTransactionState,
final Throwable localException) throws InterruptedException, UnknownHostException, RemotingException, MQBrokerException {
this.defaultMQProducerImpl.endTransaction(sendResult, localTransactionState, localException);
}
/**
* 当事务成功的时候-发送COMMIT消息
*
* @param txnMessageEvent 事件
* @throws InterruptedException
* @throws RemotingException
* @throws MQBrokerException
* @throws UnknownHostException
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void afterCommit(TxnMessageEvent txnMessageEvent) throws InterruptedException, RemotingException, MQBrokerException, UnknownHostException {
SendResult sendResult = txnMessageEvent.getSendResult();
LocalTransactionState transactionState = LocalTransactionState.COMMIT_MESSAGE;
endTransaction(sendResult, transactionState, null);
log.info("Send Transaction {} {}", transactionState, sendResult.getMsgId());
}
/**
* 当事务失败的时候-发送ROLLBACK消息
*
* @param txnMessageEvent 事件
* @throws InterruptedException
* @throws RemotingException
* @throws MQBrokerException
* @throws UnknownHostException
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void afterRollback(TxnMessageEvent txnMessageEvent) throws InterruptedException, RemotingException, MQBrokerException, UnknownHostException {
SendResult sendResult = txnMessageEvent.getSendResult();
LocalTransactionState transactionState = LocalTransactionState.ROLLBACK_MESSAGE;
endTransaction(sendResult, transactionState, new RuntimeException("事务执行失败"));
log.info("Send Transaction {} {}", transactionState, sendResult.getMsgId());
}
TxnMessageEvent
源码
import lombok.EqualsAndHashCode;
import org.apache.rocketmq.client.producer.SendResult;
import org.springframework.context.ApplicationEvent;
@EqualsAndHashCode(callSuper = true)
public class TxnMessageEvent extends ApplicationEvent {
private SendResult sendResult;
/**
* Create a new {@code ApplicationEvent}.
*
* @param source the object on which the event initially occurred or with
* which the event is associated (never {@code null})
*/
public TxnMessageEvent(Object source, SendResult sendResult) {
super(source);
this.sendResult = sendResult;
}
public SendResult getSendResult() {
return sendResult;
}
}
TransactionListener 定义
@Component
public class ExtTransactionListener implements TransactionListener {
private ConcurrentHashMap<String, Integer> countHashMap = new ConcurrentHashMap<>();
private static int MAX_COUNT = 5;
@Resource
private MessageTransactionManager messageTransactionManager;
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
String transactionId = msg.getTransactionId();
MessageTransaction transaction = messageTransactionManager.get(transactionId);
if (transaction != null) {
return LocalTransactionState.UNKNOW;
}
transaction = new MessageTransaction();
transaction.setId(transactionId);
messageTransactionManager.add(transaction);
return LocalTransactionState.COMMIT_MESSAGE;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
String id = msg.getTransactionId();
MessageTransaction transaction = messageTransactionManager.get(id);
if (transaction != null) {
countHashMap.remove(id);
return LocalTransactionState.COMMIT_MESSAGE;
}
Integer num = countHashMap.get(id);
if (num == null) {
num = 1;
}
if (++num > MAX_COUNT) {
countHashMap.remove(id);
return LocalTransactionState.ROLLBACK_MESSAGE;
}
countHashMap.put(id, num);
return LocalTransactionState.UNKNOW;
}
}
注意messageTransactionManager
这个需要改为自己的消息事务存储的Service或者Dao层即可
简单理解
发送消息的时候同时跟随事务保存一条消息事务记录,ID为事务ID(默认为消息的ID,唯一,可自行优化为一个事务只有一个事务ID)
作用
- 如果事务正常提交了,则此消息事务记录就没有用
- 如果事务正常提交了,但是mq没有收到COMMIT消息(网络故障等),后面就会执行
checkLocalTransaction
,如果根据里面逻辑判断,如果消息能够在库里面查询到,则意味着事务成功提交了,此时返回COMMIT,进而投递消息 - 如果事务失败,MQ没有收到ROLLBACK消息,就会执行
checkLocalTransaction
,根据尝试最多5次,如果都查询不到,就返ROLLBACK消息,表示消息回滚了
定义MqProderBean
@Data
@Configuration
@ConfigurationProperties(prefix = "rocketmq.producer")
public class MqProducerConfig {
private final Logger logger = LoggerFactory.getLogger(getClass());
private String groupName;
private String namesrvAddr;
private String instanceName;
private int maxMessageSize; // 4M
private int sendMsgTimeout;
@Resource
private TransactionListener transactionListener;
private ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("client-transaction-msg-check-thread");
return thread;
}
});
@Bean
public TransactionMQProducer transactionMQProducer() {
if (StringUtils.isBlank(this.groupName)) {
throw new BaseException("groupName is blank");
}
if (StringUtils.isBlank(this.namesrvAddr)) {
throw new BaseException("nameServerAddr is blank");
}
if (StringUtils.isBlank(this.instanceName)) {
throw new BaseException("instanceName is blank");
}
TransactionMQProducer producer = new TransactionMQProducer(this.groupName);
producer.setNamesrvAddr(this.namesrvAddr);
producer.setInstanceName(String.format("%s#%d", instanceName, UtilAll.getPid()));
producer.setMaxMessageSize(this.maxMessageSize);
producer.setSendMsgTimeout(this.sendMsgTimeout);
producer.setTransactionListener(transactionListener);
producer.setExecutorService(executorService);
try {
producer.start();
logger.info(String.format("producer is start ! groupName:[%s],namesrvAddr:[%s]", this.groupName,
this.namesrvAddr));
} catch (MQClientException e) {
logger.error(String.format("producer is error %s", e.getMessage()), e);
throw new BaseException(e);
}
return producer;
}
}
注意导入的TransactionMQProducer
为自定义的Producer
对应的配置文件
rocketmq.producer.groupName=xxx
rocketmq.producer.namesrvAddr=x.x.x.x:9876
rocketmq.producer.instanceName=xxx
rocketmq.producer.maxMessageSize=131072
rocketmq.producer.sendMsgTimeout=10000
发送事务消息
@Resource
private TransactionMQProducer mqProducer;
mqProducer.sendMessageInTransaction(message, null);
其中null
是传递给TransactionListener
的参数,可选
终
本文事务消息(事务结束后发送消息)场景的解决方案,是建立在rocketmq本身事务消息为基础而实现的
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。