消息队列实战之基于 Redis 实现 消息队列系统及底层源码探究

消息队列简介

一个完整的队列系统由以下三个组件组成:

  • 队列(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
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'mock:queue-worker';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Mock Queue Worker';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    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->registerFailedJobServi
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值