好,各位朋友,今天咱们来聊聊C++分布式事务这块的硬骨头。别怕,咱争取用大白话把它啃下来。重点是两阶段提交(2PC)和三阶段提交(3PC),这俩哥们是分布式事务里的扛把子。
啥是分布式事务?
简单来说,就是你的业务数据分散在不同的数据库或者服务上,一个操作需要同时修改这些数据,要么全成功,要么全失败,不能出现一部分改了,一部分没改的情况。这就像你同时给好几个朋友发红包,要么大家都收到,要么谁也别想抢,不能出现有人抢到了,有人没抢到的尴尬局面。
为啥需要分布式事务?
想象一下,你的电商系统,订单服务在一个数据库,库存服务在另一个数据库。用户下单了,你得先在订单服务里生成订单,然后在库存服务里扣减库存。如果扣减库存失败了,订单也不能生效,否则就惨了,用户白嫖了商品,你亏死了。这时候,就需要分布式事务来保证订单和库存的一致性。
2PC:两阶段提交
2PC,顾名思义,分两个阶段。我们先来看一下这个过程,然后用代码模拟一下,你就明白了。
-
阶段一:准备阶段 (Prepare Phase)
- 协调者 (Coordinator):这哥们是老大,负责指挥全局。他会向所有参与者 (Participants,比如数据库服务) 发送“准备”请求 (Prepare)。
- 参与者 (Participant):收到“准备”请求后,每个参与者会执行事务的本地操作,但不真正提交,而是把操作结果 (比如修改后的数据) 写入Undo/Redo Log。如果成功,就返回“同意” (Vote Commit) 给协调者;如果失败,就返回“拒绝” (Vote Abort)。
- 超时:如果协调者在指定时间内没有收到参与者的回复,就认为该参与者已经挂了,直接进入回滚阶段。
-
阶段二:提交/回滚阶段 (Commit/Rollback Phase)
- 如果所有参与者都同意 (Vote Commit):协调者向所有参与者发送“提交”请求 (Commit)。参与者收到“提交”请求后,会正式提交事务,释放资源,并返回“完成”给协调者。
- 如果有任何一个参与者拒绝 (Vote Abort):协调者向所有参与者发送“回滚”请求 (Rollback)。参与者收到“回滚”请求后,会根据Undo Log撤销之前的操作,释放资源,并返回“完成”给协调者。
- 超时:如果协调者在指定时间内没有收到参与者的回复,它会重试发送提交或回滚请求,直到所有参与者都完成操作。
2PC 代码模拟 (C++)
为了方便理解,我们简化一下,假设有两个数据库服务,一个负责用户账户,一个负责商品库存。
#include <iostream>
#include <vector>
#include <random>
#include <chrono>
#include <thread>
// 模拟数据库服务
class DatabaseService {
public:
std::string name;
bool canCommit; // 模拟数据库是否可以提交
DatabaseService(const std::string& name, bool canCommit = true) : name(name), canCommit(canCommit) {}
// 准备阶段
bool prepare() {
std::cout << name << ": 收到准备请求,正在准备...n";
// 模拟执行本地事务,并写入Undo/Redo Log
// 这里简单模拟成功或失败
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> distrib(0, 9); // 10% 概率失败
if (!canCommit || distrib(gen) == 0) {
std::cout << name << ": 准备失败!n";
return false; // 准备失败
}
std::cout << name << ": 准备成功!n";
return true; // 准备成功
}
// 提交阶段
void commit() {
std::cout << name << ": 收到提交请求,正在提交...n";
// 模拟提交事务
std::cout << name << ": 提交完成!n";
}
// 回滚阶段
void rollback() {
std::cout << name << ": 收到回滚请求,正在回滚...n";
// 模拟回滚事务
std::cout << name << ": 回滚完成!n";
}
};
// 协调者
class Coordinator {
public:
std::vector<DatabaseService*> participants;
void addParticipant(DatabaseService* participant) {
participants.push_back(participant);
}
// 两阶段提交
bool twoPhaseCommit() {
std::cout << "协调者:开始两阶段提交...n";
// 阶段一:准备阶段
std::vector<bool> votes;
for (DatabaseService* participant : participants) {
votes.push_back(participant->prepare());
}
// 检查是否所有参与者都同意
bool canCommit = true;
for (bool vote : votes) {
if (!vote) {
canCommit = false;
break;
}
}
// 阶段二:提交/回滚阶段
if (canCommit) {
std::cout << "协调者:所有参与者都同意,发送提交请求...n";
for (DatabaseService* participant : participants) {
participant->commit();
}
std::cout << "协调者:提交完成!n";
return true;
} else {
std::cout << "协调者:有参与者拒绝,发送回滚请求...n";
for (DatabaseService* participant : participants) {
participant->rollback();
}
std::cout << "协调者:回滚完成!n";
return false;
}
}
};
int main() {
DatabaseService accountService("账户服务", true); //账户服务默认可以提交
DatabaseService inventoryService("库存服务", false); // 库存服务设置为默认无法提交
Coordinator coordinator;
coordinator.addParticipant(&accountService);
coordinator.addParticipant(&inventoryService);
bool result = coordinator.twoPhaseCommit();
if (result) {
std::cout << "事务成功!n";
} else {
std::cout << "事务失败!n";
}
return 0;
}
这段代码模拟了2PC的过程。DatabaseService
代表参与者,Coordinator
代表协调者。你可以运行这段代码,看看不同的情况下,事务是如何提交或回滚的。注意,为了简化,我们省略了Undo/Redo Log的实现,实际场景中,这部分是必不可少的。
2PC 的优缺点
优点 | 缺点 |
---|---|
原理简单,容易理解和实现。 | 同步阻塞 (Blocking):参与者在准备阶段之后,会一直阻塞,等待协调者的决策,无法释放资源。这会导致系统性能下降。 |
可以保证强一致性,确保所有参与者要么都提交,要么都回滚。 | 单点故障 (Single Point of Failure):协调者是整个事务的中心,如果协调者挂了,整个系统都会受到影响。 |
适用于对数据一致性要求非常高的场景。 | 数据不一致风险:如果在阶段二,协调者发送了Commit请求,但部分参与者没有收到,导致部分提交,部分未提交,造成数据不一致。虽然可以通过重试机制缓解,但无法完全避免。 |
脑裂问题:当协调者和参与者之间的网络出现问题,导致协调者认为参与者挂了,但实际上参与者还在运行,可能会导致数据不一致。例如,协调者通知回滚,但参与者因为网络问题没有收到,仍然提交了。 |
3PC:三阶段提交
为了解决2PC的缺点,人们提出了3PC。3PC在2PC的基础上增加了一个“预提交”阶段,减少了同步阻塞的时间,也降低了单点故障的风险。
-
阶段一:CanCommit
- 协调者 (Coordinator):向所有参与者 (Participants) 发送“CanCommit”请求。
- 参与者 (Participant):收到“CanCommit”请求后,检查自身状态是否可以执行事务,如果可以,返回“同意” (Yes) 给协调者;如果不行,返回“拒绝” (No)。
-
阶段二:PreCommit
- 如果所有参与者都同意 (Yes):协调者向所有参与者发送“PreCommit”请求。参与者收到“PreCommit”请求后,执行事务的本地操作,但不真正提交,而是把操作结果写入Undo/Redo Log。如果成功,就返回“确认” (ACK) 给协调者;如果失败,就返回“中止” (Abort)。
- 如果有任何一个参与者拒绝 (No) 或者超时:协调者向所有参与者发送“中止”请求 (Abort)。参与者收到“中止”请求后,中断事务。
-
阶段三:DoCommit
- 如果所有参与者都确认 (ACK):协调者向所有参与者发送“DoCommit”请求。参与者收到“DoCommit”请求后,会正式提交事务,释放资源,并返回“完成”给协调者。
- 如果有任何一个参与者中止 (Abort) 或者超时:协调者向所有参与者发送“中止”请求 (Abort)。参与者收到“中止”请求后,会根据Undo Log撤销之前的操作,释放资源,并返回“完成”给协调者。
3PC 代码模拟 (C++)
#include <iostream>
#include <vector>
#include <random>
#include <chrono>
#include <thread>
// 模拟数据库服务
class DatabaseService {
public:
std::string name;
bool canCommit; // 模拟数据库是否可以提交
DatabaseService(const std::string& name, bool canCommit = true) : name(name), canCommit(canCommit) {}
// CanCommit 阶段
bool canCommitPhase() {
std::cout << name << ": 收到 CanCommit 请求,正在检查状态...n";
// 模拟检查自身状态是否可以执行事务
if (!canCommit) {
std::cout << name << ": 状态不允许提交!n";
return false;
}
std::cout << name << ": 状态允许提交!n";
return true;
}
// PreCommit 阶段
bool preCommit() {
std::cout << name << ": 收到 PreCommit 请求,正在准备...n";
// 模拟执行本地事务,并写入Undo/Redo Log
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> distrib(0, 9); // 10% 概率失败
if (distrib(gen) == 0) {
std::cout << name << ": 准备失败!n";
return false; // 准备失败
}
std::cout << name << ": 准备成功!n";
return true; // 准备成功
}
// DoCommit 阶段
void doCommit() {
std::cout << name << ": 收到 DoCommit 请求,正在提交...n";
// 模拟提交事务
std::cout << name << ": 提交完成!n";
}
// Abort 阶段
void abort() {
std::cout << name << ": 收到 Abort 请求,正在回滚...n";
// 模拟回滚事务
std::cout << name << ": 回滚完成!n";
}
};
// 协调者
class Coordinator {
public:
std::vector<DatabaseService*> participants;
void addParticipant(DatabaseService* participant) {
participants.push_back(participant);
}
// 三阶段提交
bool threePhaseCommit() {
std::cout << "协调者:开始三阶段提交...n";
// 阶段一:CanCommit 阶段
std::vector<bool> canCommitVotes;
for (DatabaseService* participant : participants) {
canCommitVotes.push_back(participant->canCommitPhase());
}
bool canPreCommit = true;
for (bool vote : canCommitVotes) {
if (!vote) {
canPreCommit = false;
break;
}
}
if (!canPreCommit) {
std::cout << "协调者:有参与者拒绝 CanCommit,发送 Abort 请求...n";
for (DatabaseService* participant : participants) {
participant->abort();
}
std::cout << "协调者:中止完成!n";
return false;
}
// 阶段二:PreCommit 阶段
std::cout << "协调者:所有参与者都同意 CanCommit,发送 PreCommit 请求...n";
std::vector<bool> preCommitAcks;
for (DatabaseService* participant : participants) {
preCommitAcks.push_back(participant->preCommit());
}
bool canDoCommit = true;
for (bool ack : preCommitAcks) {
if (!ack) {
canDoCommit = false;
break;
}
}
if (!canDoCommit) {
std::cout << "协调者:有参与者拒绝 PreCommit,发送 Abort 请求...n";
for (DatabaseService* participant : participants) {
participant->abort();
}
std::cout << "协调者:中止完成!n";
return false;
}
// 阶段三:DoCommit 阶段
std::cout << "协调者:所有参与者都同意 PreCommit,发送 DoCommit 请求...n";
for (DatabaseService* participant : participants) {
participant->doCommit();
}
std::cout << "协调者:提交完成!n";
return true;
}
};
int main() {
DatabaseService accountService("账户服务", true);
DatabaseService inventoryService("库存服务", true);
Coordinator coordinator;
coordinator.addParticipant(&accountService);
coordinator.addParticipant(&inventoryService);
bool result = coordinator.threePhaseCommit();
if (result) {
std::cout << "事务成功!n";
} else {
std::cout << "事务失败!n";
}
return 0;
}
这段代码模拟了3PC的过程。你可以修改canCommit
的值,以及preCommit
中的随机数生成逻辑,来模拟不同的场景,观察3PC是如何工作的。
3PC 的优缺点
优点 | 缺点 |
---|---|
相比2PC,减少了同步阻塞的时间,降低了单点故障的风险。 | 仍然存在数据不一致的风险。如果在阶段三,协调者发送了DoCommit请求,但部分参与者没有收到,或者在发送DoCommit之前协调者挂了,可能会导致数据不一致。 |
在CanCommit阶段,参与者可以检查自身状态,提前拒绝事务,避免不必要的资源消耗。 | 增加了协议的复杂度,实现难度较高。 |
无法完全解决数据一致性问题,只能降低风险。 | |
仍然依赖于协调者的可靠性,虽然降低了单点故障的风险,但如果协调者在关键时刻挂了,仍然会影响整个系统。 |
2PC vs 3PC:选哪个?
这是一个经典的问题,没有绝对的答案,需要根据你的实际场景来选择。
特性 | 2PC | 3PC |
---|---|---|
一致性 | 强一致性 | 最终一致性(但比没有事务强) |
性能 | 较差,同步阻塞严重 | 相对较好,减少了同步阻塞 |
可靠性 | 单点故障风险高 | 降低了单点故障风险 |
复杂度 | 简单 | 复杂 |
适用场景 | 对数据一致性要求极高的场景,例如金融系统 | 对性能要求较高,可以容忍一定数据不一致的场景 |
总结
2PC和3PC是分布式事务中常用的两种协议,它们各有优缺点。选择哪种协议,需要根据你的实际业务场景、数据一致性要求、性能要求、以及系统复杂度等因素综合考虑。记住,没有银弹,只有最适合你的方案。
额外的思考
- CAP 理论:在分布式系统中,一致性 (Consistency)、可用性 (Availability)、分区容错性 (Partition Tolerance) 三者不可兼得。2PC倾向于保证一致性,牺牲可用性;而3PC在一定程度上提高了可用性,但牺牲了一致性。
- BASE 理论:Basically Available (基本可用)、Soft state (软状态)、Eventually consistent (最终一致性)。这是一种更宽松的事务模型,适用于对一致性要求不高的场景。
- 其他分布式事务解决方案:除了2PC和3PC,还有很多其他的分布式事务解决方案,例如TCC (Try-Confirm-Cancel)、Saga模式、本地消息表、消息队列事务等。你需要根据你的实际情况选择最合适的方案。
好了,今天就聊到这里。希望这些能帮你在分布式事务的路上少踩一些坑。记住,实践是检验真理的唯一标准,多动手写代码,才能真正掌握这些技术。下次再见!