当我们讨论分布式事务时,我们通常涉及到如何在一个分布式系统中保证事务的一致性。在传统的单体应用中,事务可以保证ACID(原子性、一致性、隔离性、持久性)属性,但在分布式环境中,由于网络延迟、节点故障等因素,实现这些属性变得更加复杂。CAP理论和BASE理论是在分布式系统设计中经常提到的两个概念,它们帮助我们理解在分布式系统中需要做出哪些权衡。
CAP理论
根据CAP理论,任何分布式系统只能同时满足以下三个中的两个:
- 一致性(Consistency):所有的读操作都会得到最新的写操作的结果。
- 可用性(Availability):每个请求无论成功与否都需要在合理的时间内得到回应。
- 分区容错性(Partition tolerance):即使有消息丢失的情况下,系统仍然能够正常运行。
定理:任何分布式系统只可同时满足二点,没法三者兼顾。
-
CA系统(放弃P):指将所有数据(或者仅仅是那些与事务相关的数据)都放在一个分布式节点上,就不会存在网络分区。所以强一致性以及可用性得到满足。
-
CP系统(放弃A):如果要求数据在各个服务器上是强一致的,然而网络分区会导致同步时间无限延长,那么如此一来可用性就得不到保障了。坚持事务ACID(原子性、一致性、隔离性和持久性)的传统数据库以及对结果一致性非常敏感的应用通常会做出这样的选择。
-
AP系统(放弃C):这里所说的放弃一致性,并不是完全放弃数据一致性,而是放弃数据的强一致性,而保留数据的最终一致性。如果即要求系统高可用又要求分区容错,那么就要放弃一致性了。因为一旦发生网络分区,节点之间将无法通信,为了满足高可用,每个节点只能用本地数据提供服务,这样就会导致数据不一致。一些遵守BASE原则数据库,(如:Cassandra、CouchDB等)往往会放宽对一致性的要求(满足最终一致性即可),一次来获取基本的可用性。
常见的系统类型包括CA系统、CP系统和AP系统。例如,对于金融交易系统来说,强一致性非常重要,因此可能会选择CP系统,牺牲一定的可用性来确保数据的一致性。而对于社交网络这类对实时性要求不是特别高的系统,则可能选择AP系统,容忍一定程度的数据不一致,以换取更高的可用性。
BASE理论
BASE理论是对CAP理论的一种补充,它强调的是在分布式系统中,可以通过牺牲一定程度的一致性来提高系统的可用性。BASE理论的核心包括:
- 基本可用(Basically Available):系统能够在大多数情况下提供服务,即使在非正常情况下也能提供有限的服务。
- 软状态(Soft State):允许系统中的数据在一段时间内保持不一致状态,但最终达到一致。
- 最终一致性(Eventual Consistency):数据将在一定时间内达到一致的状态。
BASE思想主要强调基本的可用性,如果你需要High 可用性,也就是纯粹的高性能,那么就要以一致性或容错性为牺牲。
分布式事务实现方案
下面简要介绍几种实现分布式事务的方法,并给出一些伪代码示例。
2PC/XA协议
2PC是一种经典的分布式事务处理协议,它分为两个阶段:准备阶段(Prepare Phase)和提交阶段(Commit Phase)。
XA 方案,即:两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复 ok,那么就正式提交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不 ok,那么就回滚事务。
这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。
一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的。如果你要操作别人的服务的库,你必须是通过调用别的服务的接口来实现,绝对不允许交叉访问别人的数据库。
import java.util.List;
public class TwoPhaseCommit {
public boolean commit(List<Participant> participants) {
// 准备阶段
for (Participant participant : participants) {
if (!participant.prepare()) {
rollback(participants);
return false;
}
}
// 提交阶段
for (Participant participant : participants) {
participant.commit();
}
return true;
}
private void rollback(List<Participant> participants) {
for (Participant participant : participants) {
participant.rollback();
}
}
}
interface Participant {
boolean prepare();
void commit();
void rollback();
}
TCC模式
TCC模式是一个三阶段模式,它要求每个服务都有三个对应的方法:Try、Confirm和Cancel。
TCC 的全称是:Try
、Confirm
、Cancel
。
- Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行 锁定或者预留。
- Confirm 阶段:这个阶段说的是在各个服务中执行实际的操作。
- Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要 进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)
假设我们有一个TCC服务,我们可以创建一个简单的接口来表示TCC行为:
public interface TccService {
boolean tryAction(String resourceId);
void confirmAction(String resourceId);
void cancelAction(String resourceId);
}
然后可以创建一个具体的实现:
public class BankAccountService implements TccService {
@Override
public boolean tryAction(String resourceId) {
// 尝试锁定资源
return true; // 返回是否成功
}
@Override
public void confirmAction(String resourceId) {
// 确认操作
}
@Override
public void cancelAction(String resourceId) {
// 取消操作
}
}
基于消息队列的最终一致性
这种方案依赖于消息队列来保证最终一致性,例如使用RocketMQ。
- A 系统先发送一个 prepared 消息到 MQ,如果这个 prepared 消息发送失败那么就直接取消操作别执行了;
- 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉 MQ 发送确认消息,如果失败就告诉 MQ 回滚消息;
- 如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务;
- mq 会自动定时轮询所有 prepared 消息回调你的接口。问你,这个消息是不是本地事务处理失败了,所有没发送确认的消息,是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,而确认消息却发送失败了。
- 这个方案里,要是系统 B 的事务失败了咋办?重试,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。
这里假设我们使用一个简单的消息队列来传递事务状态:
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
@Service
public class MessageProducerService {
private final RabbitTemplate rabbitTemplate;
public MessageProducerService(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
public void sendPreparedMessage(String message) {
rabbitTemplate.convertAndSend("transaction-prepared-exchange", "prepared", message);
}
public void sendConfirmationMessage(String message) {
rabbitTemplate.convertAndSend("transaction-confirm-exchange", "confirm", message);
}
public void sendRollbackMessage(String message) {
rabbitTemplate.convertAndSend("transaction-rollback-exchange", "rollback", message);
}
}
然后在消费者端:
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class MessageConsumerService {
@RabbitListener(queues = "transaction-queue")
public void onConfirmationMessage(String message) {
// 处理确认消息
}
@RabbitListener(queues = "transaction-queue")
public void onRollbackMessage(String message) {
// 处理回滚消息
}
}
最大努力通知
这种方案主要用于那些对最终结果不太敏感的情况,通过不断地重试来确保事务完成。
- 系统 A 本地事务执行完之后,发送个消息到 MQ;
- 这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口;
- 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。
我们将消息发送到队列,并在另一个服务中处理:
public class MaxEffortNotificationService {
private final MessageProducerService producerService;
public MaxEffortNotificationService(MessageProducerService producerService) {
this.producerService = producerService;
}
public void processTransaction() {
// 本地事务成功
producerService.sendConfirmationMessage("success");
}
public void retryOnFailure(String message) {
// 重试
}
}