Pending 列表消息重试
解读
在国内高并发业务(电商秒杀、支付回调、物流通知)中,经常把“待重试”消息暂存在 Pending 队列。面试官问“Pending 列表消息重试”,不是让你背 RabbitMQ API,而是考察:
- 如何可靠地把失败消息重新投入消费链路,避免丢消息与重复消费;
- 如何可控地退避重试,防止瞬间把下游打垮;
- 如何可观测地埋点、告警、人工干预;
- 如何用 PHP 常见技术栈(Redis / RocketMQ / Kafka + Laravel / Hyperf)低成本落地。
一句话:让消息“死不掉、重不乱、查得到”。
知识点
- 消息失败生命周期:Ready → Consumed → Nack → Pending → Retry → Dead。
- 重试策略:固定间隔、指数退避、最大重试次数、退避上限。
- 幂等键设计:业务单号 + 来源系统 + 重试次数字段,防止重复落库。
- 并发控制:Redis SETNX 分布式锁 / Lua 脚本保证“单实例重试”。
- 延迟队列实现:
- Redis ZSET score=下次执行时间戳,PHP 定时轮询;
- RocketMQ 延迟等级 1s 5s 10s 30s 1m 2m … 18 级;
- Laravel Queue
later($delay, $job)底层也是 Redis ZSET。
- 失败记录存储:MySQL 表结构(id、topic、payload、fail_times、next_retry_at、status、created_at、updated_at)+ 联合索引 (status, next_retry_at)。
- 监控告警:Prometheus Counter(retry_total)、Histogram(retry_delay)、Grafana 面板;超过阈值发钉钉/飞书。
- 人工干预:后台“一键重发”“标记死亡”“批量导出”功能,运营可见。
- PHP 细节:
- 使用
declare(ticks=1)+pcntl_signal保证 CLI 重试进程优雅退出; - 开启
opcache.enable_cli=1防止频繁重载脚本; - 利用
Redis::multi()打包ZREM+LPUSH保证原子性。
- 使用
答案
下面给出一套可直接落地的 PHP+Laravel+Redis 方案,兼顾“可靠、可控、可观测”。
- 表结构(MySQL)
CREATE TABLE msg_retry (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
topic VARCHAR(64) NOT NULL DEFAULT '',
payload JSON NOT NULL,
fail_times TINYINT UNSIGNED NOT NULL DEFAULT 0,
next_retry_at INT UNSIGNED NOT NULL,
status ENUM('pending','done','dead') NOT NULL DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_status_next (status, next_retry_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 失败时写入 Pending 列表(业务消费端)
class OrderPaidConsumer
{
public function handle(Message $message)
{
try {
$this->doBusiness($message);
} catch (\Throwable $e) {
$failTimes = RetryService::getFailTimes($message->id());
$delay = RetryService::calcDelay($failTimes); // 指数退避
RetryService::push($message, $failTimes + 1, $delay);
// 确认消息已记录,可 ACK 防止重复投递
}
}
}
- RetryService 核心逻辑
class RetryService
{
public static function push(Message $msg, int $failTimes, int $delaySeconds): void
{
$next = time() + $delaySeconds;
// 双写:MySQL 持久化 + Redis 延迟队列
$id = DB::table('msg_retry')->insertGetId([
'topic' => $msg->topic(),
'payload' => json_encode($msg->payload()),
'fail_times' => $failTimes,
'next_retry_at'=> $next,
]);
Redis::zadd('retry:zset', $next, $id);
}
// 定时任务每分钟跑一次
public static function retry(): void
{
$now = time();
$ids = Redis::zrangebyscore('retry:zset', 0, $now, 'LIMIT', 0, 100);
foreach ($ids as $id) {
// 分布式锁,保证多实例只重试一次
$lock = Redis::set("retry:lock:$id", 1, 'EX', 5, 'NX');
if (!$lock) continue;
$row = DB::table('msg_retry')->where('id', $id)->lockForUpdate()->first();
if (!$row || $row->status !== 'pending') continue;
// 超过最大次数 => 死亡
if ($row->fail_times >= 6) {
DB::table('msg_retry')->where('id', $id)->update(['status'=>'dead']);
Redis::zrem('retry:zset', $id);
Alert::deadLetter($row);
continue;
}
// 重新投递到 Laravel Queue,并更新数据库
dispatch(new RetryJob($row))->onQueue('retry');
DB::table('msg_retry')->where('id', $id)->update(['status'=>'done']);
Redis::zrem('retry:zset', $id);
}
}
}
- 退避算法
public static function calcDelay(int $failTimes): int
{
return min(900, (int)(pow(2, $failTimes) * 10)); // 10s 20s 40s … 上限 15min
}
-
幂等保障 下游业务表加唯一索引
uk_order_id_source (order_id, source),消费逻辑先INSERT IGNORE或SELECT + INSERT,保证同一消息只写一次。 -
监控 Prometheus 埋点:
\Prometheus::counter('msg_retry_total')->inc(['topic'=>$topic]);
\Prometheus::histogram('msg_retry_delay')->observe($delay, ['topic'=>$topic]);
Grafana 配置告警:当 increase(msg_retry_total[5m]) > 200 且 delay>p99>600s 时飞书群机器人告警。
- 人工干预 Laravel Nova / Hyperf Admin 内置“重试管理”页面,支持:
- 搜索 topic、状态、时间段;
- 单条“立即重投”;
- 批量“标记死亡”;
- 导出 CSV 供运营复盘。
通过以上 7 步,PHP 团队可在不引入重量级中间件的前提下,实现高可靠、可灰度、可监控的消息重试体系,满足国内互联网大厂“高并发 + 低成本 + 快速迭代”的面试与实战要求。
拓展思考
- 如果公司全面云原生化,可把 Redis 延迟队列替换为 RocketMQ 延迟消息,利用 PHP 的
rocketmq-client扩展,消费端只需pushRetryMessage($delayLevel),省去轮询线程,但需注意 RocketMQ 延迟等级固定,无法任意秒级。 - 当重试流量突增,Redis ZSET 热 key 可能打满 CPU,可采取“分片 + 批量”策略:按
crc32($id)%64拆成 64 个小的 ZSET,定时任务并发拉取,再合并结果。 - 对于金融场景,可引入“对账补偿”机制:每日凌晨离线扫描
status=dead的记录,与业务对账文件比对,发现资金差异后触发 T+0 人工补偿流程,实现“技术 + 运营”双保险。 - PHP 进程常驻方案:除 Laravel Schedule 外,可用 Hyperf Swoole 协程常驻进程消费
retry:zset,利用Coroutine\Channel做流量削峰,QPS 可提升 5~8 倍,面试时可作为“性能优化”亮点。 - 面试反向提问:可问面试官“贵司重试最大延迟是多少?是否接受幂等键带业务版本号?是否有跨语言消费端?”体现你对多语言异构系统的思考深度。