基于 Redis 有序集合实现热门浏览文章排行榜

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

在 Redis 系列

准备模型类和数据表

开始之前,我们先创建文章表、模型类和控制器:

在生成的文章表 posts 迁移类中,编写表结构如下:

代码语言:javascript复制
<?php

use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            $table->integer('views')->unsigned()->default(0);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

新增了文章标题、内容和浏览数字段。

.env 中配置数据库连接信息:

代码语言:javascript复制
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=redis_demo
DB_USERNAME=root
DB_PASSWORD=root

创建 redis_demo 数据库,运行 php artisan migrate 在这个数据库中创建 posts 数据表。

热门浏览文章排行榜功能实现

维护基于文章浏览数的有序集合

PostController 中,定义一个文章浏览方法 show

代码语言:javascript复制
use AppModelsPost;
use IlluminateSupportFacadesRedis;

public function show(Post $post)
{
    $post->increment('views');
    if ($post->save()) {
        // 将当前文章浏览数  1,存储到对应 Sorted Set 的 score 字段
        Redis::zincrby('popular_posts', 1, $post->id);
    }
    return 'Show Post #' . $post->id;
}

我们使用 popular_posts 作为热门浏览文章排行榜有序集合的键名,当更新文章模型浏览数字段成功后,调用 Redis 门面的 zincrby 方法,通过 ZINCRBY 指令将对应文章浏览数(score)做 1 操作,有序集合内的文章成员(member)通过文章 ID 进行标识。

这样一来,随着文章的增多,用户浏览量的增长,Redis 底层会维护一个基于文章浏览数进行排序的有序集合,要实现热门浏览文章排行榜,只需要逆序从这个集合获取指定数量的成员即可获取对应的文章 ID 集合。

读取有序集合元素生成排行榜

接下来,我们就来实现这个排行榜。我们限定排行榜的大小是 10,即只显示浏览量最多的前十篇文章,这可以通过 ZREVRANGE 指令实现,对应到 Laravel 代码,我们需要在 PostController 中新增一个 popular 方法如下:

代码语言:javascript复制
// 获取热门文章排行榜
public function popular()
{
    // 获取浏览器最多的前十篇文章
    $postIds = Redis::zrevrange('popular_posts', 0, 9);
    if ($postIds) {
        $idsStr = implode(',', $postIds);
        // 查询结果排序必须和传入时的 ID 排序一致
        $posts = Post::whereIn('id', $postIds)
            ->select(['id', 'title', 'views'])
            ->orderByRaw('field(`id`, ' . $idsStr . ')')
            ->get();
    } else {
        $posts = null;
    }
    dd($posts->toArray());
}

非常简单,通过 Redis 门面调用 zrevrange 方法来执行 ZREVRANGE 指令,并传入有序集合的键名、元素区间,由于集合中存储的元素是文章 ID,所以对于返回的结果,还需要再次到数据库中去查询完整的文章记录,此外,我们还要按照传入的 ID 顺序对返回结果进行排序,否则数据库查询返回的结果顺序又变成基于 ID 值大小的排序了。

这样一来,就可以获取到排行榜中的文章数据了。

我们在 routes/web.php 为上述控制器方法注册路由:

代码语言:javascript复制
Route::get('/posts/popular', [PostController::class, 'popular']);
Route::get('/posts/{post}', [PostController::class, 'show']);

测试热门浏览文章排行榜

最后,我们来测试上述排行榜代码是否可以正常工作。

基本思路是编写一个文章模型工厂生成测试文章,然后随机浏览文章构建基于 Redis 的排行榜有序集合,最后访问排行榜数据。

先创建文章模型工厂:

代码语言:javascript复制
php artisan make:factory PostFactory

编写对应的模型工厂类代码如下:

代码语言:javascript复制
<?php

namespace DatabaseFactories;

use AppModelsPost;
use IlluminateDatabaseEloquentFactoriesFactory;

class PostFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = Post::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'title' => trim($this->faker->sentence, '.'),
            'content' => $this->faker->paragraphs(3, true),
        ];
    }
}

然后我们创建一个 Artisan 命令类用于对文章进行模拟访问:

代码语言:javascript复制
 php artisan make:command MockViewPosts 

编写 MockViewPosts 实现代码如下:

代码语言:javascript复制
<?php

namespace AppConsoleCommands;

use AppModelsPost;
use IlluminateConsoleCommand;
use IlluminateSupportFacadesHttp;
use IlluminateSupportFacadesRedis;

class MockViewPosts extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'mock:view-posts';

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

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

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        // 1、先清空 posts 表
        Post::truncate();
        // 2、删除对应的 Redis 键
        Redis::del('popular_posts');
        // 3、生成 100 篇测试文章
        Post::factory()->count(100)->create();
        // 4、模拟对所有文章进行 10000 次随机访问
        for ($i = 0; $i < 10000; $i  ) {
            $postId = mt_rand(1, 100);
            $response = Http::get('http://redis-demo.test/posts/' . $postId);
            $this->info($response->body());
        }
    }
}

这里我们使用了 Laravel 自带的 HTTP 客户端发起对 /posts/{post} 路由的模拟访问,所以需要先安装 Guzzle 这个 HTTP 扩展包才可以正常访问测试路由:

代码语言:javascript复制
composer require guzzlehttp/guzzle 

运行 php artisan mock:view-posts,在浏览器中访问 http://redis-demo.test/posts/popular,就可以看到可以返回热门文章排行榜数据了:

本系列教程首发在学院君网站(xueyuanjun.com),你可以点击页面左下角阅读原文链接查看最新更新的教程。

0 人点赞