基于 Redis 有序集合实现 Laravel 热门浏览文章排行榜功能
在 Redis 系列开篇中介绍基本数据结构及使用时,学院君就已经给大家介绍过热门文章排行榜的基本实现原理 —— 使用 Redis 自带的 Sorted Set 实现这个功能。今天我们以 Laravel 项目热门浏览文章排行榜为例进行实战演示。
准备模型类和数据表
开始之前,我们先创建文章表、模型类和控制器:
在生成的文章表 posts
迁移类中,编写表结构如下:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
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
中配置数据库连接信息:
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
:
use App\Models\Post;
use Illuminate\Support\Facades\Redis;
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
方法如下:
// 获取热门文章排行榜
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
为上述控制器方法注册路由:
Route::get('/posts/popular', [PostController::class, 'popular']);
Route::get('/posts/{post}', [PostController::class, 'show']);
测试热门浏览文章排行榜
最后,我们来测试上述排行榜代码是否可以正常工作。
基本思路是编写一个文章模型工厂生成测试文章,然后随机浏览文章构建基于 Redis 的排行榜有序集合,最后访问排行榜数据。
先创建文章模型工厂:
php artisan make:factory PostFactory
编写对应的模型工厂类代码如下:
<?php
namespace Database\Factories;
use App\Models\Post;
use Illuminate\Database\Eloquent\Factories\Factory;
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 命令类用于对文章进行模拟访问:
php artisan make:command MockViewPosts
编写 MockViewPosts
实现代码如下:
<?php
namespace App\Console\Commands;
use App\Models\Post;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Redis;
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 扩展包才可以正常访问测试路由:
composer require guzzlehttp/guzzle
运行 php artisan mock:view-posts
,在浏览器中访问 http://redis-demo.test/posts/popular
,就可以看到可以返回热门文章排行榜数据了:
16 Comments
学院君有遇到这个问题吗,就是本地执行 Artisan 命令的时候数据库和 Redis 的 host 要设置为 127.0.0.1,但是浏览器访问的时候 host 要设置为 docker 的容器名,不然会报错,比如 Redis 会报下面的错 Averias\RedisBloom\Exception\RedisClientException php_network_getaddresses: getaddrinfo failed: nodename nor servname provided, or not known
是啊 因为你的应用是运行在 Docker 容器里的 一个小技巧是新建一个 .env.local 配置文件配置数据库和Redis主机为127.0.0.1,然后运行 artisan 命令时带上 --env=local 选项指定使用local配置 .env 仍然配置成容器地址 就可以在浏览器正常访问了 当然如果使用 sail 就更简单了 直接通过 sail 运行 artisan 命令即可
赞,这个技巧很受用。不过我发现这里的 host 配置成 ifconfig 中的 ip 就可以同时在浏览器和 Artisan 命令中正常运行。另外 sail 命令我还没使用过。
此文中 排行榜数据存redis和存数据库有什么区别,redis的数据还得拿出来到数据库取出其他信息
Redis 性能更好 而且排行榜数据基本都是有时间维度的 没必要持久化 比如日排行榜 周排行榜 月排行榜 年排行榜 你觉得数据库去区分维护还有什么优势吗
学院君 root@c79737c0109:/var/www/redis-demo# php artisan mock:view-posts
Illuminate\Http\Client\ConnectionException
cURL error 7: Failed to connect to redis-demo.test port 8099: Connection refused (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for http://redis-demo.test:8099/posts/1 这种怎么解决呢,也换成ip了,还是不行
是在docker容器里面访问的吗
嗯,是的
参考下这个
Docker 容器内部不同域名之间访问报错:
解决办法是在
docker-compose.yml
的php-fpm
镜像构建中新增配置:然后重新 build 镜像:
这样就可以了实现
app.test
与blog.test
之间的访问了。学院君你这个教程是用的laravel哪个版本啊