分布式事务是指事务跨越多个数据库或多个系统的事务,这些数据库或系统分布在不同的服务器上。
在当下网络环境,分布式事务是一个不可避免的问题。
今年年初阅读到一篇很不错的文章。我根据自己的理解再写一遍。加强自己的理解。

分布式事务基础

CAP理论

CAP理论是理解分布式系统设计的一个重要概念。它描述了在分布式计算中,系统在满足以下三个特性中的两个时面临的挑战和权衡:一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。让我们逐一解释这些概念:

1. 一致性 (Consistency)

  • 定义:一致性指的是所有节点在同一时间看到的数据是一致的。也就是说,在一个事务完成后,所有的用户都应该看到同样的数据值。
  • 分布式场景中的挑战:在分布式系统中,维持强一致性意味着任何数据的变更需要即时在所有节点上反映,这可能会导致延迟,特别是在网络分区或故障时。

2. 可用性 (Availability)

  • 定义:可用性是指每次请求都能在有限的时间内得到响应,无论响应是成功还是失败。
  • 分布式场景中的挑战:在分布式系统中,保证高可用性意味着即使某些节点失败,系统也能继续操作。这可能需要复制数据和负载均衡机制。

3. 分区容错性 (Partition Tolerance)

  • 定义:分区容错性是指系统能够持续提供服务,即使发生网络分区导致某些节点间无法通信。
  • 分布式场景中的挑战:网络分区在分布式系统中是不可避免的。一个能够容忍分区的系统需要能够处理数据同步和一致性问题,当网络重新连接时。

CAP理论的含义

CAP理论表明,在一个分布式系统中,不可能同时完全满足一致性、可用性和分区容错性这三个要求。系统设计者必须在这三者之间做出权衡。

  • CP(一致性和分区容错性):强调数据的一致性,即使在网络分区的情况下也能保证数据不会出现不一致的情况。但可能牺牲可用性,即在分区情况下某些请求可能无法得到响应。
  • AP(可用性和分区容错性):保证服务的高可用性,即使在网络分区的情况下。但可能无法保证强一致性,数据可能会出现短暂的不一致状态。
  • CA(一致性和可用性):在没有网络分区的理想环境中,可以同时保证一致性和可用性。但在实际的分布式系统中,网络分区是无法避免的,因此这种组合并不现实。

实际应用

在设计分布式系统时,根据业务需求和系统特性选择适当的CAP平衡点是关键。例如,某些系统可能选择牺牲一致性以获得更高的可用性(如多数NoSQL数据库),而其他系统可能为了保持数据的严格一致性而牺牲一部分可用性(如传统的关系型数据库)。

解决方案-2PC

二阶段提交(2PC, Two-Phase Commit)是一种常用于分布式系统的事务协议,它确保所有参与节点要么全部提交事务,要么全部回滚事务,从而保证了跨多个节点的事务的原子性。在Laravel和PHP环境中实现2PC可能比较复杂,因为它需要紧密的跨服务协作和精细的控制。

二阶段提交的两个阶段:

  1. 准备阶段(Voting Phase)

    • 事务协调器向所有参与者发送准备请求。
    • 每个参与者执行事务操作,但不提交,然后返回成功或失败。
  2. 提交/回滚阶段(Commit Phase)

    • 如果所有参与者报告成功,协调器发送提交请求;否则发送回滚请求。
    • 参与者根据协调器的指令执行提交或回滚操作。

在Laravel中实现2PC:

假设你有多个服务或数据库实例,你需要一个事务协调器和各个参与者的协作。这里,我们将使用伪代码和概念性的示例来描述实现方法。

1. 事务协调器

你需要一个中心服务作为协调器,它负责指导事务的各个阶段。这个协调器可以是一个简单的Laravel服务或控制器。

1
2
3
4
5
6
7
8
9
10
11
class TransactionCoordinator {
public function prepare() {
// 向所有参与者发送准备消息
// 等待所有参与者的响应
}

public function commitOrRollback() {
// 根据参与者的准备响应决定提交还是回滚
// 发送相应的消息给所有参与者
}
}

2. 参与者服务

每个参与者需要能够响应协调器的准备和提交/回滚请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ParticipantService {
public function prepare() {
// 执行事务操作,但不提交
// 返回准备的响应
}

public function commit() {
// 提交事务
}

public function rollback() {
// 回滚事务
}
}

3. 通信机制

2PC要求协调器和参与者之间有可靠的通信机制。这通常通过HTTP请求、RPC调用或消息队列实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
class CoordinatorClient {
public function sendPrepareRequest() {
// 向参与者发送准备请求
}

public function sendCommitRequest() {
// 向参与者发送提交请求
}

public function sendRollbackRequest() {
// 向参与者发送回滚请求
}
}

注意事项

  • 死锁和超时:2PC容易导致死锁,需要设定超时机制。
  • 数据一致性:确保所有参与者都能正确响应协调器的命令。
  • 恢复机制:参与者和协调器应该具有故障恢复机制。
  • 性能考虑:2PC可能会因为锁定资源而影响性能。

XA方案的问题

  1. 需要本地数据库支持XA协议
  2. 资源锁需要等到两个阶段结束才释放,性能较差

结论

在Laravel和PHP环境中实现2PC需要考虑多个方面,包括事务协调、参与者协作、通信机制以及错误处理和性能问题。由于2PC的复杂性和潜在的性能影响,建议仅在确实需要强一致性且其他方法(如最终一致性)不足以满足需求时使用。在现代分布式系统中,更轻量级和灵活的事务模型(如最终一致性、TCC、SAGA等)通常更受欢迎。

解决方案=TCC

TCC(Try-Confirm-Cancel)是一种常用于处理分布式事务的模式,特别是在微服务架构中。TCC将一个大的分布式事务拆分为多个本地事务,并通过三个阶段(尝试(Try)、确认(Confirm)、取消(Cancel))来管理这些事务。这种模式特别适合于操作可以明确分为预留资源(Try)、最终执行(Confirm)和回滚操作(Cancel)的场景。

在使用Laravel和PHP实现TCC的分布式事务时,你需要考虑以下几个关键步骤:

1. 定义TCC接口

首先,为参与TCC事务的每个服务定义Try, Confirm和Cancel接口。这些接口负责执行实际的业务逻辑。

1
2
3
4
5
interface TccActionInterface {
public function try();
public function confirm();
public function cancel();
}

2. 实现服务

每个参与TCC事务的服务都需要实现上述接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
class OrderService implements TccActionInterface {
public function try() {
// 预留资源,例如创建订单但标记为未确认
}

public function confirm() {
// 确认操作,例如将订单状态改为已确认
}

public function cancel() {
// 取消操作,例如删除或标记订单为已取消
}
}

3. TCC事务协调器

创建一个事务协调器来管理整个TCC事务的流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TccTransactionCoordinator {
protected $services = [];

public function addService(TccActionInterface $service) {
$this->services[] = $service;
}

public function execute() {
try {
foreach ($this->services as $service) {
$service->try();
}
foreach ($this->services as $service) {
$service->confirm();
}
} catch (\Exception $e) {
foreach ($this->services as $service) {
$service->cancel();
}
throw $e;
}
}
}

4. 执行TCC事务

在业务逻辑中,使用协调器来执行整个TCC事务。

1
2
3
4
5
6
7
8
9
10
11
$coordinator = new TccTransactionCoordinator();
$coordinator->addService(new OrderService());
$coordinator->addService(new PaymentService());
// 添加其他服务...

try {
$coordinator->execute();
// 事务成功
} catch (\Exception $e) {
// 事务失败,已执行回滚
}

5. 异常处理和回滚

确保在Try阶段捕获任何异常,并在发生异常时调用Cancel操作进行回滚。

注意事项

  • 超时处理:在TCC事务中,需要考虑超时处理机制。如果在指定时间内未完成Confirm或Cancel操作,应该有相应的处理策略。
  • 幂等性:确保每个阶段的操作是幂等的,即重复执行不会产生副作用。
  • 资源锁定:在Try阶段预留资源时可能需要锁定资源,以避免并发问题。
  • 日志和监控:记录事务的每个阶段,便于问题追踪和系统监控。
  • 补偿机制:在实际应用中,需要考虑补偿机制,以处理长期未完成或失败的事务。

补充

在实现TCC(Try-Confirm-Cancel)模式的分布式事务中,确实需要特别注意三种关键的异常情况:空回滚(Null Rollback)、幂等性(Idempotence)、悬挂(Hanging)。下面详细解释这些概念及其普遍的解决方案:

1. 空回滚(Null Rollback)

定义

空回滚是指在Try阶段之前,由于系统问题或业务校验失败,导致Cancel操作被触发,但是由于Try阶段尚未执行或未完成,实际上没有任何资源被预留。

解决方案

  • 状态标记:在Try阶段开始时,设置一个标记(例如,在数据库中插入一条记录),标记此事务已进入Try阶段。在执行Cancel操作之前检查此标记。
  • 条件检查:在执行Cancel操作时,首先检查是否有资源被预留或修改。如果没有,则跳过或执行最小操作。

2. 幂等性(Idempotence)

定义

幂等性是指无论一个操作执行多少次,结果都保持不变。在TCC事务中,由于网络原因或其他因素,Confirm或Cancel操作可能会被重复执行。

解决方案

  • 唯一标识:为每个事务操作分配一个唯一标识,并在数据库中记录每个操作的执行状态。
  • 操作检查:在执行Confirm或Cancel操作前,检查该操作是否已被执行。如果已执行,直接返回成功响应而不重复执行业务逻辑。
  • 事务日志:记录每个事务操作的日志,用于恢复和重试机制。

3. 悬挂(Hanging)

定义

悬挂发生在Try操作成功后,但由于某些原因(如系统崩溃或网络问题),Confirm或Cancel操作无法及时执行。

解决方案

  • 超时机制:为每个事务设置一个超时时间。如果在规定时间内未收到Confirm或Cancel指令,触发超时处理机制。
  • 状态恢复:系统重启后,检查未完成的事务,并根据事务的当前状态决定是继续执行Confirm操作还是执行Cancel操作。
  • 定时任务:运行一个定时任务或调度器,定期检查悬挂的事务并采取相应的措施。

4. 悬挂2.0 (Hanging)

定义 2.0

悬挂发生在其第二阶段 Cancel 接口 比 Try 接口 先执行。
具体情况是 由于RPC调用的网络拥堵和超时,导致事务管理器(TM)误判事务应该回滚,而实际上参与者(RM)可能稍后才收到并执行Try操作。这就导致了资源被预留但没有相应的Confirm或Cancel操作来后续处理它们。这种情况的确是一种悬挂状态。

解决方案策略 2.0

  1. 超时设置与重试策略

    • 调整RPC调用的超时设置,确保有足够的时间等待网络拥堵解决。
    • 实现重试机制,当RPC调用失败或超时时,可以尝试重新发起调用。
  2. 事务状态管理

    • 在参与者(RM)端,增加事务状态的管理。当接收到Try请求时,检查本地事务状态,如果发现事务已被标记为回滚,则不执行Try操作或直接执行Cancel操作。
  3. 事务日志记录

    • 在RM端记录详细的事务日志,包括每个事务阶段的开始和结束时间。这可以帮助在出现问题时进行事务的恢复和状态检查。
  4. 事务恢复和补偿机制

    • 设计事务恢复机制,以便在系统重新启动后,检查悬挂的事务并采取相应的措施。
    • 实现补偿事务,以处理那些因超时或其他问题而悬挂的事务。
  5. 同步与异步处理的权衡

    • 考虑在一些关键操作中使用同步调用而非异步,尽管这可能会影响系统性能,但可以减少悬挂的风险。
  6. 监控和警报

    • 实施有效的监控机制,以便及时发现悬挂事务并手动干预。

结合实践

在实际应用中,这些异常处理策略通常需要结合业务逻辑和系统特性来设计和实现。例如,你可能需要根据业务场景调整超时时间长度,或者根据数据模型设计事务状态的记录和检查机制。同时,保证系统的健壮性和可靠性也是实现TCC事务管理的关键部分。

结论

TCC模式是处理分布式事务的一种有效方法,它通过将大事务分解为多个小事务,并通过三个阶段来管理,以达到整体的一致性。在Laravel和PHP中实现TCC模式需要精心设计接口、服务以及事务协调逻辑,并且需要考虑事务管理的复杂性和各种异常情况的处理。

解决方案-基于消息队列的最终一致性

可靠消息最终一致性是一种常用于处理分布式系统中事务一致性问题的方法。在这种模式下,通过在本地事务和消息发送之间建立一种可靠的机制来保证最终一致性。使用Laravel和PHP实现这个方案时,可以考虑以下步骤:

1. 消息发送表的设计

在本地数据库中创建一个消息发送表,用于记录将要发送到消息队列的消息。这个表至少应包含以下字段:

  • 消息ID(唯一标识)
  • 消息内容(或引用)
  • 发送状态(例如:待发送、已发送、发送失败)
  • 创建时间和更新时间
1
2
3
4
5
6
7
Schema::create('message_queue', function (Blueprint $table) {
$table->id();
$table->string('message_id')->unique();
$table->text('payload'); // 消息内容
$table->string('status')->default('pending'); // 状态
$table->timestamps();
});

2. 本地事务与消息记录

在业务逻辑中,首先执行本地事务操作,然后将需要发送的消息写入消息发送表。

1
2
3
4
5
6
7
8
9
10
11
12
DB::transaction(function () use ($message) {
// 执行业务逻辑...

// 将消息写入消息发送表
DB::table('message_queue')->insert([
'message_id' => Str::uuid(), // 或其他唯一标识生成方式
'payload' => json_encode($message),
'status' => 'pending',
'created_at' => now(),
'updated_at' => now(),
]);
});

3. 定时任务轮询

使用Laravel的定时任务功能(Scheduler)来定期检查消息发送表,并发送尚未成功发送的消息。

1
2
3
4
5
6
7
8
9
10
11
12
// 在 App\Console\Kernel.php 中的 schedule 方法添加定时任务
protected function schedule(Schedule $schedule)
{
$schedule->call(function () {
$messages = DB::table('message_queue')->where('status', 'pending')->get();

foreach ($messages as $message) {
// 发送消息到MQ...
// 根据发送结果更新消息状态
}
})->everyMinute(); // 根据需要调整时间间隔
}

4. 消息队列(RocketMQ)的使用

使用RocketMQ PHP客户端来发送消息,并根据发送结果更新本地消息表的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use RocketMQ\Client;

foreach ($messages as $message) {
$client = new Client('rocketmq-server-url');
$result = $client->sendMessage($message->payload);

if ($result->isSuccessful()) {
DB::table('message_queue')
->where('message_id', $message->message_id)
->update(['status' => 'sent']);
} else {
// 根据业务逻辑处理发送失败的情况
}
}

5. 消费者端的处理

在消费者端,处理消息并在完成业务逻辑后发送ACK反馈。

1
2
3
4
5
6
$client->consumeMessage('topic', function ($message) {
// 处理消息

// 处理完成后发送ACK
$client->acknowledgeMessage($message->messageId);
});

注意事项

  • 幂等性:确保消费端处理消息的幂等性,避免重复消费造成的问题。
  • 异常处理:在发送和消费消息过程中应妥善处理异常,确保系统的稳定性。
  • 消息追踪:记录消息的发送和消费日志,便于问题排查和系统监控。
  • 重试机制:对于发送失败的消息,可以考虑实现重试机制。
  • 死信处理:对于无法正常处理的消息,应当有相应的死信队列处理机制。

通过上述步骤,可以在Laravel和PHP环境中实现一个基于可靠消息最终一致性的分布式事务解决方案。
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。

解决方案-最大努力通知

在分布式系统中,最大努力通知(Best-Effort Notification)是一种常用来处理跨服务通知的方案,特别是在涉及到第三方服务(如支付系统)的场景中。这种方案的核心思想是发起通知方会尽最大努力通过重试机制等方式将业务处理结果通知到接收方,但不保证100%的成功率。

在Laravel和PHP环境中实现最大努力通知的分布式事务,可以按照以下步骤进行:

1. 系统间的调用流程

假设有两个系统:账户系统和充值系统。流程如下:

  1. 账户系统调用充值系统接口进行充值

    • 账户系统向充值系统发送充值请求。
    • 充值系统接收请求并处理充值逻辑。
  2. 充值系统完成支付处理后通知账户系统

    • 充值系统在支付完成后向账户系统发送结果通知。
    • 如果通知失败,充值系统会根据设定的策略重试通知。
  3. 账户系统接收通知并更新状态

    • 账户系统接收到通知后,更新充值状态。
  4. 失败回调和状态查询

    • 如果账户系统未收到通知,它会主动调用充值系统的查询接口以获取充值结果。

2. Laravel实现

账户系统(消费方)

  1. 发起充值请求

    • 使用HTTP客户端(如Guzzle)向充值系统发送充值请求。

      1
      2
      3
      4
      5
      6
      use Illuminate\Support\Facades\Http;

      $response = Http::post('https://recharge-system.com/api/recharge', [
      'user_id' => $userId,
      'amount' => $amount,
      ]);
  2. 处理通知

    • 创建一个路由和控制器来接收充值系统的通知。

      1
      Route::post('/api/recharge/notify', [RechargeController::class, 'notify']);
    • 在控制器中处理通知:

      1
      2
      3
      4
      5
      public function notify(Request $request)
      {
      // 验证通知内容
      // 更新账户充值状态
      }
  3. 状态查询

    • 如果未收到通知,通过定时任务或按需调用充值系统的查询接口。

      1
      2
      3
      4
      // 定时任务或其他机制调用
      $rechargeStatus = Http::get('https://recharge-system.com/api/check-status', [
      'user_id' => $userId,
      ]);

充值系统(发起方)

  1. 处理充值逻辑

    • 接收并处理来自账户系统的充值请求。
  2. 发送通知

    • 完成充值后,向账户系统发送通知。如果失败,则根据重试策略重发。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      $response = Http::post('http://account-system.com/api/recharge/notify', [
      'user_id' => $userId,
      'status' => 'success',
      // 其他所需数据
      ]);

      if ($response->failed()) {
      // 重试逻辑,可能是队列延迟处理、定时任务等
      }

3. 重试机制

  • 实现重试机制来确保通知的最大努力送达。
  • 可以使用Laravel的队列系统,结合延迟和最大尝试次数来管理重试。

4. 事务一致性和幂等性

  • 确保账户系统处理通知的幂等性,即重复接收相同的通知不会导致不一致的结果。
  • 使用事务来确保更新状态的一致性。

通过上述步骤,你可以在Laravel和PHP环境中实现一个基于最大努力通知的分布式

事务解决方案。这种方法特别适用于与第三方支付系统等外部服务交互的场景。

4种方式的总结对比

这四种分布式事务处理方法——2PC(Two-Phase Commit)、TCC(Try-Confirm-Cancel)、可靠消息最终一致性和最大努力通知——各有其特点和适用场景。下面是对这些方法的比较分析:

1. 2PC(Two-Phase Commit)

优点

  • 强一致性:保证了数据的强一致性,适用于对一致性要求高的场景。
  • 原子性操作:确保所有操作要么全部成功,要么全部失败。

缺点

  • 性能开销大:由于需要多个阶段的协调,性能开销较大。
  • 可用性降低:在等待所有节点确认过程中,相关资源会被锁定,影响可用性。
  • 复杂的故障恢复:在出现故障时,恢复过程较为复杂。

2. TCC(Try-Confirm-Cancel)

优点

  • 灵活性:允许业务逻辑在不同阶段有更多的控制,适合复杂业务。
  • 减少锁定资源时间:只在Try阶段锁定资源,缩短了资源锁定时间。

缺点

  • 实现复杂:需要为每个业务操作定义三个阶段,增加了实现的复杂性。
  • 一致性风险:在某些场景下可能存在数据一致性维护的挑战。

3. 可靠消息最终一致性

优点

  • 最终一致性:能够实现分布式系统的最终一致性。
  • 系统解耦:服务间通过消息传递解耦,增加了系统的灵活性和可扩展性。

缺点

  • 消息处理延迟:最终一致性可能导致数据状态更新有延迟。
  • 复杂的消息管理:需要处理消息的可靠传递、重试机制和死信队列。

4. 最大努力通知

优点

  • 实现简单:相比于其他方法,实现相对简单。
  • 降低资源锁定:不需要长时间锁定资源,提高了系统的可用性。

缺点

  • 无法保证强一致性:只能保证最大努力地通知,不能保证数据的强一致性。
  • 依赖网络可靠性:高度依赖网络和服务的可靠性。

总结表格

特性/方法 2PC TCC 可靠消息最终一致性 最大努力通知
一致性 强一致性 最终一致性 最终一致性 最大努力一致性
性能开销
实现复杂度
资源锁定
适用场景 严格事务要求的场景 需要灵活业务处理的场景 异步处理、系统解耦的场景 简单通知、低一致性要求的场景
故障恢复 复杂 复杂 相对简单 相对简单

每种方法都有其适用的

场景和限制。在选择合适的分布式事务处理方案时,需要根据具体的业务需求、系统架构和一致性需求来决定。