问题
什么是2PC和3PC?
答案
1. 2PC(Two-Phase Commit,两阶段提交)
核心概念
2PC是一种强一致性的分布式事务协议,通过协调者(Coordinator)和参与者(Participant)两种角色,分两个阶段完成分布式事务的提交。
角色说明:
- 协调者(TC, Transaction Coordinator):事务管理器,负责协调整个分布式事务
- 参与者(RM, Resource Manager):资源管理器,通常是数据库,执行实际的事务操作
执行流程
阶段一:准备阶段(Prepare Phase)
协调者 参与者1 参与者2 参与者3
| | | |
|------Prepare------------>| | |
|------Prepare----------------------------> | |
|------Prepare------------------------------------------------> |
| | | |
| [执行事务] [执行事务] [执行事务]
| [记录Undo/Redo日志] [记录Undo/Redo日志] [记录Undo/Redo日志]
| [锁定资源] [锁定资源] [锁定资源]
| | | |
|<-----Yes/No--------------| | |
|<-----Yes/No--------------------------------- | |
|<-----Yes/No------------------------------------------------ |
协调者操作:
- 向所有参与者发送
Prepare请求 - 等待所有参与者的响应
参与者操作:
- 执行事务操作(但不提交)
- 将Undo和Redo信息写入日志
- 锁定相关资源
- 向协调者返回
Yes(准备成功)或No(准备失败)
阶段二:提交阶段(Commit Phase)
情况1:所有参与者都返回Yes(成功路径)
协调者 参与者1 参与者2 参与者3
| | | |
|------Commit------------->| | |
|------Commit-----------------------------> | |
|------Commit------------------------------------------------> |
| | | |
| [提交事务] [提交事务] [提交事务]
| [释放锁] [释放锁] [释放锁]
| | | |
|<-----ACK-----------------| | |
|<-----ACK--------------------------------- | |
|<-----ACK------------------------------------------------ |
情况2:任一参与者返回No或超时(失败路径)
协调者 参与者1 参与者2 参与者3
| | | |
|------Rollback----------->| | |
|------Rollback---------------------------> | |
|------Rollback-----------------------------------------------> |
| | | |
| [回滚事务] [回滚事务] [回滚事务]
| [释放锁] [释放锁] [释放锁]
| | | |
|<-----ACK-----------------| | |
|<-----ACK--------------------------------- | |
|<-----ACK------------------------------------------------ |
代码实现示例
/**
* 2PC协调者实现
*/
public class TwoPhaseCommitCoordinator {
private List<Participant> participants;
public boolean execute(Transaction transaction) {
// ============ 阶段一:准备阶段 ============
List<Boolean> prepareResults = new ArrayList<>();
for (Participant participant : participants) {
try {
// 发送Prepare请求
boolean result = participant.prepare(transaction);
prepareResults.add(result);
if (!result) {
// 有参与者准备失败,提前结束
break;
}
} catch (Exception e) {
// 网络异常或超时,视为失败
prepareResults.add(false);
break;
}
}
// 判断是否所有参与者都准备成功
boolean allPrepared = prepareResults.size() == participants.size()
&& prepareResults.stream().allMatch(r -> r);
// ============ 阶段二:提交或回滚阶段 ============
if (allPrepared) {
// 所有参与者准备成功,发送Commit命令
for (Participant participant : participants) {
try {
participant.commit(transaction);
} catch (Exception e) {
// 提交失败,需要重试或人工介入
log.error("Commit failed for participant: {}", participant, e);
}
}
return true;
} else {
// 有参与者准备失败,发送Rollback命令
for (Participant participant : participants) {
try {
participant.rollback(transaction);
} catch (Exception e) {
// 回滚失败,需要重试或人工介入
log.error("Rollback failed for participant: {}", participant, e);
}
}
return false;
}
}
}
/**
* 参与者实现
*/
public class DatabaseParticipant implements Participant {
private DataSource dataSource;
private Connection connection;
@Override
public boolean prepare(Transaction transaction) {
try {
// 获取数据库连接
connection = dataSource.getConnection();
connection.setAutoCommit(false);
// 执行事务操作
PreparedStatement stmt = connection.prepareStatement(transaction.getSql());
stmt.execute();
// 事务执行成功但不提交,资源被锁定
return true;
} catch (Exception e) {
// 执行失败
try {
if (connection != null) {
connection.rollback();
}
} catch (SQLException ex) {
log.error("Rollback failed", ex);
}
return false;
}
}
@Override
public void commit(Transaction transaction) {
try {
if (connection != null) {
connection.commit();
connection.close();
}
} catch (SQLException e) {
log.error("Commit failed", e);
throw new RuntimeException(e);
}
}
@Override
public void rollback(Transaction transaction) {
try {
if (connection != null) {
connection.rollback();
connection.close();
}
} catch (SQLException e) {
log.error("Rollback failed", e);
}
}
}
2PC的问题
问题1:同步阻塞
时间线:
t0: 协调者发送Prepare
t1: 参与者执行事务,锁定资源
t2-t10: 等待协调者的Commit/Rollback命令
↑
资源被长时间锁定,其他事务无法访问
参与者在准备阶段锁定资源后,需要等待协调者的最终指令,期间资源被阻塞。
问题2:单点故障
场景:协调者在发送Commit前崩溃
协调者[崩溃] X
|
|-----Prepare---->参与者1 [阻塞等待]
|-----Prepare---->参与者2 [阻塞等待]
|-----Prepare---->参与者3 [阻塞等待]
结果:所有参与者持续等待,资源无法释放
问题3:数据不一致
场景:协调者发送Commit时发生网络分区
协调者
|-----Commit---->参与者1 [收到,提交] ✓
|-----Commit--X 参与者2 [未收到,阻塞] ?
|-----Commit--X 参与者3 [未收到,阻塞] ?
结果:参与者1已提交,参与者2、3不知是提交还是回滚
问题4:容错性差
- 协调者和参与者都可能成为单点
- 没有超时机制,故障恢复困难
2. 3PC(Three-Phase Commit,三阶段提交)
核心概念
3PC是2PC的改进版本,将准备阶段拆分为 CanCommit 和 PreCommit 两个阶段,并引入超时机制。
执行流程
阶段一:CanCommit(询问阶段)
协调者 参与者1 参与者2 参与者3
| | | |
|------CanCommit?--------->| | |
|------CanCommit?-----------------------------> | |
|------CanCommit?------------------------------------------------> |
| | | |
| [检查资源] [检查资源] [检查资源]
| [不执行事务] [不执行事务] [不执行事务]
| | | |
|<-----Yes/No--------------| | |
|<-----Yes/No--------------------------------- | |
|<-----Yes/No------------------------------------------------ |
特点:
- 只是询问,不执行事务
- 参与者检查自身状态,判断是否能执行事务
- 不锁定资源
阶段二:PreCommit(预提交阶段)
情况1:所有参与者都返回Yes
协调者 参与者1 参与者2 参与者3
| | | |
|------PreCommit---------->| | |
|------PreCommit-----------------------------> | |
|------PreCommit------------------------------------------------> |
| | | |
| [执行事务] [执行事务] [执行事务]
| [写Undo/Redo] [写Undo/Redo] [写Undo/Redo]
| [锁定资源] [锁定资源] [锁定资源]
| | | |
|<-----ACK-----------------| | |
|<-----ACK--------------------------------- | |
|<-----ACK------------------------------------------------ |
情况2:任一参与者返回No或超时
协调者 参与者1 参与者2 参与者3
| | | |
|------Abort-------------->| | |
|------Abort-----------------------------> | |
|------Abort------------------------------------------------> |
| | | |
| [中断事务] [中断事务] [中断事务]
阶段三:DoCommit(提交阶段)
情况1:所有参与者PreCommit成功
协调者 参与者1 参与者2 参与者3
| | | |
|------DoCommit----------->| | |
|------DoCommit---------------------------> | |
|------DoCommit-----------------------------------------------> |
| | | |
| [提交事务] [提交事务] [提交事务]
| [释放资源] [释放资源] [释放资源]
| | | |
|<-----ACK-----------------| | |
|<-----ACK--------------------------------- | |
|<-----ACK------------------------------------------------ |
情况2:PreCommit失败或超时
协调者 参与者1 参与者2 参与者3
| | | |
|------Abort-------------->| | |
|------Abort-----------------------------> | |
|------Abort------------------------------------------------> |
| | | |
| [回滚事务] [回滚事务] [回滚事务]
| [释放资源] [释放资源] [释放资源]
3PC的超时机制
关键改进:参与者引入超时自动决策
/**
* 3PC参与者实现(带超时机制)
*/
public class ThreePhaseCommitParticipant {
private static final long TIMEOUT = 30000; // 30秒超时
@Override
public void preCommit(Transaction transaction) throws Exception {
// 执行事务但不提交
executeTransaction(transaction);
// 启动超时监听
ScheduledFuture<?> timeoutTask = executor.schedule(() -> {
// 超时后自动提交(假设PreCommit成功意味着大概率提交)
try {
commit(transaction);
log.info("Auto commit due to timeout");
} catch (Exception e) {
log.error("Auto commit failed", e);
}
}, TIMEOUT, TimeUnit.MILLISECONDS);
// 等待DoCommit或Abort命令
waitForFinalDecision(transaction, timeoutTask);
}
}
超时后的行为:
- 在CanCommit阶段超时:参与者返回No
- 在PreCommit阶段超时:参与者中断事务
- 在DoCommit阶段超时:参与者自动提交(假设能进入PreCommit说明大概率会提交)
代码实现示例
/**
* 3PC协调者实现
*/
public class ThreePhaseCommitCoordinator {
private List<Participant> participants;
private static final long TIMEOUT = 10000; // 10秒超时
public boolean execute(Transaction transaction) {
// ============ 阶段一:CanCommit ============
if (!canCommitPhase(transaction)) {
return false;
}
// ============ 阶段二:PreCommit ============
if (!preCommitPhase(transaction)) {
abortTransaction(transaction);
return false;
}
// ============ 阶段三:DoCommit ============
return doCommitPhase(transaction);
}
private boolean canCommitPhase(Transaction transaction) {
List<Future<Boolean>> futures = new ArrayList<>();
for (Participant participant : participants) {
Future<Boolean> future = executor.submit(() -> {
try {
return participant.canCommit(transaction);
} catch (Exception e) {
return false;
}
});
futures.add(future);
}
// 等待所有参与者响应(带超时)
for (Future<Boolean> future : futures) {
try {
Boolean result = future.get(TIMEOUT, TimeUnit.MILLISECONDS);
if (!result) {
return false;
}
} catch (TimeoutException e) {
// 超时视为失败
return false;
} catch (Exception e) {
return false;
}
}
return true;
}
private boolean preCommitPhase(Transaction transaction) {
for (Participant participant : participants) {
try {
participant.preCommit(transaction);
} catch (Exception e) {
return false;
}
}
return true;
}
private boolean doCommitPhase(Transaction transaction) {
for (Participant participant : participants) {
try {
participant.doCommit(transaction);
} catch (Exception e) {
log.error("DoCommit failed", e);
// 注意:这里提交失败很难处理
}
}
return true;
}
private void abortTransaction(Transaction transaction) {
for (Participant participant : participants) {
try {
participant.abort(transaction);
} catch (Exception e) {
log.error("Abort failed", e);
}
}
}
}
3. 2PC vs 3PC 对比
| 维度 | 2PC | 3PC |
|---|---|---|
| 阶段数 | 2个(Prepare、Commit) | 3个(CanCommit、PreCommit、DoCommit) |
| 是否有超时 | 无 | 有 |
| 阻塞问题 | 严重(准备后一直等待) | 减轻(超时自动决策) |
| 单点故障 | 严重(协调者故障导致阻塞) | 减轻(超时可自行决策) |
| 数据一致性 | 可能不一致 | 可能不一致(但概率更低) |
| 性能 | 较差 | 更差(多一次网络往返) |
| 复杂度 | 中等 | 高 |
| 实际应用 | XA协议使用 | 几乎不用 |
4. 存在的问题与局限
共同问题
问题1:性能开销大
- 多次网络往返(2PC:2次,3PC:3次)
- 资源长时间锁定
- 同步阻塞
问题2:无法完全避免数据不一致
场景:3PC在DoCommit阶段网络分区
协调者
|-----DoCommit---->参与者1 [提交] ✓
|-----DoCommit--X 参与者2 [超时自动提交] ✓
|-----DoCommit--X 参与者3 [超时自动提交] ✓
看似正常,但如果协调者本应发送Abort呢?
参与者会因为超时而错误地提交!
问题3:不适合高并发场景
- 锁定资源时间过长
- 吞吐量低
5. 实际应用
XA协议(2PC的实现)
// 使用JTA进行分布式事务
@Transactional
public void transfer() throws Exception {
// 获取XA数据源
XADataSource xaDS1 = getXADataSource1();
XADataSource xaDS2 = getXADataSource2();
XAConnection xaConn1 = xaDS1.getXAConnection();
XAConnection xaConn2 = xaDS2.getXAConnection();
XAResource xaRes1 = xaConn1.getXAResource();
XAResource xaRes2 = xaConn2.getXAResource();
Xid xid = new MyXid();
try {
// 阶段1:Prepare
xaRes1.start(xid, XAResource.TMNOFLAGS);
xaConn1.getConnection().createStatement()
.executeUpdate("UPDATE account SET balance = balance - 100");
xaRes1.end(xid, XAResource.TMSUCCESS);
xaRes2.start(xid, XAResource.TMNOFLAGS);
xaConn2.getConnection().createStatement()
.executeUpdate("UPDATE account SET balance = balance + 100");
xaRes2.end(xid, XAResource.TMSUCCESS);
int ret1 = xaRes1.prepare(xid);
int ret2 = xaRes2.prepare(xid);
// 阶段2:Commit
if (ret1 == XAResource.XA_OK && ret2 == XAResource.XA_OK) {
xaRes1.commit(xid, false);
xaRes2.commit(xid, false);
}
} catch (Exception e) {
xaRes1.rollback(xid);
xaRes2.rollback(xid);
}
}
6. 总结
2PC核心要点:
- 两个阶段:Prepare(准备)、Commit/Rollback(提交/回滚)
- 强一致性保证,但存在同步阻塞、单点故障、数据不一致风险
- 实际应用:XA协议、JTA
3PC核心要点:
- 三个阶段:CanCommit(询问)、PreCommit(预提交)、DoCommit(提交)
- 引入超时机制,降低阻塞风险
- 性能更差,理论意义大于实际意义
面试建议:
- 能够清晰描述2PC和3PC的流程
- 理解两者的问题和局限
- 知道为什么实际生产中很少用3PC
- 了解XA协议是2PC的实际应用
- 理解为什么互联网公司更倾向柔性事务(TCC、Saga等)