消息队列简介
一个完整的队列系统由以下三个组件组成:
队列(Queue)
消息(Message)
处理进程(Worker)
对应的基本工作流程是生产者(业务代码)先将消息数据推送到队列,然后再通过其他的处理进程来消费队列中的消息数据,从而实现生产者和消费者之间的解耦。因此,消息队列非常适用于一些需要异步执行的耗时操作(比如邮件发送、文件上传),或者业务临时的高并发操作(比如秒杀、消息推送),对于提升系统性能和负载非常有效,尤其是 PHP 这种本身不支持并发编程的语言,是实现异步编程的不二之选。
在演示如何实现消息队列之前,我们先来简单介绍下上面的三个组件。
队列
队列其实是一种线性的数据结构,这一点学院君在数据结构篇中已经详细介绍过,这种数据结构有先入先出(FIFO)的特点,因此很适合做生产者和消费者之间的解耦,同时不影响业务逻辑的执行顺序。
在 PHP 中,可以使用原生的数组函数或者 SplQueue 类很轻松地实现队列这种数据结构,不过这里我们介绍的是 Redis,所以还可以借助 Redis 自带的列表类型来实现。
我们可以将上篇教程中的文章浏览数更新操作通过队列异步实现来提升系统性能。为了简化流程,我们通过 post-views-increment
来标识队列名称,推送到队列的消息数据通过文章 ID 进行标识:
// 文章浏览数 +1
public function addViews(Post $post){
// 推送消息数据到队列,通过异步进程处理数据库更新
Redis::rpush('post-views-increment', $post->id);
return ++$post->views;
}
消息
所谓消息,即推送到队列中的数据,通常是一个字符串,如果是非字符串类型,可以通过序列化操作将其转化为字符串,消费端的处理进程从队列中取出消息数据后,可以对其进行解析处理,完成业务逻辑的闭环。
生产者或者消息本身不必关心消费端处理进程如何处理消息数据,消费端的处理进程也不必关心是谁发送的消息,三者是完全解耦的,但是又通过消息数据架起了生产者和消费者之间的桥梁。
消息数据可以在应用内部传递,也可以跨应用传递,跨应用传递通常需要借助第三方的消息队列中间件,比如基于 Redis 实现的队列系统、RabbitMQ、Kafka、RocketMQ 等。
在上面的示例代码中,我们将文章 ID 作为消息数据进行传递。
处理进程
消费端的处理进程通常是一个或者多个常驻内存的进程,它们或订阅或轮询消息队列,如果消息队列不为空,则取出其中的消息数据进行处理。
这里为了简化流程,我们创建一个 Artisan 命令来模拟一个常驻内存的轮询进程作为消息处理器:
php artisan make:command MockQueueWorker
并编写其实现代码如下:
<?php namespace App\Console\Commands;use App\Models\Post;use Illuminate\Console\Command;use Illuminate\Support\Facades\Redis;class MockQueueWorker extends Command{
protected $signature = 'mock:queue-worker';protected $description = 'Mock Queue Worker';public function __construct(){
parent::__construct();
}public function handle(){
$this->info('监听消息队列 post-views-increment...');while (true) {
// 从队列中取出消息数据
$postId = Redis::lpop('post-views-increment');// 将当前文章浏览数 +1,并存储到对应 Sorted Set 的 score 字段if ($postId && Post::newQuery()->where('id', $postId)->increment('views')) {
Redis::zincrby('popular_posts', 1, $postId);$this->info("更新文章 #{$postId} 的浏览数");
}
}
}
}
重点关注 handle
方法,我们通过 while (true)
模拟常驻内存,然后不断轮询 post-views-increment
队列,如果其中有文章 ID 数据,则取出并更新文章浏览数。
这样一来,我们就实现了一个简单的消息队列,启动这个消息处理器:
然后访问任意一篇文章 http://redis.test/posts/1
,就可以在队列处理器窗口看到队列的任务处理记录:
同时在数据库中看到更新后的浏览数,证明队列消息处理成功。
以上流程也是 Laravel 队列系统底层实现的基本原理,有了这个知识储备,接下来看 Laravel 消息队列底层实现会轻松很多。
Laravel 队列系统实现和使用
基本配置
不过,Laravel 提供了更优雅的队列系统实现,不需要我们手动去编写队列、消息和处理进程的实现代码,并且支持不同的队列系统驱动,包括数据库、Beanstalkd、Amazon SQS、Redis 等,这里我们当然以 Redis 为例进行演示。
要在 Laravel 项目中使用 Redis 实现队列系统,只需在配置好 Redis 连接信息后将环境配置文件 .env
中的 QUEUE_CONNECTION
配置值调整为 redis
即可:
QUEUE_CONNECTION=redis
这样一来,Laravel 就可以基于 config/queue.php
中的 redis
配置初始化队列系统了:
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => null,
],
队列系统服务提供者
在 Laravel 应用启动时,会通过 QueueServiceProvider
来注册队列系统相关服务到服务容器:
public function register(){
$this->registerManager();
$this->registerConnection();
$this->registerWorker();
$this->registerListener();
$this->registerFailedJobServices();
}
...
// 队列管理器
protected function registerManager(){
$this->app->singleton('queue', function ($app) {
return tap(new QueueManager($app), function ($manager) {
$this->registerConnectors($manager);
});
});
}
// 默认队列连接,这里根据配置值会初始化为 redis 连接
protected function registerConnection(){
$this->app->singleton('queue.connection', function