0Pricing
PHP Academy · 课时

异步消息为何重要

了解队列如何解耦生产者与消费者。

异步消息为何重要 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。

为什么使用异步消息传递

在同步请求中,PHP 进程与邮件网关、支付处理服务或下游服务通信时会被阻塞。在高负载下,这会让您的延迟和可用性受您调用的每一个依赖影响。异步消息传递打破了这条链路:生产者将消息放入队列后立即返回;消费者稍后按照自己的节奏处理消息。

本课讲解的是为什么——在接触 RabbitMQ 或 Kafka 之前,您必须理解解耦、缓冲以及传递保证之间的权衡。

时间与空间解耦

队列沿着两个维度将生产者与消费者解耦:

  • 空间——双方都不需要知道对方的地址,只需要知道代理。
  • 时间——生产者发布消息时,消费者可以处于停机状态;消息会等待。

下面的同步版本将 Web 请求与邮件服务器的正常运行时间和处理速度绑定在了一起。

<?php
// Synchronous: the HTTP request blocks on SMTP
function registerUser(string $email): void {
    saveUser($email);
    // If SMTP is slow or down, the user waits or the request fails
    sendWelcomeEmail($email); // 800ms blocking call
    echo "Registered\n";
}

function saveUser(string $e): void { /* ... */ }
function sendWelcomeEmail(string $e): void { usleep(1); }

registerUser('dev@example.com');

发布后继续

异步版本会保存用户、发布一条 UserRegistered 消息,然后返回。另一个工作进程负责发送邮件。现在,HTTP 延迟只取决于快速的本地代理发布操作,而不取决于 SMTP 往返。

<?php
function registerUser(string $email): void {
    saveUser($email);
    // Publish a lightweight event; worker handles email later
    publish('user.registered', json_encode(['email' => $email]));
    echo "Registered (email queued)\n";
}

function saveUser(string $e): void { /* ... */ }
function publish(string $routingKey, string $payload): void {
    echo "-> queued $routingKey: $payload\n";
}

registerUser('dev@example.com');

负载平滑(缓冲)

流量峰值并不均匀,而处理能力是固定的。队列充当缓冲区:它吸收 10,000 条消息的突发流量,再由一组工作进程以可持续的速率逐步处理。没有队列时,这次峰值会直接压垮数据库或下游 API。

这就是负载平滑——您用延迟(消息可能会短暂等待)换取稳定性(系统不会崩溃)。

传递保证

每个消息系统都会做出一种传递承诺。请明确您使用的是哪一种:

  • 至多一次——发出后不管;消息可能丢失,但绝不会重复。
  • 至少一次——直到确认前都会重新投递;可能出现重复消息。这是最常见的默认方式。
  • 恰好一次——不丢失、不重复;代价高昂,而且在应用层通常只是一种部分成立的假象。

由于大多数真实系统提供的是至少一次传递,您的消费者必须能够容忍两次看到同一条消息。

幂等消费者

解决至少一次传递所产生重复消息的方法是幂等性:处理一条消息两次的结果与处理一次相同。通常的做法是使用去重键(消息标识),并将其存储在唯一索引中。

<?php
function handle(array $msg, PDO $pdo): void {
    $pdo->beginTransaction();
    try {
        // Unique constraint on message_id makes the insert the dedup gate
        $stmt = $pdo->prepare(
            'INSERT INTO processed_messages (id) VALUES (?)'
        );
        $stmt->execute([$msg['id']]);
    } catch (PDOException $e) {
        $pdo->rollBack();
        echo "Duplicate {$msg['id']} skipped\n";
        return; // already handled
    }
    chargeCustomer($msg['amount']);
    $pdo->commit();
}
function chargeCustomer(int $a): void {}

确认与重新投递

消费者通过确认表示成功。如果在确认前崩溃,代理会将消息重新投递给另一个消费者。这正是至少一次传递得以实现的原因——但这意味着 ack 必须在副作用提交之后发生,绝不能提前。

过早确认,崩溃会导致消息丢失。确认过晚(随后崩溃)则会产生重复消息——而幂等层会将其消化。

<?php
// Pseudocode of the consumer contract
function consumeLoop($channel): void {
    while ($msg = $channel->get()) {
        try {
            processSideEffect($msg);   // commit DB write first
            $channel->ack($msg);       // only then ack
        } catch (\Throwable $e) {
            $channel->nack($msg, requeue: true); // let it redeliver
        }
    }
}
function processSideEffect($m): void {}

顺序是有代价的

当消费者扩展到多个实例后,队列无法保证全局顺序。从同一个队列中提取消息的两个工作进程会并发处理,因此消息 B 可能先于消息 A 完成。

如果顺序很重要(例如账户余额变动),您必须按键分区,让所有相关消息按顺序发送给同一个消费者。Kafka 通过分区原生支持这一点;使用 RabbitMQ 时,您可以通过一致性哈希将消息路由到按键划分的队列。

死信队列

有些消息永远无法成功处理,例如格式错误的负载或引用了已删除的行。无限重试会使队列陷入停滞(这就是毒消息)。解决方案是使用死信队列(DLQ):失败 N 次后,将消息转移到一旁进行检查,而不是继续重新投递。

<?php
function consume(array $msg, $channel): void {
    $attempts = ($msg['headers']['x-attempt'] ?? 0) + 1;
    try {
        process($msg);
        $channel->ack($msg);
    } catch (\Throwable $e) {
        if ($attempts >= 5) {
            $channel->deadLetter($msg); // park in DLQ
        } else {
            $channel->republish($msg, ['x-attempt' => $attempts]);
        }
    }
}
function process(array $m): void {}

何时不要使用队列

异步消息传递会带来实际的运维成本:需要运行一个代理,需要向产品团队解释最终一致性,还会使跨进程边界的调试更加困难。

当工作缓慢、突发、可重试或发出后不需要等待结果时,请考虑使用队列。当调用方确实需要现在得到结果时,应保持同步(例如用户必须看到的权威价格)——将必须立即得到答案的调用包装进队列,只会增加延迟和复杂性。

队列与日志

有两种广义的代理结构支持这些模式,本课程的其余部分会同时使用它们:

  • 任务队列(RabbitMQ)会在消息确认后删除消息。它擅长将工作分发给一组竞争的消费者,并支持按消息路由和生存时间。
  • 提交日志(Kafka)会按照保留策略保存消息;每个消费者跟踪自己的偏移量,可以重放历史记录,并且许多相互独立的消费者组可以读取同一条流。

需要分发工作时选择队列,需要高吞吐量事件流和重放能力时选择日志。

<?php
$useCase = 'replay events for a new analytics service';
$broker = str_contains($useCase, 'replay') || str_contains($useCase, 'stream')
    ? 'Kafka (commit log)'
    : 'RabbitMQ (task queue)';
echo $broker . "\n";

快速检查

思考传递保证。

回顾

现在您已经掌握了异步消息传递背后的思维模型:

  • 队列提供空间与时间解耦,并充当负载平滑的缓冲区。
  • 大多数系统采用至少一次传递,因此消费者必须具备幂等性。
  • 在副作用提交之后确认;让失败的消息重新投递。
  • 顺序需要按键分区;毒消息需要DLQ。
  • 不要将调用方需要同步得到答案的工作放入队列。

接下来,您将在 PHP 中使用 RabbitMQ 实践这些内容。

常见问题解答

「异步消息为何重要」课时是免费的吗?

是的 — 「异步消息为何重要」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。

「异步消息为何重要」这节课中我会学到什么?

了解队列如何解耦生产者与消费者。 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 PHP Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「异步消息为何重要」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 PHP Academy 课中编写并运行代码吗?

能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 异步消息为何重要
  2. 在 PHP 中使用 RabbitMQ
  3. 使用 PHP 的 Apache Kafka
  4. 构建事件驱动的工作流
← 返回 PHP Academy