当我们讨论分布式事务时,我们通常涉及到如何在一个分布式系统中保证事务的一致性。在传统的单体应用中,事务可以保证ACID(原子性、一致性、隔离性、持久性)属性,但在分布式环境中,由于网络延迟、节点故障等因素,实现这些属性变得更加复杂。CAP理论和BASE理论是在分布式系统设计中经常提到的两个概念,它们帮助我们理解在分布式系统中需要做出哪些权衡。
根据CAP理论,任何分布式系统只能同时满足以下三个中的两个:
定理:任何分布式系统只可同时满足二点,没法三者兼顾。
CA系统(放弃P):指将所有数据(或者仅仅是那些与事务相关的数据)都放在一个分布式节点上,就不会存在网络分区。所以强一致性以及可用性得到满足。
CP系统(放弃A):如果要求数据在各个服务器上是强一致的,然而网络分区会导致同步时间无限延长,那么如此一来可用性就得不到保障了。坚持事务ACID(原子性、一致性、隔离性和持久性)的传统数据库以及对结果一致性非常敏感的应用通常会做出这样的选择。
AP系统(放弃C):这里所说的放弃一致性,并不是完全放弃数据一致性,而是放弃数据的强一致性,而保留数据的最终一致性。如果即要求系统高可用又要求分区容错,那么就要放弃一致性了。因为一旦发生网络分区,节点之间将无法通信,为了满足高可用,每个节点只能用本地数据提供服务,这样就会导致数据不一致。一些遵守BASE原则数据库,(如:Cassandra、CouchDB等)往往会放宽对一致性的要求(满足最终一致性即可),一次来获取基本的可用性。
常见的系统类型包括CA系统、CP系统和AP系统。例如,对于金融交易系统来说,强一致性非常重要,因此可能会选择CP系统,牺牲一定的可用性来确保数据的一致性。而对于社交网络这类对实时性要求不是特别高的系统,则可能选择AP系统,容忍一定程度的数据不一致,以换取更高的可用性。
BASE理论是对CAP理论的一种补充,它强调的是在分布式系统中,可以通过牺牲一定程度的一致性来提高系统的可用性。BASE理论的核心包括:
BASE思想主要强调基本的可用性,如果你需要High 可用性,也就是纯粹的高性能,那么就要以一致性或容错性为牺牲。
下面简要介绍几种实现分布式事务的方法,并给出一些伪代码示例。
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模式是一个三阶段模式,它要求每个服务都有三个对应的方法:Try、Confirm和Cancel。
TCC 的全称是: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。
这里假设我们使用一个简单的消息队列来传递事务状态:
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) {
// 处理回滚消息
}
}
这种方案主要用于那些对最终结果不太敏感的情况,通过不断地重试来确保事务完成。
我们将消息发送到队列,并在另一个服务中处理:
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) {
// 重试
}
}