问题:
解决A系统事务操作完成后要向B系统发送消息,B系统会有相应的处理。
因为A系统事务有多个操作,那么就涉及什么时候发送给B系统。
如果在事务前发给B系统,则可能A系统事务最终执行失败了,但最终B已经消费成功。
如果在事务都做完后发送给B系统,则可能最终消息丢失。
解决方案:
1.发给B系统的消息与A系统事务是一个落库
然后在事务结束后发送消息给B系统,如果成功则将事务记录状态更新为:成功。
这时如果发送消息给B系统失败,事务记录中的状态还是待处理,所以需要有一个job去做循环。
缺:多了事务表,并且需要额外一个job去做处理。
2.利用metaq事务消息
整体对于producer来讲在事务前发一个事务消息(此时事务消息状态为待处理),然后做业务事务。事务成功或失败发送相应的事务确认或取消消息给到broker。
整体原理图:
交易业务示例图:
使用示例:
发送半事务消息代码:
package com.alibaba.webx.TryHsf.app1;
import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.PropertyKeyConst;
import com.aliyun.openservices.ons.api.SendResult;
import com.aliyun.openservices.ons.api.transaction.LocalTransactionExecuter;
import com.aliyun.openservices.ons.api.transaction.TransactionProducer;
import com.aliyun.openservices.ons.api.transaction.TransactionStatus;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
public class TransactionProducerClient {
private final static Logger log = ClientLogger.getLog(); // 您需要设置自己的日志,便于排查问题。
public static void main(String[] args) throws InterruptedException {
final BusinessService businessService = new BusinessService(); // 本地业务。
Properties properties = new Properties();
// 您在消息队列RocketMQ版控制台创建的Group ID。注意:事务消息的Group ID不能与其他类型消息的Group ID共用。
properties.put(PropertyKeyConst.GROUP_ID,"XXX");
// AccessKey ID阿里云身份验证,在阿里云RAM控制台创建。
properties.put(PropertyKeyConst.AccessKey,"XXX");
// AccessKey Secret阿里云身份验证,在阿里云RAM控制台创建。
properties.put(PropertyKeyConst.SecretKey,"XXX");
// 设置TCP接入域名,进入消息队列RocketMQ版控制台实例详情页面的接入点区域查看。
properties.put(PropertyKeyConst.NAMESRV_ADDR,"XXX");
TransactionProducer producer = ONSFactory.createTransactionProducer(properties,
new LocalTransactionCheckerImpl());
producer.start();
Message msg = new Message("Topic","TagA","Hello MQ transaction===".getBytes());
try {
SendResult sendResult = producer.send(msg, new LocalTransactionExecuter() {
@Override
public TransactionStatus execute(Message msg, Object arg) {
// 消息ID(有可能消息体一样,但消息ID不一样,当前消息属于半事务消息,所以消息ID在消息队列RocketMQ版控制台无法查询)。
String msgId = msg.getMsgID();
// 消息体内容进行crc32,也可以使用其它的如MD5。
long crc32Id = HashUtil.crc32Code(msg.getBody());
// 消息ID和crc32id主要是用来防止消息重复。
// 如果业务本身是幂等的,可以忽略,否则需要利用msgId或crc32Id来做幂等。
// 如果要求消息绝对不重复,推荐做法是对消息体使用crc32或MD5来防止重复消息。
Object businessServiceArgs = new Object();
TransactionStatus transactionStatus = TransactionStatus.Unknow;
try {
boolean isCommit =
businessService.execbusinessService(businessServiceArgs);
if (isCommit) {
// 本地事务已成功则提交消息。
transactionStatus = TransactionStatus.CommitTransaction;
} else {
// 本地事务已失败则回滚消息。
transactionStatus = TransactionStatus.RollbackTransaction;
}
} catch (Exception e) {
log.error("Message Id:{}", msgId, e);
}
System.out.println(msg.getMsgID());
log.warn("Message Id:{}transactionStatus:{}", msgId, transactionStatus.name());
return transactionStatus;
}
}, null);
}
catch (Exception e) {
// 消息发送失败,需要进行重试处理,可重新发送这条消息或持久化这条数据进行补偿处理。
System.out.println(new Date() + " Send mq message failed. Topic is:" + msg.getTopic());
e.printStackTrace();
}
// demo example防止进程退出(实际使用不需要这样)。
TimeUnit.MILLISECONDS.sleep(Integer.MAX_VALUE);
}
}
提供broker回调检查事务状态接口:
public class LocalTransactionCheckerImpl implements LocalTransactionChecker {
private final static Logger log = ClientLogger.getLog();
final BusinessService businessService = new BusinessService();
@Override
public TransactionStatus check(Message msg) {
//消息ID(有可能消息体一样,但消息ID不一样,当前消息属于半事务消息,所以消息ID在消息队列RocketMQ版控制台无法查询)。
String msgId = msg.getMsgID();
//消息体内容进行crc32,也可以使用其它的方法如MD5。
long crc32Id = HashUtil.crc32Code(msg.getBody());
//消息ID和crc32Id主要是用来防止消息重复。
//如果业务本身是幂等的,可以忽略,否则需要利用msgId或crc32Id来做幂等。
//如果要求消息绝对不重复,推荐做法是对消息体使用crc32或MD5来防止重复消息。
//业务自己的参数对象,这里只是一个示例,需要您根据实际情况来处理。
Object businessServiceArgs = new Object();
TransactionStatus transactionStatus = TransactionStatus.Unknow;
try {
boolean isCommit = businessService.checkbusinessService(businessServiceArgs);
if (isCommit) {
//本地事务已成功则提交消息。
transactionStatus = TransactionStatus.CommitTransaction;
} else {
//本地事务已失败则回滚消息。
transactionStatus = TransactionStatus.RollbackTransaction;
}
} catch (Exception e) {
log.error("Message Id:{}", msgId, e);
}
log.warn("Message Id:{}transactionStatus:{}", msgId, transactionStatus.name());
return transactionStatus;
}
}