写在前面:
什么是分布式事务?
单数据源的一致性依靠单机事务来保证。多数据源的一致性就要靠分布式事务实现。分布式事务用于在分布式系统中保证不同节点之间的数据一致性。分布式事务的实现有很多种,最具有代表性的是由Oracle Tuxedo系统提出的XA分布式协议。XA协议包含两阶段提交(2PC)和三阶段提交(3PC)两种实现。
一,分布式四种解决方案
1,两阶段提交(2PC)
两阶段提交2PC是分布式事务中最强大的事务类型之一,两段提交就是分两个阶段提交,第一阶段询问各个事务数据源是否准备好,第二阶段才真正将数据提交给事务数据源。
为了保证该事务可以满足ACID,就要引入一个协调者(Cooradinator)。其他的节点被称为参与者(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。处理流程如下:
①,准备阶段
a) 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复。
b) 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
c) 如参与者执行成功,给协调者反馈 yes,否则反馈 no。
②,提交阶段
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息。两种情况处理如下:
两阶段提交问题:
- 性能问题:XA协议遵循强一致性。在事务执行过程中,各个节点占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知提交,参与者提交后释放资源。这样的过程有着非常明显的性能问题。
- 可靠性问题:如果协调者存在单点故障问题,或出现故障,提供者将一直处于锁定状态。
- 数据一致性问题:在XA协议的第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。
优点:尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)。
缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
2,三阶段提交(3PC)
三阶段提交是在二阶段提交上的改进版本,3PC最关键要解决的就是协调者和参与者同时挂掉的问题,所以3PC把2PC的准备阶段再次一分为二,这样三阶段提交。处理流程如下:
①,第一阶段 (canCommit)
a) 协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。
b) 参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。
②,第二阶段(preCommit)
协调者根据参与者响应情况,有以下两种可能。
情况1:所有参与者均反馈 yes,协调者预执行事务
a) 协调者向所有参与者发出 preCommit 请求,进入准备阶段。
b) 参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
c) 各参与者向协调者反馈 ack 响应或 no 响应,并等待最终指令。
情况2:只要有一个参与者反馈 no,或者等待超时后协调者尚无法收到所有提供者的反馈,即中断事务
a) 协调者向所有参与者发出 abort 请求。
b) 无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。
③,第三阶段(doCommit)
该阶段进行真正的事务提交,也可以分为以下两种情况。
情况 1:所有参与者均反馈 ack 响应,执行真正的事务提交
a) 如果协调者处于工作状态,则向所有参与者发出 do Commit 请求。
b) 参与者收到 do Commit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况2:只要有一个参与者反馈 no,或者等待超时后协调组尚无法收到所有提供者的反馈,即回滚事务。
a) 如果协调者处于工作状态,向所有参与者发出 rollback 请求。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调组反馈 ack 完成的消息。
d) 协调组收到所有参与者反馈的 ack 消息后,即完成事务回滚。
优点:相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题。阶段 3 中协调者出现问题时,参与者会继续提交事务。
缺点:数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commite 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
3,补偿事务(TCC)
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
-
Try 阶段主要是对业务系统做检测及资源预留
-
Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
-
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
举个例子,假入 Bob 要向 Smith 转账,思路大概是: 我们有一个本地方法,里面依次调用
- 首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来。
- 在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
- 如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些
缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。
4,本地消息表(异步保存)
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。
- 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
- 之后将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
- 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
5,MQ消息事务(最终一致性)
支持事务消息的MQ,其支持事务消息的方式采用类似于二阶段提交。
基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。
条件:
a) 需要补偿逻辑
b) 业务处理逻辑需要幂等
处理流程:
c) 消费者向MQ发送half消息。
d) MQ Server将消息持久化后,向发送方ack确认消息发送成功。
e) 消费者开始执行事务逻辑。
f) 消费者根据本地事务执行结果向MQ Server提交二次确认或者回滚。
g) MQ Server收到commit状态则将half消息标记可投递状态。
h) 服务提供者收到该消息,执行本地业务逻辑。返回处理结果。
优点:
消息数据独立存储,降低业务系统与消息系统之间的耦合。
吞吐量优于本地消息表方案。
缺点:
一次消息发送需要两次网络请求(half消息 + commit/rollback)。
需要实现消息回查接口。
二,RocketMQ事务消息
在上一篇文章《RocketMQ安装与简单使用》中,介绍了rocketmq的集中生产/消费模式。RocketMQ支持事务消息的方式也是类似于采用的二阶段提交。
我们以微服务架构的购物场景为例,参照一下RocketMQ官方的例子,用户A发起订单,支付100块钱操作完成后,能得到100积分,账户服务和会员服务是两个独立的微服务模块,有各自的数据库,按照上文提及的问题可能性,将会出现这些情况:
- 如果先扣款,再发消息,可能钱刚扣完,宕机了,消息没发出去,结果积分没增加。
- 如果先发消息,再扣款,可能积分增加了,但钱没扣掉,白送了100积分给人家。
- 钱正常扣了,消息也发送成功了,但会员服务实例消费消息出现问题,结果积分没增加。
由此引出的是数据库事务与MQ消息的事务一致性问题,rocketmq事务消息解决的问题:解决本地事务执行与消息发送的原子性问题。这里界限一定要明白,是确保MQ生产端正确无误地将消息发送出来,没有多发,也不会漏发。但至于发送后消费端有没有正常的消费掉(如上面提及的第三种情况,钱正常扣了,消息也发了,但下游消费出问题导致积分不对),这种异常场景将由MQ消息消费失败重试机制来保证,不在此次的讨论范围内。
常用的MQ组件针对此场景都有自己的实现方案,如ActiveMQ使用AMQP协议(二阶提交方式)保证消息正确发送,这里我们以RocketMQ为重点进行学习。
2.1,RocketMQ事务消息设计思路
根据CAP理论,RocketMQ事务消息通过异步确保方式,保证事务的最终一致性。设计流程上借鉴两阶段提交理论,流程图如下:
- 应用模块遇到要发送事务消息的场景时,先发送prepare消息给MQ。
- prepare消息发送成功后,应用模块执行数据库事务(本地事务)。
- 根据数据库事务执行的结果,再返回Commit或Rollback给MQ。
- 如果是Commit,MQ把消息下发给Consumer端,如果是Rollback,直接删掉prepare消息。
- 第3步的执行结果如果没响应,或是超时的,启动定时任务回查事务状态(最多重试15次,超过了默认丢弃此消息),处理结果同第4步。
- MQ消费的成功机制由MQ自己保证。
2.2,RocketMQ事务消息实现流程
以RocketMQ 4.5.2版本为例,事务消息有专门的一个队列RMQ_SYS_TRANS_HALF_TOPIC,所有的prepare消息都先往这里放,当消息收到Commit请求后,就把消息再塞到真实的Topic队列里,供Consumer消费,同时向RMQ_SYS_TRANS_OP_HALF_TOPIC塞一条消息。简易流程图如下:
应用模块的事务因为中断,或是其他的网络原因,导致无法立即响应的,RocketMQ当做UNKNOW处理,RocketMQ事务消息还提供了一个补救方案:定时查询事务消息的数据库事务状态
简易流程图如下:
2.3,RocketMQ事务消息源码剖析
在第一阶段提交prepare消息后,需要对本地事务做具体的操作。比如执行本地事务或者do nothing。所以我们需要创建一个监听类,执行我们的本地事务与回查操作。
public class TransactionListenerImpl implements TransactionListener{
private AtomicInteger transactionIndex = new AtomicInteger(0);
private AtomicInteger checkTimes = new AtomicInteger(0);
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
/**
* 执行本地事务
* @param message
* @param o
* @return
*/
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
String msgKey = message.getKeys();
System.out.println("start execute local transaction " + msgKey);
LocalTransactionState state;
if (msgKey.contains("1")) {
// 第一条消息让他通过
state = LocalTransactionState.COMMIT_MESSAGE;
} else if (msgKey.contains("2")) {
// 第二条消息模拟异常,明确回复回滚操作
state = LocalTransactionState.ROLLBACK_MESSAGE;
} else {
// 第三条消息无响应,让它调用回查事务方法
state = LocalTransactionState.UNKNOW;
// 给剩下3条消息,放1,2,3三种状态
localTrans.put(msgKey, transactionIndex.incrementAndGet());
}
System.out.println("executeLocalTransaction:" + message.getKeys() + ",execute state:" + state + ",current time:" + System.currentTimeMillis());
return state;
}
/**
* 回查本地事务结果
* @param messageExt
* @return
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
String msgKey = messageExt.getKeys();
System.out.println("start check local transaction " + msgKey);
Integer state = localTrans.get(msgKey);
switch (state) {
case 1:
System.out.println("check result unknown 回查次数" + checkTimes.incrementAndGet());
return LocalTransactionState.UNKNOW;
case 2:
System.out.println("check result commit message, 回查次数" + checkTimes.incrementAndGet());
return LocalTransactionState.COMMIT_MESSAGE;
case 3:
System.out.println("check result rollback message, 回查次数" + checkTimes.incrementAndGet());
return LocalTransactionState.ROLLBACK_MESSAGE;
default:
return LocalTransactionState.COMMIT_MESSAGE;
}
}
}
事务消息生产者代码示例,共发送5条消息,基本上包含全部的场景,休眠时间设置足够的时间,保证回查事务时实例还在运行中,代码如下:
package com.equaker.demo.test.mq;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.*;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class Mq_Producer_Test {
public static void main(String[] args) throws Exception {
//Async_Producer();
//Sync_Producer();
//OneWay_Producer();
//Order_Producer();
//Schedule_Producer();
//Batch_Producer();
//Filter_Producer();
Transaction_Producer();
}
public static void Transaction_Producer() throws Exception {
TransactionListener transactionListener = new TransactionListenerImpl();
TransactionMQProducer producer = new TransactionMQProducer("producer1");
ExecutorService 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;
}
});
producer.setExecutorService(executorService);
producer.setTransactionListener(transactionListener);
producer.start();
String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 5; i++) {
try {
Message msg =
new Message("TopicTest", tags[i % tags.length], "KEY" + i,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
System.out.printf("%s%n", sendResult);
Thread.sleep(10);
} catch (MQClientException | UnsupportedEncodingException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 100000; i++) {
Thread.sleep(1000);
}
producer.shutdown();
}
}
我们把关注点放在producer.sendMessageInTransaction(msg, null);这一行。进行源码追踪。
DefaultMQProducerImpl.java:
public TransactionSendResult sendMessageInTransaction(final Message msg,
final LocalTransactionExecuter localTransactionExecuter, final Object arg)
throws MQClientException {
TransactionListener transactionListener = getCheckListener();
if (null == localTransactionExecuter && null == transactionListener) {
throw new MQClientException("tranExecutor is null", null);
}
Validators.checkMessage(msg, this.defaultMQProducer);
SendResult sendResult = null;
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
try {
//发送prepare消息
sendResult = this.send(msg);
} catch (Exception e) {
throw new MQClientException("send message Exception", e);
}
LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
Throwable localException = null;
//根据第一阶段提交prepare返回信息,确定mq的接下来动作
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);
}
if (null != localTransactionExecuter) {
localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
} else if (transactionListener != null) {
//如果执行成功,由于我们上面创建了TransactionListener监听类,这里回执行本地事务
log.debug("Used new transaction API");
localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
}
if (null == localTransactionState) {
localTransactionState = LocalTransactionState.UNKNOW;
}
if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {
log.info("executeLocalTransactionBranch return {}", localTransactionState);
log.info(msg.toString());
}
} catch (Throwable e) {
log.info("executeLocalTransactionBranch exception", e);
log.info(msg.toString());
localException = e;
}
}
break;
case FLUSH_DISK_TIMEOUT:
case FLUSH_SLAVE_TIMEOUT:
case SLAVE_NOT_AVAILABLE:
localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
break;
default:
break;
}
try {
//根据第一阶段的prepare返回结果,执行后续操作结束事务
this.endTransaction(sendResult, localTransactionState, localException);
} catch (Exception e) {
log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
}
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;
}