基于全局中间件 + Redis 实现 Laravel 全站访问计数器功能

2021-01-08 15:41:06 浏览数 (1)

上篇教程学院君已经给大家简单介绍了 Redis 的基本数据结构和常见使用场景,接下来我们就以 Laravel 项目为例来演示如何实现这些常见的业务功能。

首先从最简单的计数器开始,学院君这里将通过 Redis 来实现一个全站访问统计计数器。

你可以先阅读下 Laravel Redis 文档先熟悉下。

安装 PHP Redis 扩展

开始之前,我们先新建一个 Laravel 示例项目 redis-demo

代码语言:javascript复制
laravel new redis-demo

要想在 Laravel/PHP 项目中使用 Redis,需要先安装 PHP Redis 扩展,在 Mac/Linux 系统中可以通过 pecl install redis 快速安装,如果使用的是 Laradock 集成开发环境,只需要在 Laradock 的 .env 环境配置文件中启用 Redis 扩展:

代码语言:javascript复制
WORKSPACE_INSTALL_PHPREDIS=true
PHP_FPM_INSTALL_PHPREDIS=true

然后为 redis-demo 项目配置虚拟域名 redis-demo.test,重新构建 nginx 镜像并重启 nginx 容器服务,最后通过打印 phpinfo 信息看到列表中包含 redis,则表明扩展安装成功:

此外,还可以通过 Composer 安装 predis 扩展包在 Laravel/PHP 项目中使用 Redis,不过作者宣称已停止更新该扩展包,所以推荐使用 PHP Redis 扩展包,且该扩展包基于 C 语言编写,性能也更好。

如果你使用的是 Laravel 官方提供的 Sail 构建 Docker 开发环境,则 PHP Redis 扩展包已经默认安装:

Redis 客户端连接与配置

redis-demo 项目根目录下的 .env 环境配置文件中配置 Redis 连接信息:

代码语言:javascript复制
REDIS_CLIENT=phpredis
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379

就可以在 Laravel 项目中与 Redis 服务器进行交互了,我们可以通过 Laravel 提供的 Redis 门面获取 Redis 客户端连接:

代码语言:javascript复制
Route::get('/connection', function () {
    dd(IlluminateSupportFacadesRedis::connection());
});

当然,门面本质上是对服务容器中 Redis 绑定对象的代理,对应的绑定代码位于 IlluminateRedisRedisServiceProviderregister 方法中:

代码语言:javascript复制
public function register()
{
    $this->app->singleton('redis', function ($app) {
        $config = $app->make('config')->get('database.redis', []);
        // 使用 phpredis 还是 predis 连接器取决于 database.redis.client 配置,默认值是 phpredis
        return new RedisManager($app, Arr::pull($config, 'client', 'phpredis'), $config);
    });

    $this->app->bind('redis.connection', function ($app) {
        return $app['redis']->connection();
    });
}

所以你也可以通过服务容器解析访问 Redis 连接实例:

代码语言:javascript复制
dd(app('redis')->connection());
// 或者 
dd(app('redis.connection'));

在浏览器中访问 http://redis-demo.test/connection,即可查看到对应的打印结果:

可以看到,由于 REDIS_CLIENT 配置值是 phpredis,所以使用的是 PhpRedisConnector 与 Redis 服务器建立客户端连接(如果配置为 predis,则对应的类文件是 PredisConnector)。

与服务端建立连接的配置值位于 config 属性中,其中包含了 Redis 服务器 IP(redis 容器)、端口号(6379)、密码(默认为空)和数据库信息(默认是 0)等,此外还有一个 options 属性指定额外的连接选项,cluster 表示集群,prefix 表示键名前缀,所有这些配置项都是在 config/database.php 中完成配置的:

代码语言:javascript复制
'redis' => [

    'client' => env('REDIS_CLIENT', 'phpredis'),

    'options' => [
        'cluster' => env('REDIS_CLUSTER', 'redis'),
        'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
    ],

    'default' => [
        'url' => env('REDIS_URL'),
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', '6379'),
        'database' => env('REDIS_DB', '0'),
    ],

    'cache' => [
        'url' => env('REDIS_URL'),
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', '6379'),
        'database' => env('REDIS_CACHE_DB', '1'),
    ],

],

Redis 计数器功能实现

完成上述安装和配置工作后,接下来,我们就可以正式基于 Redis 实现全站访问计数器功能了。

我们可以基于 Laravel 全局中间件结合 Redis 的 INCR 指令来实现这个功能,创建一个名为 SiteVisits 的中间件:

代码语言:javascript复制
 php artisan make:middleware SiteVisits

编写其实现代码如下:

代码语言:javascript复制
<?php

namespace AppHttpMiddleware;

use Closure;
use IlluminateHttpRequest;
use IlluminateSupportFacadesRedis;

class SiteVisits
{
    /**
     * Handle an incoming request.
     *
     * @param  IlluminateHttpRequest  $request
     * @param  Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        Redis::incr('site_total_visits');
        return $next($request);
    }
}

非常简单,我们只需要在 handle 方法中通过 Redis 门面将 Redis 指令作为静态方法名进行调用(底层会通过网络调用将其转化为真正的 Redis 指令执行),传入键名作为参数即可。

全局访问计数器是一个自增的计数操作,每次自增步长是 1,所以调用 incr 方法即可,如果首次调用键值不存在,则先将其初始化为 0,再进行 1 操作。

此外,Redis 的 INCR 指令是原子操作,可以保证并发安全,所以用在这里再合适不过了。

app/Http/Kernel.php 中应用这个全局中间件:

代码语言:javascript复制
protected $middleware = [
    ...
    AppHttpMiddlewareSiteVisits::class,
];

这样一来,每次访问 Laravel Web 路由,就可以通过这个中间件统计全局访问量了。

获取 Redis 计数器的值

我们在 routes/web.php 中注册一个路由获取计数器的值进行测试:

代码语言:javascript复制
Route::get('/site_visits', function () {
    return '网站全局访问量:' . IlluminateSupportFacadesRedis::get('site_total_visits');
});

在浏览器中访问该路由,每次刷新页面计数器的值都会 1,说明计数器工作正常。

不过,如果你通过 Redis 命令行客户端进行访问的话,直接通过 site_total_visits 是无法获取到计数器的值的:

因为 Laravel 会给 Redis 所有键设置一个前缀 prefix,其默认值是 laravel_database_,所以在 Redis 底层,需要通过 laravel_database_site_total_visits 才能获取到对应计数器的值:

如果你初来乍到,不知道前缀是什么,可以通过 Redis 的 KEYS 指令进行模糊匹配:

然后通过匹配结果再去执行 GET 指令获取计数器的值。

那 Laravel 代码中为何可以直接使用 site_total_visits 键进行访问呢?因为在建立 Redis 连接的时候,会将键名前缀设置到 Redis 的连接属性 Redis::OPT_PREFIX 上(源码位于 PhpRedisConnector 中):

代码语言:javascript复制
if (! empty($config['prefix'])) {
    $client->setOption(Redis::OPT_PREFIX, $config['prefix']);
}

这样一来,每次不管是设置还是获取键值,都会自动应用这个键名前缀。

0 人点赞