欢迎光临
我们一直在努力

用Redis队列秒级提升接口响应速度

前段时间接了个需求,用户提交表单之后要发邮件、写日志、还要同步一份数据到第三方接口。一开始我图省事,全塞在接口里同步跑,结果接口响应从 200ms 直接飙到 3 秒多,用户点提交转圈转到怀疑人生。后来改成用 Redis 做消息队列,把耗时操作丢到后台异步处理,接口响应立马回到 200ms 以内。这篇就把这套简单实现整理一下,代码都是能直接跑的,希望能帮到遇到同样问题的朋友。

为什么选 Redis 而不是 RabbitMQ

说实话,如果项目体量大、对消息可靠性要求高,RabbitMQ 或者 Kafka 肯定是更正规的选择。但很多中小项目其实用不上那么重的东西,Redis 本身项目里就在用(缓存、Session 都靠它),再拿来做轻量级队列,几乎零额外成本。

Redis 做队列主要有两种思路:

  • List 结构:用 LPUSH 入队、BRPOP 出队,简单直接,适合大多数场景。
  • Stream 结构:Redis 5.0 之后引入,支持消费组和 ACK 机制,可靠性更好,但用起来稍复杂。

下面我以 List 方案为主,因为它的代码量最少,理解起来也最直观。

整体思路先捋清楚

整个流程其实就三步:生产者把任务序列化成 JSON 塞进 Redis 列表,消费者用阻塞方式不断取任务,取到之后执行具体业务逻辑。中间如果执行失败,可以重新入队或者记录到失败队列里人工处理。

flowchart LR
A[Web 接口] –>|LPUSH| B[(Redis List)]
B –>|BRPOP| C[消费者进程]
C –> D{执行成功?}
D –>|是| E[记录日志]
D –>|否| F[重试或进失败队列]

环境准备

我本地用的是 PHP 7.4 + Redis 6.2,扩展装的是 phpredis。如果你用的是 Predis 这种纯 PHP 客户端,代码稍微改一下连接部分就行,逻辑是一样的。

# 安装 phpredis 扩展
pecl install redis
确认扩展已加载
php -m | grep redis

生产者:把任务丢进队列

生产者这块很简单,核心就是 lPush。我习惯把任务类型、参数、时间戳一起打包,方便消费者那边做区分和排查问题。

<?php
class QueueProducer
{
private $redis;
private $queueKey = 'app:task:queue';
public function __construct()
{
$this-&gt;redis = new Redis();
$this-&gt;redis-&gt;connect('127.0.0.1', 6379);
$this-&gt;redis-&gt;auth('your_password'); // 没设密码就注释掉
}

public function push(string $type, array $payload): bool
{
$task = [
'id' =&gt; uniqid('task_', true),
'type' =&gt; $type,
'payload' =&gt; $payload,
'created_at' =&gt; time(),
'retry' =&gt; 0,
];

$json = json_encode($task, JSON_UNESCAPED_UNICODE);
return $this-&gt;redis-&gt;lPush($this-&gt;queueKey, $json) &gt; 0;
}
}
// 调用示例:用户注册后异步发欢迎邮件
$producer = new QueueProducer();
$producer->push('send_welcome_mail', [
'user_id' => 10086,
'email' => 'test@example.com',
]);

这里有个小细节:json_encode 记得加 JSON_UNESCAPED_UNICODE,不然中文参数会变成一堆 \\uXXXX,排查日志的时候看着头疼。

消费者:常驻进程取任务

消费者一般用 CLI 方式跑,配合 supervisor 做进程守护。核心是 brPop,它是阻塞的,队列空的时候会挂起等待,不会空转浪费 CPU。

<?php
class QueueConsumer
{
private $redis;
private $queueKey = 'app:task:queue';
private $maxRetry = 3;
public function __construct()
{
$this-&gt;redis = new Redis();
$this-&gt;redis-&gt;connect('127.0.0.1', 6379);
$this-&gt;redis-&gt;auth('your_password');
// 阻塞超时设为 5 秒,方便进程优雅退出
$this-&gt;redis-&gt;setOption(Redis::OPT_READ_TIMEOUT, 5);
}

public function run(): void
{
echo "消费者启动,等待任务…\\n";

while (true) {
// 第二个参数是超时秒数,返回 [key, value] 或空数组
$result = $this-&gt;redis-&gt;brPop([$this-&gt;queueKey], 5);

if (empty($result)) {
continue; // 超时没任务,继续下一轮
}

$task = json_decode($result[1], true);
if (!$task) {
echo "任务格式错误,丢弃\\n";
continue;
}

$this-&gt;handle($task);
}
}

private function handle(array $task): void
{
try {
switch ($task['type']) {
case 'send_welcome_mail':
$this-&gt;sendMail($task['payload']);
break;
default:
echo "未知任务类型: {$task['type']}\\n";
}
echo "任务 {$task['id']} 执行成功\\n";
} catch (Throwable $e) {
$this-&gt;retry($task, $e);
}
}

private function sendMail(array $payload): void
{
// 这里换成你项目里的邮件发送逻辑
sleep(1); // 模拟耗时操作
echo "已发送邮件给 {$payload['email']}\\n";
}

private function retry(array $task, Throwable $e): void
{
$task['retry']++;
echo "任务 {$task['id']} 失败: {$e-&gt;getMessage()}\\n";

if ($task['retry'] &lt; $this-&gt;maxRetry) {
// 重新入队,稍后重试
$this-&gt;redis-&gt;lPush($this-&gt;queueKey, json_encode($task, JSON_UNESCAPED_UNICODE));
} else {
// 超过重试次数,进失败队列
$this-&gt;redis-&gt;lPush('app:task:failed', json_encode($task, JSON_UNESCAPED_UNICODE));
echo "任务 {$task['id']} 已进入失败队列\\n";
}
}
}
(new QueueConsumer())->run();

几个容易踩的坑

代码看着简单,但实际跑起来还是有几个地方要注意:

  • 别用 rPop 轮询。有人图省事写个 while(true) 里套 rPop,队列空的时候疯狂空转,CPU 直接拉满。老老实实用 brPop。
  • 任务要幂等。网络抖动或者进程重启,同一个任务可能被执行两次,发邮件这种操作最好加个去重标记。
  • 消费者要守护。用 supervisor 配置自动重启,进程挂了能拉起来,不然队列会越堆越多。
  • 监控队列长度。用 LLEN 定时检查,堆积太多说明消费者处理不过来,得加进程了。
  • 写在最后

    这套方案不算什么高深技术,但胜在简单够用,几十行代码就能把接口响应时间降下来。当然它也有局限,比如没有 ACK 机制,消费者取走任务后如果进程突然挂掉,这个任务就丢了。如果业务对可靠性要求高,建议直接上 Redis Stream 或者专业消息队列。

    对于大部分中小项目来说,先把同步改异步、把响应速度提上去,比一上来就追求完美架构更实际。等业务量真的上来了,再平滑迁移也不迟。

    赞(0)
    未经允许不得转载:171主机测评 » 用Redis队列秒级提升接口响应速度
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址