一、概述
最近几年,都提倡将复杂的应用系统,按照业务或者功能拆分成多个职能单一的微服务(只处理自己的业务或者功能),各个服务之间通过PRC或者Restful通信,而且微服务开发效率高、部署简单、代码好维护、稳定性高、扩展性强、松耦合、可以使用不同语言开发等优点,但是也存在测试难度提升、管理复杂、调用复杂、分布式事务等劣势。
对于事务,之前的单体应用,不需要考虑跨模块回滚事务的问题,但是在分布式系统中,我们需要保证事务最终一致。
现有分布式事务的主要实现方式:
XA 方案
TCC 方案
可靠消息最终一致性方案
最大努力通知方案
本文只讨论通过RocketMQ事务消息来实现分布式事务的最终一致。
具体方案:A系统在执行本地事务之前,先发送一条prepare消息,然后RocketMQ broker轮询这条prepare消息是否可以提交提交(或者回归,或者继续等待),当A系统本地事务执行成功之后,broker将提交这条消息;B系统监听到这条消息之后,开始执行本地事务。
潜在的问题:
1、发送prepare消息失败:那么A系统本地事务就不需要执行,直接丢弃这条消息。
2、B系统在收到消息之后,重试多次之后依旧失败:只能人工干预,回滚数据,没有完美的设计方案。
3、prepare消息每隔多长时间轮询一次,prepare消息是否可以提交、回归、继续等待:默认每隔1分钟轮询一次,可以在broker.config配置轮询时间。
4、最大轮询次数:默认15次,超过上限之后,丢弃消息。
二、具体实现
在上一篇文章的基础之上(RocketMq顺序消息解决方案),来写这次的demo
1、OrderConstant
package com.cn.dl.springbootdemo.constant;
public interface OrderConstant {
String ORDER_TOPIC = "order_msg";
String ORDER_CONSUMER_GROUP = "order_msg_consumer";
String REPAY_TRANSACTION_GROUP = "repay_transaction_group";
String REPAY_MSG_TOPIC = "repay_msg_topic";
String REPAY_MSG_GROUP = "repay_msg_group";
String RANDOM_INT = "random_int";
}
2、RepayTransactionProducer
package com.cn.dl.springbootdemo.mq;
import com.cn.dl.springbootdemo.bean.Repay;
import com.cn.dl.springbootdemo.constant.OrderConstant;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* Created by yanshao on 2020-11-05.
*/
@Component
public class RepayTransactionProducer {
private static final Logger logger = LoggerFactory.getLogger(RepayTransactionProducer.class);
@Resource
private RocketMQTemplate rocketMQTemplate;
public void sendRepayTransactionMsg(Repay repay){
Message<? extends Repay> message = MessageBuilder.withPayload(repay)
.setHeader(RocketMQHeaders.TRANSACTION_ID,repay.getRepayId())
.build();
TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(
OrderConstant.REPAY_TRANSACTION_GROUP,OrderConstant.REPAY_MSG_TOPIC,message,new String[]{"test","demo"});
logger.info("send repay transaction msg. repay: {},sendResult: {}",repay,sendResult);
}
}
3、RepayTransactionListener
package com.cn.dl.springbootdemo.mq;
import com.cn.dl.springbootdemo.constant.OrderConstant;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
/**
* Created by yanshao on 2020-11-05.
*/
@Component
@RocketMQTransactionListener(txProducerGroup = OrderConstant.REPAY_TRANSACTION_GROUP)
public class RepayTransactionListener implements RocketMQLocalTransactionListener {
private static final Logger logger = LoggerFactory.getLogger(RepayTransactionListener.class);
/**
发送prepare消息成功后会这样这个方法,只执行一次
@param message 消息头、消息体
@param args 接受额外的参数
@return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object args) {
logger.info("execute local transaction transactionId: {},args: {}"
,message.getHeaders().get(RocketMQHeaders.TRANSACTION_ID),args);
return RocketMQLocalTransactionState.UNKNOWN;
}
/**
* 判断本地事务的执行状态,如果本地执行成功,返回COMMIT,如果本地事务还在进行中或者状态未知,
* 返回UNKNOW,如果本地事务执行失败,返回ROLLBACK
*
* 默认执行15次,超过上限则丢弃消息
* @param message
* @return
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
logger.info("check local transaction transactionId: {}",message.getHeaders().get(RocketMQHeaders.TRANSACTION_ID));
return RocketMQLocalTransactionState.COMMIT;
}
}
4、RepayMsgConsumer
package com.cn.dl.springbootdemo.mq;
import com.alibaba.fastjson.JSONObject;
import com.cn.dl.springbootdemo.bean.Repay;
import com.cn.dl.springbootdemo.constant.OrderConstant;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
/**
* Created by yanshao on 2020-11-05.
*/
@Component
@RocketMQMessageListener(
topic = OrderConstant.REPAY_MSG_TOPIC,
consumerGroup = OrderConstant.REPAY_MSG_GROUP
)
public class RepayMsgConsumer implements RocketMQListener<MessageExt> {
private static final Logger logger = LoggerFactory.getLogger(RepayTransactionListener.class);
@Override
public void onMessage(MessageExt messageExt) {
String msg = null;
try {
msg = new String(messageExt.getBody(), StandardCharsets.UTF_8.name());
Repay repay = JSONObject.parseObject(msg, Repay.class);
logger.info("repay msg consumer. repay: {}",repay);
}catch (Exception e){
logger.error("repay msg consumer exception. msgId: {}",messageExt.getMsgId());
throw new RuntimeException("repay msg consumer exception!");
}
}
}
5、Repay
package com.cn.dl.springbootdemo.bean;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* Created by yanshao on 2020-11-05.
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Repay implements Serializable {
private static final long serialVersionUID = -5915842247156810870L;
/**
* 还款唯一ID
*/
private String repayId;
/**
* 用户ID
*/
private String customerId;
/**
* 还款金额
*/
private BigDecimal repayAmount;
/**
* 备注
*/
private String remark;
}
6、测试入口
@Resource
private RepayTransactionProducer repayTransactionProducer;
@Test
public void sendRepayTransaction() throws InterruptedException {
for(int i=0; i<1; i++){
Repay repay = Repay.builder()
.customerId("customerId" + i)
.repayId("repayId" + i)
.repayAmount(new BigDecimal(i * 1000))
.build();
repayTransactionProducer.sendRepayTransactionMsg(repay);
}
TimeUnit.SECONDS.sleep(30000);
}
7、执行结果
8、测试丢弃和继续等待两种状态,修改RepayTransactionProducer、RepayTransactionListener
public void sendRepayTransactionMsg(Repay repay){
Message<? extends Repay> message = MessageBuilder.withPayload(repay)
.setHeader(RocketMQHeaders.TRANSACTION_ID,repay.getRepayId())
//为了测试,这里增加随机树,默认本地是否执行失败等情况
.setHeader(OrderConstant.RANDOM_INT,new Random().nextInt(10))
.build();
TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(
OrderConstant.REPAY_TRANSACTION_GROUP,
OrderConstant.REPAY_MSG_TOPIC,
message,
new String[]{"test","demo"});
logger.info("send repay transaction msg. repay: {},sendResult: {}",repay,sendResult);
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
int randomInt = Integer.parseInt(String.valueOf(message.getHeaders().get(OrderConstant.RANDOM_INT)));
if(randomInt < 3){
logger.info("unknown transaction transactionId: {}",message.getHeaders().get(RocketMQHeaders.TRANSACTION_ID));
return RocketMQLocalTransactionState.UNKNOWN;
} else if(randomInt % 2 == 0){//如果随机数大于2 && 为偶数,丢弃消息
logger.info("rollback transaction transactionId: {}",message.getHeaders().get(RocketMQHeaders.TRANSACTION_ID));
return RocketMQLocalTransactionState.ROLLBACK;
} else {
logger.info("commit transaction transactionId: {}",message.getHeaders().get(RocketMQHeaders.TRANSACTION_ID));
return RocketMQLocalTransactionState.COMMIT;
}
}