目录
2:TCC方案(框架:ByteTCC,TCC-transaction,Himly)
1:2PC方案(Prepare预提交 Commit提交)
1.1:方案介绍
两阶段提交又称2PC(two-phase commit protocol),2pc是一个非常经典的强一致、中心化的原子提交协议。这里所说的中心化是指协议中有两类节点:一个是中心化协调者节点(coordinator)和N个参与者节点(partcipant)。
下面我们就以一个尽量贴近实际业务场景的操作来举例:"假设在一个分布式架构的系统中事务的发起者通过分布式事务协调者(如RocketMQ,在早期RocketMQ版本不提供事务消息特性时,有些公司会自己研发一个基于MQ的可靠消息服务来实现一定的分布式事务的特性)分别向应用服务A、应用服务B发起处理请求,二者在处理的过程中会分别操作自身服务的数据库,现在要求应用服务A、应用服务B的数据处理操作要在一个事务里"?
在上面这个例子中如果采用两阶段提交来实现分布式事务,那么其运行原理应该是个什么样的呢?(如👇):
第一阶段:请求/表决阶段(点击放大)
既然称为两阶段提交,说明在这个过程中是大致存在两个阶段的处理流程。第一个阶段如👆图所示,这个阶段被称之为请求/表决阶段。是个什么意思呢?
就是在分布式事务的发起方在向分布式事务协调者(Coordinator)发送请求时,Coordinator首先会分别向参与者(Partcipant)节点A、参与这节点(Partcipant)节点B分别发送事务预处理请求,称之为Prepare,有些资料也叫"Vote Request"。
说的直白点就是问一下这些参与节点"这件事你们能不能处理成功了",此时这些参与者节点一般来说就会打开本地数据库事务,然后开始执行数据库本地事务,但在执行完成后并不会立马提交数据库本地事务,而是先向Coordinator报告说:“我这边可以处理了/我这边不能处理”。
如果所有的参与这节点都向协调者作了“Vote Commit”的反馈的话,那么此时流程就会进入第二个阶段了。
第二阶段:提交/执行阶段(正常流程)
如果所有参与者节点都向协调者报告说“我这边可以处理”,那么此时协调者就会向所有参与者节点发送“全局提交确认通知(global_commit)”,即你们都可以进行本地事务提交了,此时参与者节点就会完成自身本地数据库事务的提交,并最终将提交结果回复“ack”消息给Coordinator,然后Coordinator就会向调用方返回分布式事务处理完成的结果。
第二阶段:提交/执行阶段(异常流程)
相反,在第二阶段除了所有的参与者节点都反馈“我这边可以处理了”的情况外,也会有节点反馈说“我这边不能处理”的情况发生,此时参与者节点就会向协调者节点反馈“Vote_Abort”的消息。此时分布式事务协调者节点就会向所有的参与者节点发起事务回滚的消息(“global_rollback”),此时各个参与者节点就会回滚本地事务,释放资源,并且向协调者节点发送“ack”确认消息,协调者节点就会向调用方返回分布式事务处理失败的结果
1.2:方案优缺点
此方案依赖全局的事务管理器,如果事务管理器出错,很容易导致系统卡死,或者网络问题导致A、B业务系统的需要回滚的时候,网络通信,长时间锁表,占用资源。
2:TCC方案(框架:ByteTCC,TCC-transaction,Himly)
2.1:方案介绍
终于有人把“TCC分布式事务”实现原理讲明白了! - JaJian - 博客园
说起分布式事务的概念,不少人都会搞混淆,似乎好像分布式事务就是TCC。实际上TCC与2PC、3PC一样,只是分布式事务的一种实现方案而已。
TCC(Try-Confirm-Cancel)又称补偿事务。其核心思想是:"针对每个操作都要注册一个与其对应的确认和补偿(撤销操作)"。它分为三个操作:
- Try阶段:主要是对业务系统做检测及资源预留。()
- Confirm阶段:确认执行业务操作。(真正的执行事务)
- Cancel阶段:取消执行业务操作。(回滚事务)
TCC事务的处理流程与2PC两阶段提交类似,不过2PC通常都是在跨库的DB层面,而TCC本质上就是一个应用层面的2PC,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。
2.2:方案优缺点
而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口还必须实现幂等。
3:RocketMq方案
3.1:方案介绍
1、A服务先发送个Half Message给Brock端,消息中携带 B服务 即将要+100元的信息。
2、当A服务知道Half Message发送成功后,那么开始第3步执行本地事务。
3、执行本地事务(会有三种情况1、执行成功。2、执行失败。3、网络等原因导致没有响应)
4.1)、如果本地事务成功,那么Product像Brock服务器发送Commit,这样B服务就可以消费该message。
4.2)、如果本地事务失败,那么Product像Brock服务器发送Rollback,那么就会直接删除上面这条半消息。
4.3)、如果因为网络等原因迟迟没有返回失败还是成功,那么会执行RocketMQ的回调接口,来进行事务的回查。
3.2:代码实现
@Component
public class MqProducer {
@Value("${mq.nameserver.addr}")
private String mqNameAddr = null;
@Value("${mq.topicname}")
private String topticName = null;
@Autowired
StockLogDoMapper stockLogDoMapper;
private DefaultMQProducer producer;
@Autowired
OrderServcce orderServcce;
//1:定义事务型生产者,数据库事务执行成功,才会发送mq,否则不发送消息
private TransactionMQProducer transactionMQProducer;
@PostConstruct
public void init() throws MQClientException {
//做mq的初始化方法
producer = new DefaultMQProducer("pay_group");
// 指定nameServer地址,多个地址之间以 ; 隔开
producer.setNamesrvAddr(mqNameAddr);
producer.start();
transactionMQProducer = new TransactionMQProducer("tran_pay_grop");
transactionMQProducer.setNamesrvAddr(mqNameAddr);
transactionMQProducer.start();
transactionMQProducer.setTransactionListener(new TransactionListener() {
//3:执行事务方法,没有异常,将消息的状态commit,否则ROLLBACK_MESSAGE
public LocalTransactionState executeLocalTransaction(Message message, Object args) {
//获取参数体
Integer userid = (Integer) ((Map) args).get("userid");
Integer itemid = (Integer) ((Map) args).get("itemid");
Integer promoId = (Integer) ((Map) args).get("promoId");
Integer amount = (Integer) ((Map) args).get("amount");
String stocklog_id = (String) ((Map) args).get("stocklogid");
try {
//如果事务成功,则提交消息,提供消费,否则撤回消息
//这里可能死机了没有返回状态,或者执行时间很长没有返回状态,
//此时会执行checkLocalTransaction方法定时重试
orderServcce.creatOrder(userid, itemid, promoId, amount, stocklog_id);
} catch (BusenessException e) {
e.printStackTrace();
return LocalTransactionState.ROLLBACK_MESSAGE;//mq消息一直是待确认状态此刻回滚
}
return LocalTransactionState.COMMIT_MESSAGE;
}
//4:事务检查方法 防止创建订单方法卡死,根据流水id查询,如果executeLocalTransaction方法返回值卡死的情况,此方法会定时调用查看消息状态
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
//获取消息体
String mbody = new String(messageExt.getBody());
Map<String, Integer> map = JSON.parseObject(mbody, Map.class);
Integer stocklogid = map.get("stocklogid");
Integer itemid = map.get("itemid");
Integer mount = map.get("amount");
//查询数据库数据,根据状态提交消息队列
StockLogDo stockLogDo = stockLogDoMapper.selectByPrimaryKey(String.valueOf(stocklogid));
if (stockLogDo == null) {
return LocalTransactionState.UNKNOW;
}else if (stockLogDo.getStatus().equals("2")) {
return LocalTransactionState.COMMIT_MESSAGE;
}else if (stockLogDo.getStatus().equals("1")){
return LocalTransactionState.UNKNOW;
}
return LocalTransactionState.ROLLBACK_MESSAGE;
}
});
}
//2:发送MQ消息:半开类型的消息,消费者不能消费到该消息
public boolean asyncTransactionDealStock(Integer userid, Integer itemid, Integer promoId, Integer amount, String stock_log_id) {
//制作消息体
Map<String, Object> hashMap = new HashMap();
hashMap.put("itemid", itemid);
hashMap.put("amount", amount);
hashMap.put("stocklogid", stock_log_id);
//制作参数体
Map<String, Object> argshashMap = new HashMap();
argshashMap.put("userid", userid);
argshashMap.put("itemid", itemid);
argshashMap.put("promoId", promoId);
argshashMap.put("amount", amount);
argshashMap.put("stocklogid", stock_log_id);
//信息设置topic的名字,消费者监听指定的topic
Message message = new Message();
message.setTopic(topticName);//消息主题
message.setTags("item_stock");//一个消息主题能有多个消息标签
message.setBody(JSON.toJSON(hashMap).toString().getBytes(Charset.forName("utf-8")));
TransactionSendResult sendResult = null;
try {
//此处发送的待消费消费,根据执行方法的状态确定消息
sendResult = transactionMQProducer.sendMessageInTransaction(message, argshashMap);
} catch (MQClientException e) {
e.printStackTrace();
return false;
}
if (sendResult.getLocalTransactionState()
.equals(LocalTransactionState.ROLLBACK_MESSAGE)) {
return false;
} else if (sendResult.getLocalTransactionState()
.equals(LocalTransactionState.COMMIT_MESSAGE)) {
return true;
} else {
return false;
}
}
}