基于 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 条评论
Laravel 8
我这里没有配置你虚拟主机,使用的环境是Laravel Sail。
请问学员君: 访问 http://localhost/posts/popular 一直提示 404 | NOT FOUND,
但是在路由文件中,但是访问 http://localhost/posts/1,正常访问~
新建测试路由,也是可以的
Route::get('/hello/world', function(){ print "hello world"; });
发现修改路由: Route::get('/post/popular', [PostController::class, 'popular']); 将posts去掉s就可以访问
这是什么情况?没有调试的思路?
学院君请教一个问题我按文中所说创建一个循环请求,但是报错,按照评论中的配置更改了extra_hosts值还是无法请求通,在php的容器中可以ping通blog.cn,很是费解!望看到后帮忙解答,谢谢~Illuminate\Http\Client\ConnectionException cURL error 7: Failed to connect to blog.cn port 80: Connection refused 使用的是dnmp的环境
目前已在容器中跑起来了,刚才是在phpstorm中使用php镜像的解释器一直没有跑通
https://segmentfault.com/a/1190000018990416 看看这里,我也有同样的问题,这样处理解决了。
在执行mock:view-posts 命令时遇到了这个问题,app.php里 alias的Redis也移除了。
Class 'Redis' not found
at vendor/laravel/framework/src/Illuminate/Redis/Connectors/PhpRedisConnector.php:75 71▕ * @throws \LogicException 72▕ */ 73▕ protected function createClient(array $config) 74▕ { ➜ 75▕ return tap(new Redis, function ($client) use ($config) { 76▕ if ($client instanceof RedisFacade) { 77▕ throw new LogicException( 78▕ extension_loaded('redis') 79▕ ? 'Please remove or rename the Redis facade alias in your "app" configuration file in order to avoid collision with the PHP Redis extension.'
• A class import is missing: You have a missing class import. Try importing this class:
Illuminate\Support\Facades\Redis
.laravel6