异步消息为何重要
了解队列如何解耦生产者与消费者。
异步消息为何重要 是 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 反馈 — 无需本地设置。