问题

什么是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------------------------------------------------               |

协调者操作

  1. 向所有参与者发送 Prepare 请求
  2. 等待所有参与者的响应

参与者操作

  1. 执行事务操作(但不提交)
  2. 将Undo和Redo信息写入日志
  3. 锁定相关资源
  4. 向协调者返回 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的改进版本,将准备阶段拆分为 CanCommitPreCommit 两个阶段,并引入超时机制

执行流程

阶段一: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等)